diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a8919337a9..cdec442276 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ /beacon_node/network/ @jxs /beacon_node/lighthouse_network/ @jxs +/beacon_node/store/ @michaelsproul diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/default-issue-template.md similarity index 79% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/default-issue-template.md index d73b9ff6f0..784add20f3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/default-issue-template.md @@ -1,3 +1,12 @@ +--- +name: Default issue template +about: Use this template for all issues +title: '' +labels: '' +assignees: '' + +--- + ## Description Please provide a brief description of the issue. diff --git a/.github/mergify.yml b/.github/mergify.yml index 73267904b8..0b917b2546 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,8 +1,10 @@ pull_request_rules: - name: Ask to resolve conflict conditions: + - -closed - conflict - -author=dependabot[bot] + - label=ready-for-review - or: - -draft # Don't report conflicts on regular draft. - and: # Do report conflicts on draft that are scheduled for the next major release. @@ -12,6 +14,64 @@ pull_request_rules: comment: message: This pull request has merge conflicts. Could you please resolve them @{{author}}? 🙏 + label: + add: + - waiting-on-author + remove: + - ready-for-review + + - name: Ask to resolve CI failures + conditions: + - -closed + - label=ready-for-review + - or: + - check-skipped=test-suite-success + - check-skipped=local-testnet-success + - check-failure=test-suite-success + - check-failure=local-testnet-success + actions: + comment: + message: Some required checks have failed. Could you please take a look @{{author}}? 🙏 + label: + add: + - waiting-on-author + remove: + - ready-for-review + + - name: Update labels when PR is unblocked + conditions: + - -closed + - -draft + - label=waiting-on-author + - -conflict + # Unfortunately, it doesn't look like there's an easy way to check for PRs pending + # CI workflows approvals. + - check-success=test-suite-success + - check-success=local-testnet-success + # Update the label only if there are no more change requests from any reviewers and no unresolved threads. + # This rule ensures that a PR with passing CI can be marked as `waiting-on-author`. + - "#changes-requested-reviews-by = 0" + - "#review-threads-unresolved = 0" + actions: + label: + remove: + - waiting-on-author + add: + - ready-for-review + + - name: Close stale pull request after 30 days of inactivity + conditions: + - -closed + - label=waiting-on-author + - updated-at<=30 days ago + actions: + close: + message: > + Hi @{{author}}, this pull request has been closed automatically due to 30 days of inactivity. + If you’d like to continue working on it, feel free to reopen at any time. + label: + add: + - stale - name: Approve trivial maintainer PRs conditions: @@ -45,6 +105,10 @@ queue_rules: {{ body | get_section("## Proposed Changes", "") }} + + {% for commit in commits | unique(attribute='email_author') %} + Co-Authored-By: {{ commit.author }} <{{ commit.email_author }}> + {% endfor %} queue_conditions: - "#approved-reviews-by >= 1" - "check-success=license/cla" diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index e9db3b6ab1..2834d9f36a 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -13,7 +13,7 @@ jobs: build-and-upload-to-s3: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 diff --git a/.github/workflows/docker-reproducible.yml b/.github/workflows/docker-reproducible.yml new file mode 100644 index 0000000000..f3479e9468 --- /dev/null +++ b/.github/workflows/docker-reproducible.yml @@ -0,0 +1,176 @@ +name: docker-reproducible + +on: + push: + branches: + - unstable + - stable + tags: + - v* + workflow_dispatch: # allows manual triggering for testing purposes and skips publishing an image + +env: + DOCKER_REPRODUCIBLE_IMAGE_NAME: >- + ${{ github.repository_owner }}/lighthouse-reproducible + DOCKER_PASSWORD: ${{ secrets.DH_KEY }} + DOCKER_USERNAME: ${{ secrets.DH_ORG }} + +jobs: + extract-version: + name: extract version + runs-on: ubuntu-22.04 + steps: + - name: Extract version + run: | + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + # It's a tag (e.g., v1.2.3) + VERSION="${GITHUB_REF#refs/tags/}" + elif [[ "${{ github.ref }}" == refs/heads/stable ]]; then + # stable branch -> latest + VERSION="latest" + elif [[ "${{ github.ref }}" == refs/heads/unstable ]]; then + # unstable branch -> latest-unstable + VERSION="latest-unstable" + else + # For manual triggers from other branches and will not publish any image + VERSION="test-build" + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + id: extract_version + outputs: + VERSION: ${{ steps.extract_version.outputs.VERSION }} + + verify-and-build: + name: verify reproducibility and build + needs: extract-version + strategy: + matrix: + arch: [amd64, arm64] + include: + - arch: amd64 + rust_target: x86_64-unknown-linux-gnu + rust_image: >- + rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816 + platform: linux/amd64 + runner: ubuntu-22.04 + - arch: arm64 + rust_target: aarch64-unknown-linux-gnu + rust_image: >- + rust:1.88-bullseye@sha256:8b22455a7ce2adb1355067638284ee99d21cc516fab63a96c4514beaf370aa94 + platform: linux/arm64 + runner: ubuntu-22.04-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker + + - name: Verify reproducible builds (${{ matrix.arch }}) + run: | + # Build first image + docker build -f Dockerfile.reproducible \ + --platform ${{ matrix.platform }} \ + --build-arg RUST_TARGET="${{ matrix.rust_target }}" \ + --build-arg RUST_IMAGE="${{ matrix.rust_image }}" \ + -t lighthouse-verify-1-${{ matrix.arch }} . + + # Extract binary from first build + docker create --name extract-1-${{ matrix.arch }} lighthouse-verify-1-${{ matrix.arch }} + docker cp extract-1-${{ matrix.arch }}:/lighthouse ./lighthouse-1-${{ matrix.arch }} + docker rm extract-1-${{ matrix.arch }} + + # Clean state for second build + docker buildx prune -f + docker system prune -f + + # Build second image + docker build -f Dockerfile.reproducible \ + --platform ${{ matrix.platform }} \ + --build-arg RUST_TARGET="${{ matrix.rust_target }}" \ + --build-arg RUST_IMAGE="${{ matrix.rust_image }}" \ + -t lighthouse-verify-2-${{ matrix.arch }} . + + # Extract binary from second build + docker create --name extract-2-${{ matrix.arch }} lighthouse-verify-2-${{ matrix.arch }} + docker cp extract-2-${{ matrix.arch }}:/lighthouse ./lighthouse-2-${{ matrix.arch }} + docker rm extract-2-${{ matrix.arch }} + + # Compare binaries + echo "=== Comparing binaries ===" + echo "Build 1 SHA256: $(sha256sum lighthouse-1-${{ matrix.arch }})" + echo "Build 2 SHA256: $(sha256sum lighthouse-2-${{ matrix.arch }})" + + if cmp lighthouse-1-${{ matrix.arch }} lighthouse-2-${{ matrix.arch }}; then + echo "Reproducible build verified for ${{ matrix.arch }}" + else + echo "Reproducible build FAILED for ${{ matrix.arch }}" + echo "BLOCKING RELEASE: Builds are not reproducible!" + echo "First 10 differences:" + cmp -l lighthouse-1-${{ matrix.arch }} lighthouse-2-${{ matrix.arch }} | head -10 + exit 1 + fi + + # Clean up verification artifacts but keep one image for publishing + rm -f lighthouse-*-${{ matrix.arch }} + docker rmi lighthouse-verify-1-${{ matrix.arch }} || true + + # Re-tag the second image for publishing (we verified it's identical to first) + VERSION=${{ needs.extract-version.outputs.VERSION }} + FINAL_TAG="${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}" + docker tag lighthouse-verify-2-${{ matrix.arch }} "$FINAL_TAG" + + - name: Log in to Docker Hub + if: ${{ github.event_name != 'workflow_dispatch' }} + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} + + - name: Push verified image (${{ matrix.arch }}) + if: ${{ github.event_name != 'workflow_dispatch' }} + run: | + VERSION=${{ needs.extract-version.outputs.VERSION }} + IMAGE_TAG="${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}" + docker push "$IMAGE_TAG" + + - name: Clean up local images + run: | + docker rmi lighthouse-verify-2-${{ matrix.arch }} || true + VERSION=${{ needs.extract-version.outputs.VERSION }} + docker rmi "${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }}:${VERSION}-${{ matrix.arch }}" || true + + - name: Upload verification artifacts (on failure) + if: failure() + uses: actions/upload-artifact@v4 + with: + name: verification-failure-${{ matrix.arch }} + path: | + lighthouse-*-${{ matrix.arch }} + + create-manifest: + name: create multi-arch manifest + runs-on: ubuntu-22.04 + needs: [extract-version, verify-and-build] + if: ${{ github.event_name != 'workflow_dispatch' }} + steps: + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} + + - name: Create and push multi-arch manifest + run: | + IMAGE_NAME=${{ env.DOCKER_REPRODUCIBLE_IMAGE_NAME }} + VERSION=${{ needs.extract-version.outputs.VERSION }} + + # Create manifest for the version tag + docker manifest create \ + ${IMAGE_NAME}:${VERSION} \ + ${IMAGE_NAME}:${VERSION}-amd64 \ + ${IMAGE_NAME}:${VERSION}-arm64 + + docker manifest push ${IMAGE_NAME}:${VERSION} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e768208973..415f4db0e6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -64,7 +64,7 @@ jobs: VERSION: ${{ needs.extract-version.outputs.VERSION }} VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Update Rust if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 7e8d9135dd..cc7775c083 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Run mdbook server run: | diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index 1cd2f24548..9992273e0a 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -14,13 +14,13 @@ concurrency: jobs: dockerfile-ubuntu: - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build Docker image run: | - docker build --build-arg FEATURES=portable -t lighthouse:local . + docker build --build-arg FEATURES=portable,spec-minimal -t lighthouse:local . docker save lighthouse:local -o lighthouse-docker.tar - name: Upload Docker image artifact @@ -31,10 +31,10 @@ jobs: retention-days: 3 run-local-testnet: - runs-on: ubuntu-22.04 + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} needs: dockerfile-ubuntu steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Kurtosis run: | @@ -52,23 +52,22 @@ jobs: - name: Load Docker image run: docker load -i lighthouse-docker.tar - - name: Start local testnet - run: ./start_local_testnet.sh -e local -c -b false && sleep 60 + - name: Start local testnet with Assertoor + run: ./start_local_testnet.sh -e local-assertoor -c -a -b false && sleep 60 working-directory: scripts/local_testnet + - name: Await Assertoor test result + id: assertoor_test_result + uses: ethpandaops/assertoor-github-action@v1 + with: + kurtosis_enclave_name: local-assertoor + - name: Stop local testnet and dump logs - run: ./stop_local_testnet.sh local - working-directory: scripts/local_testnet - - - name: Start local testnet with blinded block production - run: ./start_local_testnet.sh -e local-blinded -c -p -b false && sleep 60 - working-directory: scripts/local_testnet - - - name: Stop local testnet and dump logs - run: ./stop_local_testnet.sh local-blinded + run: ./stop_local_testnet.sh local-assertoor working-directory: scripts/local_testnet - name: Upload logs artifact + if: always() uses: actions/upload-artifact@v4 with: name: logs-local-testnet @@ -76,11 +75,34 @@ jobs: scripts/local_testnet/logs retention-days: 3 + - name: Return Assertoor test result + shell: bash + run: | + test_result="${{ steps.assertoor_test_result.outputs.result }}" + test_status=$( + cat <<"EOF" + ${{ steps.assertoor_test_result.outputs.test_overview }} + EOF + ) + failed_test_status=$( + cat <<"EOF" + ${{ steps.assertoor_test_result.outputs.failed_test_details }} + EOF + ) + + echo "Test Result: $test_result" + echo "$test_status" + if ! [ "$test_result" == "success" ]; then + echo "Failed Test Task Status:" + echo "$failed_test_status" + exit 1 + fi + doppelganger-protection-success-test: needs: dockerfile-ubuntu - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Kurtosis run: | @@ -104,6 +126,7 @@ jobs: working-directory: scripts/tests - name: Upload logs artifact + if: always() uses: actions/upload-artifact@v4 with: name: logs-doppelganger-protection-success @@ -113,9 +136,9 @@ jobs: doppelganger-protection-failure-test: needs: dockerfile-ubuntu - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Kurtosis run: | @@ -139,6 +162,7 @@ jobs: working-directory: scripts/tests - name: Upload logs artifact + if: always() uses: actions/upload-artifact@v4 with: name: logs-doppelganger-protection-failure @@ -146,19 +170,106 @@ jobs: scripts/local_testnet/logs retention-days: 3 + # Tests checkpoint syncing to a live network (current fork) and a running devnet (usually next scheduled fork) + checkpoint-sync-test: + name: checkpoint-sync-test-${{ matrix.network }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} + needs: dockerfile-ubuntu + if: contains(github.event.pull_request.labels.*.name, 'syncing') + continue-on-error: true + strategy: + matrix: + network: [sepolia] + steps: + - uses: actions/checkout@v5 + + - name: Install Kurtosis + run: | + echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list + sudo apt update + sudo apt install -y kurtosis-cli + kurtosis analytics disable + + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: lighthouse-docker + path: . + + - name: Load Docker image + run: docker load -i lighthouse-docker.tar + + - name: Run the checkpoint sync test script + run: | + ./checkpoint-sync.sh "sync-${{ matrix.network }}" "checkpoint-sync-config-${{ matrix.network }}.yaml" + working-directory: scripts/tests + + - name: Upload logs artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-checkpoint-sync-${{ matrix.network }} + path: | + scripts/local_testnet/logs + retention-days: 3 + + # Test syncing from genesis on a local testnet. Aims to cover forward syncing both short and long distances. + genesis-sync-test: + name: genesis-sync-test-${{ matrix.fork }}-${{ matrix.offline_secs }}s + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} + needs: dockerfile-ubuntu + strategy: + matrix: + fork: [electra, fulu] + offline_secs: [120, 300] + steps: + - uses: actions/checkout@v5 + + - name: Install Kurtosis + run: | + echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list + sudo apt update + sudo apt install -y kurtosis-cli + kurtosis analytics disable + + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: lighthouse-docker + path: . + + - name: Load Docker image + run: docker load -i lighthouse-docker.tar + + - name: Run the genesis sync test script + run: | + ./genesis-sync.sh "sync-${{ matrix.fork }}-${{ matrix.offline_secs }}s" "genesis-sync-config-${{ matrix.fork }}.yaml" "${{ matrix.fork }}" "${{ matrix.offline_secs }}" + working-directory: scripts/tests + + - name: Upload logs artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-genesis-sync-${{ matrix.fork }}-${{ matrix.offline_secs }}s + path: | + scripts/local_testnet/logs + retention-days: 3 # This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether # a PR is safe to merge. New jobs should be added here. local-testnet-success: name: local-testnet-success - runs-on: ubuntu-latest + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} needs: [ 'dockerfile-ubuntu', 'run-local-testnet', 'doppelganger-protection-success-test', 'doppelganger-protection-failure-test', + 'genesis-sync-test' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check that success job is dependent on all others - run: ./scripts/ci/check-success-job.sh ./.github/workflows/local-testnet.yml local-testnet-success + run: | + exclude_jobs='checkpoint-sync-test' + ./scripts/ci/check-success-job.sh ./.github/workflows/local-testnet.yml local-testnet-success "$exclude_jobs" diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 0000000000..be52c5b84d --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,135 @@ +# We only run tests on `RECENT_FORKS` on CI. To make sure we don't break prior forks, we run nightly tests to cover all prior forks. +name: nightly-tests + +on: + schedule: + # Run at 8:30 AM UTC every day + - cron: '30 8 * * *' + workflow_dispatch: # Allow manual triggering + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Deny warnings in CI + # Disable debug info (see https://github.com/sigp/lighthouse/issues/4005) + RUSTFLAGS: "-D warnings -C debuginfo=0" + # Prevent Github API rate limiting. + LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} + # Disable incremental compilation + CARGO_INCREMENTAL: 0 + # Enable portable to prevent issues with caching `blst` for the wrong CPU type + TEST_FEATURES: portable + +jobs: + setup-matrix: + name: setup-matrix + runs-on: ubuntu-latest + outputs: + forks: ${{ steps.set-matrix.outputs.forks }} + steps: + - name: Set matrix + id: set-matrix + run: | + # All prior forks to cover in nightly tests. This list should be updated when we remove a fork from `RECENT_FORKS`. + echo 'forks=["phase0", "altair", "bellatrix", "capella", "deneb"]' >> $GITHUB_OUTPUT + + beacon-chain-tests: + name: beacon-chain-tests + needs: setup-matrix + runs-on: 'ubuntu-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run beacon_chain tests for ${{ matrix.fork }} + run: make test-beacon-chain-${{ matrix.fork }} + timeout-minutes: 60 + + http-api-tests: + name: http-api-tests + needs: setup-matrix + runs-on: 'ubuntu-latest' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run http_api tests for ${{ matrix.fork }} + run: make test-http-api-${{ matrix.fork }} + timeout-minutes: 60 + + op-pool-tests: + name: op-pool-tests + needs: setup-matrix + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run operation_pool tests for ${{ matrix.fork }} + run: make test-op-pool-${{ matrix.fork }} + timeout-minutes: 60 + + network-tests: + name: network-tests + needs: setup-matrix + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + fork: ${{ fromJson(needs.setup-matrix.outputs.forks) }} + fail-fast: false + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Create CI logger dir + run: mkdir ${{ runner.temp }}/network_test_logs + - name: Run network tests for ${{ matrix.fork }} + run: make test-network-${{ matrix.fork }} + timeout-minutes: 60 + env: + TEST_FEATURES: portable + CI_LOGGER_DIR: ${{ runner.temp }}/network_test_logs + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: network_test_logs_${{ matrix.fork }} + path: ${{ runner.temp }}/network_test_logs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de4fd29409..f7b65f07c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,9 +32,7 @@ jobs: matrix: arch: [aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, - x86_64-apple-darwin, - aarch64-apple-darwin, - x86_64-windows] + aarch64-apple-darwin] include: - arch: aarch64-unknown-linux-gnu runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} @@ -42,38 +40,19 @@ jobs: - arch: x86_64-unknown-linux-gnu runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} profile: maxperf - - arch: x86_64-apple-darwin - runner: macos-13 - profile: maxperf - arch: aarch64-apple-darwin runner: macos-14 profile: maxperf - - arch: x86_64-windows - runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "release"]') || 'windows-2019' }} - profile: maxperf runs-on: ${{ matrix.runner }} needs: extract-version steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable - # ============================== - # Windows dependencies - # ============================== - - - uses: KyleMayes/install-llvm-action@v1 - if: env.SELF_HOSTED_RUNNERS == 'false' && startsWith(matrix.arch, 'x86_64-windows') - with: - version: "17.0" - directory: ${{ runner.temp }}/llvm - - name: Set LIBCLANG_PATH - if: startsWith(matrix.arch, 'x86_64-windows') - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV - # ============================== # Builds # ============================== @@ -94,20 +73,11 @@ jobs: if: contains(matrix.arch, 'unknown-linux-gnu') run: mv target/${{ matrix.arch }}/${{ matrix.profile }}/lighthouse ~/.cargo/bin/lighthouse - - name: Build Lighthouse for x86_64-apple-darwin - if: matrix.arch == 'x86_64-apple-darwin' - run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - - name: Build Lighthouse for aarch64-apple-darwin if: matrix.arch == 'aarch64-apple-darwin' run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - - name: Build Lighthouse for Windows - if: matrix.arch == 'x86_64-windows' - run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - - name: Configure GPG and create artifacts - if: startsWith(matrix.arch, 'x86_64-windows') != true env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} @@ -126,20 +96,6 @@ jobs: done mv *tar.gz* .. - - name: Configure GPG and create artifacts Windows - if: startsWith(matrix.arch, 'x86_64-windows') - env: - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: | - echo $env:GPG_SIGNING_KEY | gpg --batch --import - mkdir artifacts - move $env:USERPROFILE/.cargo/bin/lighthouse.exe ./artifacts - cd artifacts - tar -czf lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz lighthouse.exe - gpg --passphrase "$env:GPG_PASSPHRASE" --batch --pinentry-mode loopback -ab lighthouse-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz - move *tar.gz* .. - # ======================================================================= # Upload artifacts # This is required to share artifacts between different jobs @@ -168,7 +124,7 @@ jobs: steps: # This is necessary for generating the changelog. It has to come before "Download Artifacts" or else it deletes the artifacts. - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -244,11 +200,9 @@ jobs: | System | Architecture | Binary | PGP Signature | |:---:|:---:|:---:|:---| - | Apple logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) | | Apple logo | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) | | Linux logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) | | Raspberrypi logo | aarch64 | [lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) | - | Windows logo | x86_64 | [lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/lighthouse-${{ env.VERSION }}-x86_64-windows.tar.gz.asc) | | | | | | | **System** | **Option** | - | **Resource** | | Docker logo | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}/tags?page=1&ordering=last_updated&name=${{ env.VERSION }}) | [${{ env.IMAGE_NAME }}](https://hub.docker.com/r/${{ env.IMAGE_NAME }}) | diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index a94a19900c..7344a9367b 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -22,8 +22,6 @@ env: # NOTE: this token is a personal access token on Jimmy's account due to the default GITHUB_TOKEN # not having access to other repositories. We should eventually devise a better solution here. LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} - # Enable self-hosted runners for the sigp repo only. - SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} # Disable incremental compilation CARGO_INCREMENTAL: 0 # Enable portable to prevent issues with caching `blst` for the wrong CPU type @@ -78,17 +76,15 @@ jobs: name: release-tests-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' - # Use self-hosted runners only on the sigp repo. - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Set Java version to 21. (required since Web3Signer 24.12.0). - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '21' - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 with: channel: stable @@ -97,58 +93,25 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' uses: foundry-rs/foundry-toolchain@v1 with: version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run tests in release - run: make nextest-release - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats - release-tests-windows: - name: release-tests-windows - needs: [check-labels] - if: needs.check-labels.outputs.skip_ci != 'true' - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} - steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - bins: cargo-nextest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Install make - if: env.SELF_HOSTED_RUNNERS == 'false' - run: choco install -y make - - name: Set LIBCLANG_PATH - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV - - name: Run tests in release - run: make nextest-release + run: make test-release - name: Show cache stats if: env.SELF_HOSTED_RUNNERS == 'true' + continue-on-error: true run: sccache --show-stats beacon-chain-tests: name: beacon-chain-tests needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' - # Use self-hosted runners only on the sigp repo. - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 with: channel: stable @@ -156,9 +119,23 @@ jobs: bins: cargo-nextest - name: Run beacon_chain tests for all known forks run: make test-beacon-chain - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + http-api-tests: + name: http-api-tests + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v5 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run http_api tests for all recent forks + run: make test-http-api op-pool-tests: name: op-pool-tests needs: [check-labels] @@ -167,7 +144,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -184,7 +161,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -196,9 +173,10 @@ jobs: - name: Run network tests for all known forks run: make test-network env: - TEST_FEATURES: portable,ci_logger + TEST_FEATURES: portable CI_LOGGER_DIR: ${{ runner.temp }}/network_test_logs - name: Upload logs + if: always() uses: actions/upload-artifact@v4 with: name: network_test_logs @@ -212,7 +190,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -225,35 +203,29 @@ jobs: name: debug-tests-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' - # Use self-hosted runners only on the sigp repo. - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 with: channel: stable bins: cargo-nextest - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' uses: foundry-rs/foundry-toolchain@v1 with: version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run tests in debug - run: make nextest-debug - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + run: make test-debug state-transition-vectors-ubuntu: name: state-transition-vectors-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -265,31 +237,26 @@ jobs: name: ef-tests-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' - # Use self-hosted runners only on the sigp repo. - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 with: channel: stable cache-target: release bins: cargo-nextest - name: Run consensus-spec-tests with blst and fake_crypto - run: make nextest-ef - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + run: make test-ef basic-simulator-ubuntu: name: basic-simulator-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -300,6 +267,7 @@ jobs: - name: Run a basic beacon chain sim that starts from Deneb run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs - name: Upload logs + if: always() uses: actions/upload-artifact@v4 with: name: basic_simulator_logs @@ -310,7 +278,7 @@ jobs: if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -321,6 +289,7 @@ jobs: - name: Run a beacon chain sim which tests VC fallback behaviour run: cargo run --release --bin simulator fallback-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/fallback_simulator_logs - name: Upload logs + if: always() uses: actions/upload-artifact@v4 with: name: fallback_simulator_logs @@ -329,11 +298,10 @@ jobs: name: execution-engine-integration-ubuntu needs: [check-labels] if: needs.check-labels.outputs.skip_ci != 'true' - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 with: channel: stable @@ -341,9 +309,6 @@ jobs: cache: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Add go compiler to $PATH - if: env.SELF_HOSTED_RUNNERS == 'true' - run: echo "/usr/local/go/bin" >> $GITHUB_PATH - name: Run exec engine integration tests in release run: make test-exec-engine check-code: @@ -352,14 +317,14 @@ jobs: env: CARGO_INCREMENTAL: 1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: channel: stable cache-target: release components: rustfmt,clippy - bins: cargo-audit + bins: cargo-audit,cargo-deny - name: Check formatting with cargo fmt run: make cargo-fmt - name: Lint code for quality and style with Clippy @@ -372,6 +337,8 @@ jobs: run: make arbitrary-fuzz - name: Run cargo audit run: make audit-CI + - name: Run cargo deny + run: make deny-CI - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose run: CARGO_HOME=$(readlink -f $HOME) make vendor - name: Markdown-linter @@ -382,7 +349,7 @@ jobs: name: check-msrv runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Rust at Minimum Supported Rust Version (MSRV) run: | metadata=$(cargo metadata --no-deps --format-version 1) @@ -396,7 +363,7 @@ jobs: if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of nightly Rust uses: moonrepo/setup-rust@v1 with: @@ -424,7 +391,7 @@ jobs: if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install dependencies run: sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang - name: Use Rust beta @@ -437,7 +404,7 @@ jobs: if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -451,7 +418,7 @@ jobs: if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get latest version of stable Rust uses: moonrepo/setup-rust@v1 with: @@ -470,11 +437,11 @@ jobs: 'check-labels', 'target-branch-check', 'release-tests-ubuntu', - 'release-tests-windows', 'beacon-chain-tests', 'op-pool-tests', 'network-tests', 'slasher-tests', + 'http-api-tests', 'debug-tests-ubuntu', 'state-transition-vectors-ubuntu', 'ef-tests-ubuntu', @@ -490,6 +457,6 @@ jobs: 'cargo-sort', ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check that success job is dependent on all others run: ./scripts/ci/check-success-job.sh ./.github/workflows/test-suite.yml test-suite-success diff --git a/.gitignore b/.gitignore index e63e218a3b..efd7916b05 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ perf.data* *.tar.gz /bin genesis.ssz -/clippy.toml /.cargo # IntelliJ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..65447c4390 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.cargo.cfgs": [ + "!debug_assertions" + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..3e9ab169f3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,332 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +**Important**: Always branch from `unstable` and target `unstable` when creating pull requests. + +### Building and Installation + +- `make install` - Build and install the main Lighthouse binary in release mode +- `make install-lcli` - Build and install the `lcli` utility binary +- `cargo build --release` - Standard Rust release build +- `cargo build --bin lighthouse --features "gnosis,slasher-lmdb"` - Build with specific features + +### Testing + +- `make test` - Run the full test suite in release mode (excludes EF tests, beacon_chain, slasher, network, http_api) +- `make test-release` - Run tests using nextest (faster parallel test runner) +- `make test-beacon-chain` - Run beacon chain tests for all supported forks +- `make test-slasher` - Run slasher tests with all database backend combinations +- `make test-ef` - Download and run Ethereum Foundation test vectors +- `make test-full` - Complete test suite including linting, EF tests, and execution engine tests +- `cargo nextest run -p ` - Run tests for a specific package +- `cargo nextest run -p ` - Run individual test (preferred during development iteration) +- `FORK_NAME=electra cargo nextest run -p beacon_chain` - Run tests for specific fork + +**Note**: Full test suite takes ~20 minutes. When iterating, prefer running individual tests. + +### Linting and Code Quality + +- `make lint` - Run Clippy linter with project-specific rules +- `make lint-full` - Run comprehensive linting including tests (recommended for thorough checking) +- `make cargo-fmt` - Check code formatting with rustfmt +- `make check-benches` - Typecheck benchmark code +- `make audit` - Run security audit on dependencies + +### Cross-compilation + +- `make build-x86_64` - Cross-compile for x86_64 Linux +- `make build-aarch64` - Cross-compile for ARM64 Linux +- `make build-riscv64` - Cross-compile for RISC-V 64-bit Linux + +## Architecture Overview + +Lighthouse is a modular Ethereum consensus client with two main components: + +### Core Components + +**Beacon Node** (`beacon_node/`) + +- Main consensus client that syncs with the Ethereum network +- Contains the beacon chain state transition logic (`beacon_node/beacon_chain/`) +- Handles networking, storage, and P2P communication +- Provides HTTP API for validator clients and external tools +- Entry point: `beacon_node/src/lib.rs` + +**Validator Client** (`validator_client/`) + +- Manages validator keystores and performs validator duties +- Connects to beacon nodes via HTTP API +- Handles block proposals, attestations, and sync committee duties +- Includes slashing protection and doppelganger detection +- Entry point: `validator_client/src/lib.rs` + +### Key Subsystems + +**Consensus Types** (`consensus/types/`) + +- Core Ethereum consensus data structures (BeaconState, BeaconBlock, etc.) +- Ethereum specification implementations for different networks (mainnet, gnosis) +- SSZ encoding/decoding and state transition primitives + +**Storage** (`beacon_node/store/`) + +- Hot/cold database architecture for efficient beacon chain storage +- Supports multiple backends (LevelDB, RocksDB, REDB) +- Handles state pruning and historical data management + +**Networking** (`beacon_node/lighthouse_network/`, `beacon_node/network/`) + +- Libp2p-based P2P networking stack +- Gossipsub for message propagation +- Discovery v5 for peer discovery +- Request/response protocols for sync + +**Fork Choice** (`consensus/fork_choice/`, `consensus/proto_array/`) + +- Implements Ethereum's fork choice algorithm (proto-array) +- Manages chain reorganizations and finality + +**Execution Layer Integration** (`beacon_node/execution_layer/`) + +- Interfaces with execution clients +- Retrieves payloads from local execution layer or external block builders +- Handles payload validation and builder integration + +**Slasher** (`slasher/`) + +- Optional slashing detection service +- Supports LMDB, MDBX, and REDB database backends +- Can be enabled with `--slasher` flag + +### Utilities + +**Account Manager** (`account_manager/`) - CLI tool for managing validator accounts and keystores +**LCLI** (`lcli/`) - Lighthouse command-line utilities for debugging and testing +**Database Manager** (`database_manager/`) - Database maintenance and migration tools + +### Build System Notes + +- Uses Cargo workspace with 90+ member crates +- Supports multiple Ethereum specifications via feature flags (`gnosis`, `spec-minimal`) +- Cross-compilation support for Linux x86_64, ARM64, and RISC-V +- Multiple build profiles: `release`, `maxperf`, `reproducible` +- Feature-based compilation for different database backends and optional components + +### Network Support + +- **Mainnet**: Default production network +- **Gnosis**: Alternative network (requires `gnosis` feature) +- **Testnets**: Holesky, Sepolia via built-in network configs +- **Custom networks**: Via `--testnet-dir` flag + +### Key Configuration + +- Default data directory: `~/.lighthouse/{network}` +- Beacon node data: `~/.lighthouse/{network}/beacon` +- Validator data: `~/.lighthouse/{network}/validators` +- Configuration primarily via CLI flags and YAML files + +## Common Review Standards + +### CI/Testing Requirements + +- All checks must pass before merge +- Test coverage expected for significant changes +- Flaky tests are actively addressed and fixed +- New features often require corresponding tests +- `beacon_chain` and `http_api` tests support fork-specific testing using `FORK_NAME` env var when `beacon_chain/fork_from_env` feature is enabled + +### Code Quality Standards + +- Clippy warnings must be fixed promptly (multiple PRs show this pattern) +- Code formatting with `cargo fmt` enforced +- Must run `cargo sort` when adding dependencies - dependency order is enforced on CI +- Performance considerations for hot paths + +### Documentation and Context + +- PRs require clear descriptions of what and why +- Breaking changes need migration documentation +- API changes require documentation updates +- When CLI is updated, run `make cli-local` to generate updated help text in lighthouse book +- Comments appreciated for complex logic + +### Security and Safety + +- Careful review of consensus-critical code paths +- Error handling patterns must be comprehensive +- Input validation for external data + +## Development Patterns and Best Practices + +### Panics and Error Handling + +- **Panics should be avoided at all costs** +- Always prefer returning a `Result` or `Option` over causing a panic (e.g., prefer `array.get(1)?` over `array[1]`) +- Avoid `expect` or `unwrap` at runtime - only acceptable during startup when validating CLI flags or configurations +- If you must make assumptions about panics, use `.expect("Helpful message")` instead of `.unwrap()` and provide detailed reasoning in nearby comments +- Use proper error handling with `Result` types and graceful error propagation + +### Rayon Usage + +- Avoid using the rayon global thread pool as it results in CPU oversubscription when the beacon processor has fully allocated all CPUs to workers +- Use scoped rayon pools started by beacon processor for computational intensive tasks + +### Locks + +- Take great care to avoid deadlocks when working with fork choice locks - seek detailed review ([reference](beacon_node/beacon_chain/src/canonical_head.rs:9)) +- Keep lock scopes as narrow as possible to avoid blocking fast-responding functions like the networking stack + +### Async Patterns + +- Avoid blocking computations in async tasks +- Spawn a blocking task instead for CPU-intensive work + +### Tracing + +- Design spans carefully and avoid overuse of spans just to add context data to events +- Avoid using spans on simple getter methods as it can result in performance overhead +- Be cautious of span explosion with recursive functions +- Use spans per meaningful step or computationally critical step +- Avoid using `span.enter()` or `span.entered()` in async tasks + +### Database + +- Maintain schema continuity on `unstable` branch +- Database migrations must be backward compatible + +### Consensus Crate + +- Use safe math methods like `saturating_xxx` or `checked_xxx` +- Critical that this crate behaves deterministically and MUST not have undefined behavior + +### Testing Patterns + +- **Use appropriate test types for the right scenarios**: + - **Unit tests** for single component edge cases and isolated logic + - **Integration tests** using [`BeaconChainHarness`](beacon_node/beacon_chain/src/test_utils.rs:668) for end-to-end workflows +- **`BeaconChainHarness` guidelines**: + - Excellent for integration testing but slower than unit tests + - Prefer unit tests instead for testing edge cases of single components + - Reserve for testing component interactions and full workflows +- **Mocking strategies**: + - Use `mockall` crate for unit test mocking + - Use `mockito` for HTTP API mocking (see [`validator_test_rig`](testing/validator_test_rig/src/mock_beacon_node.rs:20) for examples) +- **Event-based testing for sync components**: + - Use [`TestRig`](beacon_node/network/src/sync/tests/mod.rs) pattern for testing sync components + - Sync components interact with the network and beacon chain via events (their public API), making event-based testing more suitable than using internal functions and mutating internal states + - Enables testing of complex state transitions and timing-sensitive scenarios +- **Testing `BeaconChain` dependent components**: + - `BeaconChain` is difficult to create for TDD + - Create intermediate adapter structs to enable easy mocking + - See [`beacon_node/beacon_chain/src/fetch_blobs/tests.rs`](beacon_node/beacon_chain/src/fetch_blobs/tests.rs) for the adapter pattern +- **Local testnet for manual/full E2E testing**: + - Use Kurtosis-based local testnet setup for comprehensive testing + - See [`scripts/local_testnet/README.md`](scripts/local_testnet/README.md) for setup instructions + +### TODOs and Comments + +- All `TODO` statements must be accompanied by a GitHub issue link +- Prefer line (`//`) comments to block comments (`/* ... */`) +- Use doc comments (`///`) before attributes for public items +- Keep documentation concise and clear - avoid verbose explanations +- Provide examples in doc comments for public APIs when helpful + +## Logging Guidelines + +Use appropriate log levels for different scenarios: + +- **`crit`**: Critical issues with major impact to Lighthouse functionality - Lighthouse may not function correctly without resolving. Needs immediate attention. +- **`error`**: Error cases that may have moderate impact to Lighthouse functionality. Expect to receive reports from users for this level. +- **`warn`**: Unexpected code paths that don't have major impact - fully recoverable. Expect user reports if excessive warning logs occur. +- **`info`**: High-level logs indicating beacon node status and block import status. Should not be used excessively. +- **`debug`**: Events lower level than info useful for developers. Can also log errors expected during normal operation that users don't need to action. + +## Code Examples + +### Safe Math in Consensus Crate + +```rust +// ❌ Avoid - could panic +let result = a + b; + +// ✅ Preferred +let result = a.saturating_add(b); +// or +use safe_arith::SafeArith; + +let result = a.safe_add(b)?; +``` + +### Panics and Error Handling + +```rust +// ❌ Avoid - could panic at runtime +let value = some_result.unwrap(); +let item = array[1]; + +// ✅ Preferred - proper error handling +let value = some_result.map_err(|e| CustomError::SomeVariant(e))?; +let item = array.get(1)?; + +// ✅ Acceptable during startup for CLI/config validation +let config_value = matches.get_one::("required-flag") + .expect("Required flag must be present due to clap validation"); + +// ✅ If you must make runtime assumptions, use expect with explanation +let item = array.get(1).expect("Array always has at least 2 elements due to validation in constructor"); +// Detailed reasoning should be provided in nearby comments +``` + +### TODO Format + +```rust +pub fn my_function(&mut self, _something: &[u8]) -> Result { + // TODO: Implement proper validation here + // https://github.com/sigp/lighthouse/issues/1234 +} +``` + +### Async Task Spawning for Blocking Work + +```rust +// ❌ Avoid - blocking in async context +async fn some_handler() { + let result = expensive_computation(); // blocks async runtime +} + +// ✅ Preferred +async fn some_handler() { + let result = tokio::task::spawn_blocking(|| { + expensive_computation() + }).await?; +} +``` + +### Tracing Span Usage + +```rust +// ❌ Avoid - span on simple getter +#[instrument] +fn get_head_block_root(&self) -> Hash256 { + self.head_block_root +} + +// ✅ Preferred - span on meaningful operations +#[instrument(skip(self))] +async fn process_block(&self, block: Block) -> Result<(), Error> { + // meaningful computation +} +``` + +## Build and Development Notes + +- Full builds and tests take 5+ minutes - use large timeouts (300s+) for any `cargo build`, `cargo nextest`, or `make` commands +- Use `cargo check` for faster iteration during development and always run after code changes +- Prefer targeted package tests (`cargo nextest run -p `) and individual tests over full test suite when debugging specific issues +- Use `cargo fmt --all && make lint-fix` to format code and fix linting issues once a task is complete +- Always understand the broader codebase patterns before making changes +- Minimum Supported Rust Version (MSRV) is documented in `lighthouse/Cargo.toml` - ensure Rust version meets or exceeds this requirement diff --git a/Cargo.lock b/Cargo.lock index 9ff9d62f5e..7fc1459c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,19 +2,9 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "account_manager" -version = "0.3.5" +version = "8.0.1" dependencies = [ "account_utils", "bls", @@ -44,10 +34,11 @@ dependencies = [ name = "account_utils" version = "0.1.0" dependencies = [ + "bls", "eth2_keystore", "eth2_wallet", "filesystem", - "rand 0.8.5", + "rand 0.9.2", "regex", "rpassword", "serde", @@ -58,20 +49,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -80,7 +62,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -93,7 +75,7 @@ dependencies = [ "cipher 0.3.0", "cpufeatures", "ctr 0.8.0", - "opaque-debug 0.3.1", + "opaque-debug", ] [[package]] @@ -135,9 +117,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -149,90 +131,272 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "alloy-consensus" -version = "0.3.6" +name = "alloy-chains" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629b62e38d471cc15fea534eb7283d2f8a4e8bdb1811bcc5d66dda6cfce6fae1" +checksum = "4bc32535569185cbcb6ad5fa64d989a47bccb9a08e27284b1f2a3ccf16e6d010" +dependencies = [ + "alloy-primitives", + "num_enum", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", "c-kzg", + "derive_more 2.0.1", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdff496dd4e98a81f4861e66f7eaf5f2488971848bb42d9c892f871730245c8" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "winnow", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.17", ] [[package]] name = "alloy-eip2930" -version = "0.1.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", + "serde", ] [[package]] name = "alloy-eip7702" -version = "0.1.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea59dc42102bc9a1905dc57901edc6dd48b9f38115df86c7d252acba70d71d04" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", + "serde", + "thiserror 2.0.17", ] [[package]] name = "alloy-eips" -version = "0.3.6" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f923dd5fca5f67a43d81ed3ebad0880bd41f6dd0ada930030353ac356c54cd0f" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ + "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", "alloy-primitives", "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", "c-kzg", - "derive_more 1.0.0", - "once_cell", + "derive_more 2.0.1", + "either", "serde", + "serde_with", "sha2 0.10.9", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-json-abi" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http 1.3.1", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more 2.0.1", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", ] [[package]] name = "alloy-primitives" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "arbitrary", "bytes", "cfg-if", "const-hex", - "derive_arbitrary", "derive_more 2.0.1", - "foldhash", - "getrandom 0.2.16", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "foldhash 0.2.0", + "getrandom 0.3.4", + "hashbrown 0.16.0", + "indexmap 2.12.0", "itoa", - "k256 0.13.4", + "k256", "keccak-asm", "paste", "proptest", "proptest-derive", - "rand 0.8.5", + "rand 0.9.2", "ruint", "rustc-hash 2.1.1", "serde", - "sha3 0.10.8", + "sha3", "tiny-keccak", ] [[package]] -name = "alloy-rlp" -version = "0.3.11" +name = "alloy-provider" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6" +checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "alloy-signer", + "alloy-sol-types", + "alloy-transport", + "alloy-transport-http", + "async-stream", + "async-trait", + "auto_impl", + "dashmap", + "either", + "futures", + "futures-utils-wasm", + "lru 0.13.0", + "parking_lot", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -241,20 +405,247 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "alloy-rpc-client" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "alloy-transport-http", + "futures", + "pin-project", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-serde" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-signer-local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.12.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.110", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.110", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-transport" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" +dependencies = [ + "alloy-json-rpc", + "auto_impl", + "base64 0.22.1", + "derive_more 2.0.1", + "futures", + "futures-utils-wasm", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-transport-http" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" +dependencies = [ + "alloy-json-rpc", + "alloy-transport", + "reqwest", + "serde_json", + "tower 0.5.2", + "tracing", + "url", +] + +[[package]] +name = "alloy-trie" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more 2.0.1", + "nybbles", + "serde", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] [[package]] name = "android_system_properties" @@ -273,9 +664,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -288,50 +679,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -389,6 +780,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -409,6 +820,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.110", +] + [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -434,6 +855,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -455,6 +889,18 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -475,6 +921,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -492,12 +948,15 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "asn1-rs" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -505,19 +964,19 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", ] [[package]] name = "asn1-rs-derive" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", "synstructure", ] @@ -529,7 +988,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -561,9 +1020,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -573,65 +1032,53 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix 0.38.44", + "rustix 1.1.2", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "async-lock" -version = "3.4.0" +name = "async-stream" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ - "event-listener 5.4.0", - "event-listener-strategy", + "async-stream-impl", + "futures-core", "pin-project-lite", ] [[package]] -name = "async-recursion" -version = "1.1.1" +name = "async-stream-impl" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version 0.4.1", + "syn 2.0.110", ] [[package]] @@ -655,38 +1102,16 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attohttpc" -version = "0.24.1" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ - "http 0.2.12", + "base64 0.22.1", + "http 1.3.1", "log", "url", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - -[[package]] -name = "auto_impl" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "auto_impl" version = "1.3.0" @@ -695,28 +1120,60 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backtrace" -version = "0.3.75" +name = "axum" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", ] [[package]] @@ -725,12 +1182,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - [[package]] name = "base16ct" version = "0.2.0" @@ -738,27 +1189,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base58" -version = "0.1.0" +name = "base256emoji" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" - -[[package]] -name = "base58check" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee2fe4c9a0c84515f136aaae2466744a721af6d63339c18689d9e995d74d99b" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" dependencies = [ - "base58", - "sha2 0.8.2", + "const-str", + "match-lookup", ] -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.1" @@ -779,20 +1218,19 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "beacon_chain" version = "0.2.0" dependencies = [ "alloy-primitives", - "bitvec 1.0.1", + "bitvec", "bls", "criterion", - "derivative", - "eth1", + "educe", "eth2", "eth2_network_config", "ethereum_hashing", @@ -800,6 +1238,7 @@ dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", "execution_layer", + "fixed_bytes", "fork_choice", "futures", "genesis", @@ -807,18 +1246,22 @@ dependencies = [ "int_to_bytes", "itertools 0.10.5", "kzg", + "lighthouse_tracing", "lighthouse_version", "logging", - "lru", + "lru 0.12.5", "maplit", "merkle_proof", "metrics", + "milhouse", + "mockall", + "mockall_double", "once_cell", "oneshot_broadcast", "operation_pool", - "parking_lot 0.12.3", + "parking_lot", "proto_array", - "rand 0.8.5", + "rand 0.9.2", "rayon", "safe_arith", "sensitive_url", @@ -839,15 +1282,18 @@ dependencies = [ "tracing", "tree_hash", "tree_hash_derive", + "typenum", "types", + "zstd 0.13.3", ] [[package]] name = "beacon_node" -version = "7.1.0-beta.0" +version = "8.0.1" dependencies = [ "account_utils", "beacon_chain", + "bls", "clap", "clap_utils", "client", @@ -859,9 +1305,10 @@ dependencies = [ "genesis", "hex", "http_api", - "hyper 1.6.0", + "hyper 1.8.1", "lighthouse_network", "monitoring_api", + "network_utils", "node_test_rig", "sensitive_url", "serde_json", @@ -871,17 +1318,18 @@ dependencies = [ "task_executor", "tracing", "types", - "unused_port", ] [[package]] name = "beacon_node_fallback" version = "0.1.0" dependencies = [ + "bls", "clap", "eth2", "futures", "itertools 0.10.5", + "sensitive_url", "serde", "slot_clock", "strum", @@ -904,7 +1352,7 @@ dependencies = [ "logging", "metrics", "num_cpus", - "parking_lot 0.12.3", + "parking_lot", "serde", "slot_clock", "strum", @@ -915,12 +1363,6 @@ dependencies = [ "types", ] -[[package]] -name = "bech32" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" - [[package]] name = "bincode" version = "1.3.3" @@ -936,7 +1378,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -949,7 +1391,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.101", + "syn 2.0.110", "which", ] @@ -968,6 +1410,22 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -976,31 +1434,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "bitvec" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" -dependencies = [ - "either", - "radium 0.3.0", -] - -[[package]] -name = "bitvec" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" -dependencies = [ - "funty 1.1.0", - "radium 0.6.2", - "tap", - "wyz 0.2.0", -] +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -1008,10 +1444,10 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty 2.0.0", - "radium 0.7.0", + "funty", + "radium", "tap", - "wyz 0.5.1", + "wyz", ] [[package]] @@ -1023,26 +1459,13 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.2.1", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1051,24 +1474,18 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] -name = "block-padding" -version = "0.1.5" +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "byte-tools", + "objc2", ] -[[package]] -name = "block-padding" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" - [[package]] name = "bls" version = "0.2.0" @@ -1081,7 +1498,7 @@ dependencies = [ "ethereum_ssz", "fixed_bytes", "hex", - "rand 0.8.5", + "rand 0.9.2", "safe_arith", "serde", "tree_hash", @@ -1090,9 +1507,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ "cc", "glob", @@ -1108,8 +1525,8 @@ checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" dependencies = [ "blst", "byte-slice-cast", - "ff 0.13.1", - "group 0.13.0", + "ff", + "group", "pairing", "rand_core 0.6.4", "serde", @@ -1118,7 +1535,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "7.1.0-beta.0" +version = "8.0.1" dependencies = [ "beacon_node", "bytes", @@ -1130,6 +1547,7 @@ dependencies = [ "lighthouse_network", "log", "logging", + "network_utils", "serde", "tokio", "tracing", @@ -1137,6 +1555,29 @@ dependencies = [ "types", ] +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "bs58" version = "0.4.0" @@ -1156,20 +1597,24 @@ dependencies = [ name = "builder_client" version = "0.1.0" dependencies = [ + "bls", + "context_deserialize", "eth2", "ethereum_ssz", "lighthouse_version", + "mockito", "reqwest", "sensitive_url", "serde", "serde_json", + "tokio", ] [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" @@ -1177,12 +1622,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.5.0" @@ -1191,9 +1630,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -1220,9 +1659,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "1.0.3" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0307f72feab3300336fb803a57134159f6e20139af1357f36c54cb90d8e8928" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ "blst", "cc", @@ -1235,11 +1674,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.9" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1251,20 +1690,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo_metadata" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" -dependencies = [ - "camino", - "cargo-platform", - "semver 1.0.26", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "cargo_metadata" version = "0.19.2" @@ -1273,10 +1698,10 @@ checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -1287,10 +1712,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.21" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1307,9 +1733,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1343,14 +1769,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -1388,7 +1814,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1415,9 +1841,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -1425,9 +1851,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -1438,21 +1864,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clap_utils" @@ -1479,7 +1905,6 @@ dependencies = [ "directory", "dirs", "environment", - "eth1", "eth2", "eth2_config", "ethereum_ssz", @@ -1495,7 +1920,7 @@ dependencies = [ "monitoring_api", "network", "operation_pool", - "rand 0.8.5", + "rand 0.9.2", "sensitive_url", "serde", "serde_json", @@ -1523,68 +1948,11 @@ dependencies = [ "cc", ] -[[package]] -name = "coins-bip32" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634c509653de24b439672164bbf56f5f582a2ab0e313d3b0f6af0b7345cf2560" -dependencies = [ - "bincode", - "bs58 0.4.0", - "coins-core", - "digest 0.10.7", - "getrandom 0.2.16", - "hmac 0.12.1", - "k256 0.11.6", - "lazy_static", - "serde", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "coins-bip39" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a11892bcac83b4c6e95ab84b5b06c76d9d70ad73548dd07418269c5c7977171" -dependencies = [ - "bitvec 0.17.4", - "coins-bip32", - "getrandom 0.2.16", - "hex", - "hmac 0.12.1", - "pbkdf2 0.11.0", - "rand 0.8.5", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "coins-core" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94090a6663f224feae66ab01e41a2555a8296ee07b5f20dab8888bdefc9f617" -dependencies = [ - "base58check", - "base64 0.12.3", - "bech32", - "blake2", - "digest 0.10.7", - "generic-array 0.14.7", - "hex", - "ripemd", - "serde", - "serde_derive", - "sha2 0.10.9", - "sha3 0.10.8", - "thiserror 1.0.69", -] - [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -1597,15 +1965,19 @@ dependencies = [ [[package]] name = "compare_fields" -version = "0.2.0" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05162add7c8618791829528194a271dca93f69194d35b19db1ca7fbfb8275278" dependencies = [ "compare_fields_derive", - "itertools 0.10.5", + "itertools 0.14.0", ] [[package]] name = "compare_fields_derive" -version = "0.2.0" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ee468b2e568b668e2a686112935e7bbe9a81bf4fa6b9f6fc3410ea45fb7ce" dependencies = [ "quote", "syn 1.0.109", @@ -1621,16 +1993,54 @@ dependencies = [ ] [[package]] -name = "const-hex" -version = "1.14.0" +name = "console-api" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic 0.12.3", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic 0.12.3", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const-hex" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", - "hex", "proptest", - "serde", + "serde_core", ] [[package]] @@ -1640,10 +2050,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "const_format" -version = "0.2.34" +name = "const-str" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -1667,21 +2083,21 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "context_deserialize" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5f9ea0a0ae2de4943f5ca71590b6dbd0b952475f0a0cafb30a470cec78c8b9" dependencies = [ - "milhouse", + "context_deserialize_derive", "serde", - "ssz_types", ] [[package]] name = "context_deserialize_derive" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c57b2db1e4e3ed804dcc49894a144b68fe6c754b8f545eb1dda7ad3c7dbe7e6" dependencies = [ - "context_deserialize", "quote", - "serde", - "serde_json", "syn 1.0.109", ] @@ -1691,15 +2107,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1710,6 +2117,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1735,62 +2152,25 @@ dependencies = [ ] [[package]] -name = "crate_crypto_internal_eth_kzg_bls12_381" -version = "0.5.4" +name = "crc" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f9cdad245e39a3659bc4c8958e93de34bd31ba3131ead14ccfb4b2cd60e52d" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ - "blst", - "blstrs", - "ff 0.13.1", - "group 0.13.0", - "pairing", - "subtle", + "crc-catalog", ] [[package]] -name = "crate_crypto_internal_eth_kzg_erasure_codes" -version = "0.5.4" +name = "crc-catalog" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581d28bcc93eecd97a04cebc5293271e0f41650f03c102f24d6cd784cbedb9f2" -dependencies = [ - "crate_crypto_internal_eth_kzg_bls12_381", - "crate_crypto_internal_eth_kzg_polynomial", -] - -[[package]] -name = "crate_crypto_internal_eth_kzg_maybe_rayon" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fc0f984e585ea984a766c5b58d6bf6c51e463b0a0835b0dd4652d358b506b3" - -[[package]] -name = "crate_crypto_internal_eth_kzg_polynomial" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dff7a45e2d80308b21abdbc5520ec23c3ebfb3a94fafc02edfa7f356af6d7f" -dependencies = [ - "crate_crypto_internal_eth_kzg_bls12_381", -] - -[[package]] -name = "crate_crypto_kzg_multi_open_fk20" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c2f82695a88809e713e1ff9534cb90ceffab0a08f4bd33245db711f9d356f" -dependencies = [ - "crate_crypto_internal_eth_kzg_bls12_381", - "crate_crypto_internal_eth_kzg_maybe_rayon", - "crate_crypto_internal_eth_kzg_polynomial", - "hex", - "sha2 0.10.9", -] +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1831,6 +2211,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1867,21 +2253,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", -] +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1889,7 +2263,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "subtle", "zeroize", @@ -1897,11 +2271,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] @@ -1912,7 +2286,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ - "generic-array 0.14.7", + "generic-array", "subtle", ] @@ -1936,12 +2310,13 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.6" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" dependencies = [ - "nix 0.29.0", - "windows-sys 0.59.0", + "dispatch2", + "nix 0.30.1", + "windows-sys 0.61.2", ] [[package]] @@ -1968,7 +2343,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -1991,6 +2366,16 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling_core" version = "0.13.4" @@ -2016,7 +2401,22 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.101", + "syn 2.0.110", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim 0.11.1", + "syn 2.0.110", ] [[package]] @@ -2038,7 +2438,18 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.101", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.110", ] [[package]] @@ -2061,6 +2472,20 @@ dependencies = [ "libc", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -2084,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -2125,7 +2550,10 @@ dependencies = [ name = "deposit_contract" version = "0.2.0" dependencies = [ - "ethabi 16.0.0", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "bls", "ethereum_ssz", "hex", "reqwest", @@ -2135,16 +2563,6 @@ dependencies = [ "types", ] -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - [[package]] name = "der" version = "0.7.10" @@ -2152,15 +2570,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] [[package]] name = "der-parser" -version = "9.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", @@ -2172,11 +2589,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -2192,13 +2610,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -2207,20 +2625,11 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.101", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", + "syn 2.0.110", ] [[package]] @@ -2229,18 +2638,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl 2.0.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "derive_more-impl", ] [[package]] @@ -2251,26 +2649,17 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", "unicode-xid", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -2316,9 +2705,9 @@ dependencies = [ [[package]] name = "discv5" -version = "0.9.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4b4e7798d2ff74e29cee344dc490af947ae657d6ab5273dde35d58ce06a4d71" +checksum = "f170f4f6ed0e1df52bf43b403899f0081917ecf1500bfe312505cc3b515a8899" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -2334,19 +2723,31 @@ dependencies = [ "hkdf", "lazy_static", "libp2p-identity", - "lru", + "lru 0.12.5", "more-asserts", "multiaddr", - "parking_lot 0.12.3", + "parking_lot", "rand 0.8.5", "smallvec", - "socket2", + "socket2 0.5.10", "tokio", "tracing", "uint 0.10.0", "zeroize", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2355,7 +2756,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -2363,11 +2764,12 @@ name = "doppelganger_service" version = "0.1.0" dependencies = [ "beacon_node_fallback", + "bls", "environment", "eth2", "futures", "logging", - "parking_lot 0.12.3", + "parking_lot", "slot_clock", "task_executor", "tokio", @@ -2376,6 +2778,12 @@ dependencies = [ "validator_store", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dtoa" version = "1.0.10" @@ -2389,16 +2797,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "ecdsa" -version = "0.14.8" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" @@ -2406,12 +2808,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.10", + "der", "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", ] [[package]] @@ -2420,15 +2823,15 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", + "pkcs8", + "signature", ] [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -2448,7 +2851,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -2459,8 +2862,8 @@ dependencies = [ "beacon_chain", "bls", "compare_fields", - "compare_fields_derive", - "derivative", + "context_deserialize", + "educe", "eth2_network_config", "ethereum_ssz", "ethereum_ssz_derive", @@ -2470,16 +2873,52 @@ dependencies = [ "hex", "kzg", "logging", + "milhouse", "rayon", "serde", "serde_json", "serde_repr", "serde_yaml", "snap", + "ssz_types", "state_processing", "swap_or_not_shuffle", "tree_hash", "tree_hash_derive", + "typenum", + "types", +] + +[[package]] +name = "eip4844" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ab45fc63db6bbe5c3eb7c79303b2aff7ee529c991b2111c46879d1ea38407e" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", + "ekzg-polynomial", + "ekzg-serialization", + "ekzg-single-open", + "ekzg-trusted-setup", + "hex", + "itertools 0.14.0", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "eip_3076" +version = "0.1.0" +dependencies = [ + "arbitrary", + "bls", + "ethereum_serde_utils", + "fixed_bytes", + "serde", + "serde_json", + "tempfile", "types", ] @@ -2488,25 +2927,94 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] -name = "elliptic-curve" -version = "0.12.3" +name = "ekzg-bls12-381" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "05c599a59deba6188afd9f783507e4d89efc997f0fa340a758f0d0992b322416" dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest 0.10.7", - "ff 0.12.1", - "generic-array 0.14.7", - "group 0.12.1", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1 0.3.0", + "blst", + "blstrs", + "ff", + "group", + "pairing", "subtle", - "zeroize", +] + +[[package]] +name = "ekzg-erasure-codes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8474a41a30ddd2b651798b1aa9ce92011207c3667186fe9044184683250109e7" +dependencies = [ + "ekzg-bls12-381", + "ekzg-polynomial", +] + +[[package]] +name = "ekzg-maybe-rayon" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf94d1385185c1f7caef4973be49702c7d9ffdeaf832d126dbb9ed6efe09d40" + +[[package]] +name = "ekzg-multi-open" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d37456a32cf79bdbddd6685a2adec73210e2d60332370bc0e9a502b6d93beb" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", + "ekzg-polynomial", + "sha2 0.10.9", +] + +[[package]] +name = "ekzg-polynomial" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704751bac85af4754bb8a14457ef24d820738062d0b6f3763534d0980b1a1e81" +dependencies = [ + "ekzg-bls12-381", + "ekzg-maybe-rayon", +] + +[[package]] +name = "ekzg-serialization" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb983d9f75b2804c00246def8d52c01cf05f70c22593b8d314fbcf0cf89042b" +dependencies = [ + "ekzg-bls12-381", + "hex", +] + +[[package]] +name = "ekzg-single-open" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799d5806d51e1453fa0f528d6acf4127e2a89e98312c826151ebc24ee3448ec3" +dependencies = [ + "ekzg-bls12-381", + "ekzg-polynomial", + "itertools 0.14.0", +] + +[[package]] +name = "ekzg-trusted-setup" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85314d56718dc2c6dd77c3b3630f1839defcb6f47d9c20195608a0f7976095ab" +dependencies = [ + "ekzg-bls12-381", + "ekzg-serialization", + "hex", + "serde", + "serde_json", ] [[package]] @@ -2515,16 +3023,16 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", + "base16ct", + "crypto-bigint", "digest 0.10.7", - "ff 0.13.1", - "generic-array 0.14.7", - "group 0.13.0", - "pem-rfc7468", - "pkcs8 0.10.2", + "ff", + "generic-array", + "group", + "pkcs8", "rand_core 0.6.4", - "sec1 0.7.3", + "sec1", + "serdect", "subtle", "zeroize", ] @@ -2549,11 +3057,11 @@ dependencies = [ "bytes", "ed25519-dalek", "hex", - "k256 0.13.4", + "k256", "log", "rand 0.8.5", "serde", - "sha3 0.10.8", + "sha3", "zeroize", ] @@ -2563,53 +3071,30 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", + "syn 2.0.110", ] [[package]] @@ -2642,104 +3127,39 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "eth-keystore" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" -dependencies = [ - "aes 0.8.4", - "ctr 0.9.2", - "digest 0.10.7", - "hex", - "hmac 0.12.1", - "pbkdf2 0.11.0", - "rand 0.8.5", - "scrypt 0.10.0", - "serde", - "serde_json", - "sha2 0.10.9", - "sha3 0.10.8", - "thiserror 1.0.69", - "uuid 0.8.2", -] - -[[package]] -name = "eth1" -version = "0.2.0" -dependencies = [ - "environment", - "eth1_test_rig", - "eth2", - "ethereum_ssz", - "ethereum_ssz_derive", - "execution_layer", - "futures", - "logging", - "merkle_proof", - "metrics", - "parking_lot 0.12.3", - "sensitive_url", - "serde", - "serde_yaml", - "state_processing", - "superstruct", - "task_executor", - "tokio", - "tracing", - "tree_hash", - "types", -] - -[[package]] -name = "eth1_test_rig" -version = "0.2.0" -dependencies = [ - "deposit_contract", - "ethers-contract", - "ethers-core", - "ethers-providers", - "hex", - "serde_json", - "tokio", - "types", - "unused_port", + "windows-sys 0.52.0", ] [[package]] name = "eth2" version = "0.1.0" dependencies = [ - "derivative", - "either", - "enr", + "bls", + "context_deserialize", + "educe", + "eip_3076", "eth2_keystore", "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "futures", "futures-util", - "libp2p-identity", "mediatype", - "multiaddr", "pretty_reqwest_error", "proto_array", - "rand 0.8.5", + "rand 0.9.2", "reqwest", "reqwest-eventsource", "sensitive_url", "serde", "serde_json", - "slashing_protection", "ssz_types", + "superstruct", "test_random_derive", "tokio", "types", @@ -2789,8 +3209,8 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand 0.8.5", - "scrypt 0.7.0", + "rand 0.9.2", + "scrypt", "serde", "serde_json", "serde_repr", @@ -2809,6 +3229,7 @@ dependencies = [ "discv5", "eth2_config", "ethereum_ssz", + "fixed_bytes", "kzg", "pretty_reqwest_error", "reqwest", @@ -2830,7 +3251,7 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand 0.8.5", + "rand 0.9.2", "serde", "serde_json", "serde_repr", @@ -2848,101 +3269,11 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ethabi" -version = "16.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c98847055d934070b90e806e12d3936b787d0a115068981c1d8dfd5dfef5a5" -dependencies = [ - "ethereum-types 0.12.1", - "hex", - "serde", - "serde_json", - "sha3 0.9.1", - "thiserror 1.0.69", - "uint 0.9.5", -] - -[[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types 0.14.1", - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "sha3 0.10.8", - "thiserror 1.0.69", - "uint 0.9.5", -] - -[[package]] -name = "ethbloom" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" -dependencies = [ - "crunchy", - "fixed-hash 0.7.0", - "impl-rlp", - "impl-serde 0.3.2", - "tiny-keccak", -] - -[[package]] -name = "ethbloom" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" -dependencies = [ - "crunchy", - "fixed-hash 0.8.0", - "impl-codec 0.6.0", - "impl-rlp", - "impl-serde 0.4.0", - "scale-info", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" -dependencies = [ - "ethbloom 0.11.1", - "fixed-hash 0.7.0", - "impl-rlp", - "impl-serde 0.3.2", - "primitive-types 0.10.1", - "uint 0.9.5", -] - -[[package]] -name = "ethereum-types" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" -dependencies = [ - "ethbloom 0.13.0", - "fixed-hash 0.8.0", - "impl-codec 0.6.0", - "impl-rlp", - "impl-serde 0.4.0", - "primitive-types 0.12.2", - "scale-info", - "uint 0.9.5", -] - [[package]] name = "ethereum_hashing" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" +checksum = "5aa93f58bb1eb3d1e556e4f408ef1dac130bad01ac37db4e7ade45de40d1c86a" dependencies = [ "cpufeatures", "ring", @@ -2951,9 +3282,9 @@ dependencies = [ [[package]] name = "ethereum_serde_utils" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70cbccfccf81d67bff0ab36e591fa536c8a935b078a7b0e58c1d00d418332fc9" +checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" dependencies = [ "alloy-primitives", "hex", @@ -2964,12 +3295,13 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86da3096d1304f5f28476ce383005385459afeaf0eea08592b65ddbc9b258d16" +checksum = "7e8cd8c4f47dfb947dbfe3cdf2945ae1da808dbedc592668658e827a12659ba1" dependencies = [ "alloy-primitives", "arbitrary", + "context_deserialize", "ethereum_serde_utils", "itertools 0.13.0", "serde", @@ -2980,200 +3312,14 @@ dependencies = [ [[package]] name = "ethereum_ssz_derive" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d832a5c38eba0e7ad92592f7a22d693954637fbb332b4f669590d66a5c3183e5" +checksum = "78d247bc40823c365a62e572441a8f8b12df03f171713f06bc76180fcd56ab71" dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "ethers-contract" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c3c3e119a89f0a9a1e539e7faecea815f74ddcf7c90d0b00d1f524db2fdc9c" -dependencies = [ - "ethers-contract-abigen", - "ethers-contract-derive", - "ethers-core", - "ethers-providers", - "futures-util", - "hex", - "once_cell", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "ethers-contract-abigen" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e5ad46aede34901f71afdb7bb555710ed9613d88d644245c657dc371aa228" -dependencies = [ - "Inflector", - "cfg-if", - "dunce", - "ethers-core", - "eyre", - "getrandom 0.2.16", - "hex", - "proc-macro2", - "quote", - "regex", - "reqwest", - "serde", - "serde_json", - "syn 1.0.109", - "toml", - "url", - "walkdir", -] - -[[package]] -name = "ethers-contract-derive" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f192e8e4cf2b038318aae01e94e7644e0659a76219e94bcd3203df744341d61f" -dependencies = [ - "ethers-contract-abigen", - "ethers-core", - "hex", - "proc-macro2", - "quote", - "serde_json", - "syn 1.0.109", -] - -[[package]] -name = "ethers-core" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15" -dependencies = [ - "arrayvec", - "bytes", - "cargo_metadata 0.15.4", - "chrono", - "convert_case 0.6.0", - "elliptic-curve 0.12.3", - "ethabi 18.0.0", - "generic-array 0.14.7", - "hex", - "k256 0.11.6", - "once_cell", - "open-fastrlp", - "proc-macro2", - "rand 0.8.5", - "rlp", - "rlp-derive", - "serde", - "serde_json", - "strum", - "syn 1.0.109", - "thiserror 1.0.69", - "tiny-keccak", - "unicode-xid", -] - -[[package]] -name = "ethers-etherscan" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9713f525348e5dde025d09b0a4217429f8074e8ff22c886263cc191e87d8216" -dependencies = [ - "ethers-core", - "getrandom 0.2.16", - "reqwest", - "semver 1.0.26", - "serde", - "serde-aux", - "serde_json", - "thiserror 1.0.69", - "tracing", -] - -[[package]] -name = "ethers-middleware" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e71df7391b0a9a51208ffb5c7f2d068900e99d6b3128d3a4849d138f194778b7" -dependencies = [ - "async-trait", - "auto_impl 0.5.0", - "ethers-contract", - "ethers-core", - "ethers-etherscan", - "ethers-providers", - "ethers-signers", - "futures-locks", - "futures-util", - "instant", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-futures", - "url", -] - -[[package]] -name = "ethers-providers" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a9e0597aa6b2fdc810ff58bc95e4eeaa2c219b3e615ed025106ecb027407d8" -dependencies = [ - "async-trait", - "auto_impl 1.3.0", - "base64 0.13.1", - "ethers-core", - "futures-core", - "futures-timer", - "futures-util", - "getrandom 0.2.16", - "hashers", - "hex", - "http 0.2.12", - "once_cell", - "parking_lot 0.11.2", - "pin-project", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-futures", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-timer", - "web-sys", - "ws_stream_wasm", -] - -[[package]] -name = "ethers-signers" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f41ced186867f64773db2e55ffdd92959e094072a1d09a5e5e831d443204f98" -dependencies = [ - "async-trait", - "coins-bip32", - "coins-bip39", - "elliptic-curve 0.12.3", - "eth-keystore", - "ethers-core", - "hex", - "rand 0.8.5", - "sha2 0.10.9", - "thiserror 1.0.69", + "syn 2.0.110", ] [[package]] @@ -3184,9 +3330,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -3199,7 +3345,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "pin-project-lite", ] @@ -3218,25 +3364,29 @@ dependencies = [ name = "execution_engine_integration" version = "0.1.0" dependencies = [ + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-signer-local", "async-channel 1.9.0", + "bls", "deposit_contract", - "ethers-core", - "ethers-middleware", - "ethers-providers", - "ethers-signers", "execution_layer", + "fixed_bytes", "fork_choice", "futures", "hex", "logging", + "network_utils", "reqwest", "sensitive_url", "serde_json", "task_executor", "tempfile", "tokio", + "typenum", "types", - "unused_port", ] [[package]] @@ -3246,13 +3396,14 @@ dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-rlp", + "alloy-rpc-types-eth", "arc-swap", + "bls", "builder_client", "bytes", "eth2", "ethereum_serde_utils", "ethereum_ssz", - "ethers-core", "fixed_bytes", "fork_choice", "hash-db", @@ -3263,11 +3414,11 @@ dependencies = [ "kzg", "lighthouse_version", "logging", - "lru", + "lru 0.12.5", "metrics", - "parking_lot 0.12.3", + "parking_lot", "pretty_reqwest_error", - "rand 0.8.5", + "rand 0.9.2", "reqwest", "sensitive_url", "serde", @@ -3286,27 +3437,12 @@ dependencies = [ "tree_hash", "tree_hash_derive", "triehash", + "typenum", "types", "warp", "zeroize", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3332,7 +3468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ "arrayvec", - "auto_impl 1.3.0", + "auto_impl", "bytes", ] @@ -3343,7 +3479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ "arrayvec", - "auto_impl 1.3.0", + "auto_impl", "bytes", ] @@ -3357,23 +3493,13 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec 1.0.1", + "bitvec", "rand_core 0.6.4", "subtle", ] @@ -3409,16 +3535,10 @@ dependencies = [ ] [[package]] -name = "fixed-hash" -version = "0.7.0" +name = "find-msvc-tools" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixed-hash" @@ -3442,9 +3562,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-sys", @@ -3463,6 +3583,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -3485,11 +3611,13 @@ dependencies = [ "beacon_chain", "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "logging", "metrics", "proto_array", "state_processing", "store", + "superstruct", "tokio", "tracing", "types", @@ -3497,13 +3625,19 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "fs2" version = "0.4.3" @@ -3514,12 +3648,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "funty" version = "2.0.0" @@ -3587,24 +3715,14 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "futures-core", "pin-project-lite", ] -[[package]] -name = "futures-locks" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" -dependencies = [ - "futures-channel", - "futures-task", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -3613,7 +3731,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -3623,7 +3741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.27", + "rustls 0.23.35", "rustls-pki-types", ] @@ -3664,35 +3782,10 @@ dependencies = [ ] [[package]] -name = "fxhash" -version = "0.2.1" +name = "futures-utils-wasm" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generator" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" -dependencies = [ - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.58.0", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" [[package]] name = "generic-array" @@ -3709,19 +3802,13 @@ dependencies = [ name = "genesis" version = "0.2.0" dependencies = [ - "environment", - "eth1", - "eth1_test_rig", + "bls", "ethereum_hashing", "ethereum_ssz", - "futures", "int_to_bytes", - "logging", "merkle_proof", "rayon", - "sensitive_url", "state_processing", - "tokio", "tracing", "tree_hash", "types", @@ -3736,21 +3823,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -3760,41 +3847,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ - "opaque-debug 0.3.1", + "opaque-debug", "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "git-version" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" -dependencies = [ - "git-version-macro", -] - -[[package]] -name = "git-version-macro" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "graffiti_file" @@ -3808,35 +3869,24 @@ dependencies = [ "types", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.1", + "ff", "rand 0.8.5", "rand_core 0.6.4", - "rand_xorshift", + "rand_xorshift 0.3.0", "subtle", ] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -3844,7 +3894,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.9.0", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -3853,9 +3903,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -3863,7 +3913,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -3872,12 +3922,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -3913,23 +3964,23 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", - "serde", + "foldhash 0.1.5", ] [[package]] -name = "hashers" -version = "1.0.1" +name = "hashbrown" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ - "fxhash", + "foldhash 0.2.0", + "serde", ] [[package]] @@ -3950,6 +4001,28 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "headers" version = "0.3.9" @@ -3984,12 +4057,6 @@ dependencies = [ "psutil", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -3998,38 +4065,23 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "hermit-abi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" dependencies = [ - "serde", + "arrayvec", ] [[package]] @@ -4040,11 +4092,10 @@ checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] name = "hickory-proto" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d00147af6310f4392a31680db52a3ed45a2e0f68eb18e8c3fe5537ecc96d9e2" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", "data-encoding", @@ -4055,9 +4106,10 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.1", - "socket2", - "thiserror 2.0.12", + "rand 0.9.2", + "ring", + "socket2 0.5.10", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -4066,9 +4118,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5762f69ebdbd4ddb2e975cd24690bf21fe6b2604039189c26acddbc427f12887" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -4076,11 +4128,11 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.3", - "rand 0.9.1", + "parking_lot", + "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -4115,11 +4167,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4184,29 +4236,33 @@ version = "0.1.0" dependencies = [ "beacon_chain", "beacon_processor", + "bls", "bs58 0.4.0", "bytes", + "context_deserialize", "directory", "either", - "eth1", "eth2", "ethereum_serde_utils", "ethereum_ssz", "execution_layer", + "fixed_bytes", "futures", "genesis", "health_metrics", "hex", "lighthouse_network", + "lighthouse_tracing", "lighthouse_version", "logging", - "lru", + "lru 0.12.5", "metrics", "network", + "network_utils", "operation_pool", - "parking_lot 0.12.3", + "parking_lot", "proto_array", - "rand 0.8.5", + "rand 0.9.2", "safe_arith", "sensitive_url", "serde", @@ -4237,6 +4293,7 @@ dependencies = [ "logging", "malloc_utils", "metrics", + "network_utils", "reqwest", "serde", "slot_clock", @@ -4262,9 +4319,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -4276,14 +4333,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -4292,20 +4349,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.10", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -4313,46 +4372,69 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", + "http 1.3.1", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.35", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.8.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.32", + "http-body-util", + "hyper 1.8.1", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.8.1", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -4360,9 +4442,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -4370,7 +4452,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.62.2", ] [[package]] @@ -4384,21 +4466,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -4407,99 +4490,61 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -4508,9 +4553,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -4519,9 +4564,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -4544,7 +4589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" dependencies = [ "async-io", - "core-foundation", + "core-foundation 0.9.4", "fnv", "futures", "if-addrs", @@ -4555,16 +4600,16 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", - "system-configuration 0.6.1", + "system-configuration", "tokio", - "windows 0.53.0", + "windows", ] [[package]] name = "igd-next" -version = "0.15.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" dependencies = [ "async-trait", "attohttpc", @@ -4572,79 +4617,22 @@ dependencies = [ "futures", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "log", - "rand 0.8.5", + "rand 0.9.2", "tokio", "url", "xmltree", ] -[[package]] -name = "igd-next" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06464e726471718db9ad3fefc020529fabcde03313a0fc3967510e2db5add12" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http 1.3.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "log", - "rand 0.9.1", - "tokio", - "url", - "xmltree", -] - -[[package]] -name = "impl-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" -dependencies = [ - "parity-scale-codec 2.3.1", -] - [[package]] name = "impl-codec" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.7.4", -] - -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" -dependencies = [ - "serde", -] - -[[package]] -name = "impl-serde" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" -dependencies = [ - "serde", + "parity-scale-codec", ] [[package]] @@ -4655,15 +4643,9 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - [[package]] name = "indexmap" version = "1.9.3" @@ -4672,18 +4654,20 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -4697,8 +4681,8 @@ dependencies = [ "filesystem", "lockfile", "metrics", - "parking_lot 0.12.3", - "rand 0.8.5", + "parking_lot", + "rand 0.9.2", "reqwest", "serde", "serde_json", @@ -4718,19 +4702,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "generic-array", ] [[package]] @@ -4751,25 +4723,14 @@ dependencies = [ "num-traits", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", - "widestring 1.2.0", + "socket2 0.5.10", + "widestring 1.2.1", "windows-sys 0.48.0", "winreg", ] @@ -4781,21 +4742,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "is-terminal" -version = "0.4.16" +name = "iri-string" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ - "hermit-abi 0.5.1", + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -4824,6 +4795,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -4832,19 +4812,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -4865,19 +4845,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "k256" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" -dependencies = [ - "cfg-if", - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2 0.10.9", - "sha3 0.10.8", -] - [[package]] name = "k256" version = "0.13.4" @@ -4885,11 +4852,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "once_cell", + "serdect", "sha2 0.10.9", - "signature 2.2.0", + "signature", ] [[package]] @@ -4917,7 +4885,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b286e6b663fb926e1eeb68528e69cb70ed46c6d65871a21b2215ae8154c6d3c" dependencies = [ - "primitive-types 0.12.2", + "primitive-types", "tiny-keccak", ] @@ -4928,15 +4896,17 @@ dependencies = [ "arbitrary", "c-kzg", "criterion", - "derivative", + "educe", "ethereum_hashing", "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "hex", + "rayon", "rust_eth_kzg", "serde", "serde_json", + "tracing", "tree_hash", ] @@ -4957,7 +4927,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "7.1.0-beta.0" +version = "8.0.1" dependencies = [ "account_utils", "beacon_chain", @@ -4965,7 +4935,6 @@ dependencies = [ "clap", "clap_utils", "deposit_contract", - "env_logger 0.9.3", "environment", "eth2", "eth2_network_config", @@ -4973,11 +4942,13 @@ dependencies = [ "ethereum_hashing", "ethereum_ssz", "execution_layer", + "fixed_bytes", "hex", "lighthouse_network", "lighthouse_version", "log", "malloc_utils", + "network_utils", "rayon", "serde", "serde_json", @@ -5017,18 +4988,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -5048,15 +5019,15 @@ dependencies = [ "indexmap 1.9.3", "libc", "mdbx-sys", - "parking_lot 0.12.3", + "parking_lot", "thiserror 1.0.69", ] [[package]] name = "libp2p" -version = "0.55.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b72dc443ddd0254cb49a794ed6b6728400ee446a0f7ab4a07d0209ee98de20e9" +checksum = "ce71348bf5838e46449ae240631117b487073d5f347c06d434caddcb91dceb5a" dependencies = [ "bytes", "either", @@ -5081,14 +5052,14 @@ dependencies = [ "multiaddr", "pin-project", "rw-stream-sink", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "libp2p-allow-block-list" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38944b7cb981cc93f2f0fb411ff82d0e983bd226fbcc8d559639a3a73236568b" +checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5097,9 +5068,9 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe9323175a17caa8a2ed4feaf8a548eeef5e0b72d03840a0eab4bcb0210ce1c" +checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5108,9 +5079,9 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193c75710ba43f7504ad8f58a62ca0615b1d7e572cb0f1780bc607252c39e9ef" +checksum = "4d28e2d2def7c344170f5c6450c0dbe3dfef655610dbfde2f6ac28a527abbe36" dependencies = [ "either", "fnv", @@ -5120,13 +5091,12 @@ dependencies = [ "multiaddr", "multihash", "multistream-select", - "once_cell", - "parking_lot 0.12.3", + "parking_lot", "pin-project", "quick-protobuf", "rand 0.8.5", "rw-stream-sink", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "unsigned-varint 0.8.0", "web-time", @@ -5134,26 +5104,26 @@ dependencies = [ [[package]] name = "libp2p-dns" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b780a1150214155b0ed1cdf09fbd2e1b0442604f9146a431d1b21d23eef7bd7" +checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" dependencies = [ "async-trait", "futures", "hickory-resolver", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.3", + "parking_lot", "smallvec", "tracing", ] [[package]] name = "libp2p-gossipsub" -version = "0.49.0" -source = "git+https://github.com/sigp/rust-libp2p.git?rev=61b2820#61b2820de7a3fab5ae5e1362c4dfa93bd7c41e98" +version = "0.50.0" +source = "git+https://github.com/sigp/rust-libp2p.git?rev=5acdf89a65d64098f9346efa5769e57bcd19dea9#5acdf89a65d64098f9346efa5769e57bcd19dea9" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "asynchronous-codec", "base64 0.22.1", "byteorder", @@ -5163,7 +5133,7 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.16", - "hashlink 0.9.1", + "hashlink 0.10.0", "hex_fmt", "libp2p-core", "libp2p-identity", @@ -5180,9 +5150,9 @@ dependencies = [ [[package]] name = "libp2p-identify" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c06862544f02d05d62780ff590cc25a75f5c2b9df38ec7a370dcae8bb873cf" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" dependencies = [ "asynchronous-codec", "either", @@ -5195,37 +5165,35 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", ] [[package]] name = "libp2p-identity" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb68ea10844211a59ce46230909fd0ea040e8a192454d4cc2ee0d53e12280eb" +checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" dependencies = [ "asn1_der", "bs58 0.5.1", "ed25519-dalek", "hkdf", - "k256 0.13.4", + "k256", "multihash", - "p256", "quick-protobuf", "rand 0.8.5", - "sec1 0.7.3", "sha2 0.10.9", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "zeroize", ] [[package]] name = "libp2p-mdns" -version = "0.47.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d0ba095e1175d797540e16b62e7576846b883cb5046d4159086837b36846cc" +checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" dependencies = [ "futures", "hickory-proto", @@ -5235,16 +5203,16 @@ dependencies = [ "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2", + "socket2 0.5.10", "tokio", "tracing", ] [[package]] name = "libp2p-metrics" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce58c64292e87af624fcb86465e7dd8342e46a388d71e8fec0ab37ee789630a" +checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" dependencies = [ "futures", "libp2p-core", @@ -5258,9 +5226,9 @@ dependencies = [ [[package]] name = "libp2p-mplex" -version = "0.43.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aaa6fee3722e355443058472fc4705d78681bc2d8e447a0bdeb3fecf40cd197" +checksum = "95a4019ba30c4e42b776113e9778071691fe3f34bf23b6b3bf0dfcf29d801f3d" dependencies = [ "asynchronous-codec", "bytes", @@ -5268,7 +5236,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "nohash-hasher", - "parking_lot 0.12.3", + "parking_lot", "rand 0.8.5", "smallvec", "tracing", @@ -5277,9 +5245,9 @@ dependencies = [ [[package]] name = "libp2p-noise" -version = "0.46.0" +version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afcc133e0f3cea07acde6eb8a9665cb11b600bd61110b010593a0210b8153b16" +checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" dependencies = [ "asynchronous-codec", "bytes", @@ -5288,12 +5256,11 @@ dependencies = [ "libp2p-identity", "multiaddr", "multihash", - "once_cell", "quick-protobuf", "rand 0.8.5", "snow", "static_assertions", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "x25519-dalek", "zeroize", @@ -5317,9 +5284,9 @@ dependencies = [ [[package]] name = "libp2p-quic" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41432a159b00424a0abaa2c80d786cddff81055ac24aa127e0cf375f7858d880" +checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" dependencies = [ "futures", "futures-timer", @@ -5330,18 +5297,18 @@ dependencies = [ "quinn", "rand 0.8.5", "ring", - "rustls 0.23.27", - "socket2", - "thiserror 2.0.12", + "rustls 0.23.35", + "socket2 0.5.10", + "thiserror 2.0.17", "tokio", "tracing", ] [[package]] name = "libp2p-swarm" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803399b4b6f68adb85e63ab573ac568154b193e9a640f03e0f2890eabbcb37f8" +checksum = "6aa762e5215919a34e31c35d4b18bf2e18566ecab7f8a3d39535f4a3068f8b62" dependencies = [ "either", "fnv", @@ -5350,9 +5317,8 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm-derive", - "lru", + "lru 0.12.5", "multistream-select", - "once_cell", "rand 0.8.5", "smallvec", "tokio", @@ -5362,37 +5328,36 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" -version = "0.35.0" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206e0aa0ebe004d778d79fb0966aa0de996c19894e2c0605ba2f8524dd4443d8" +checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" dependencies = [ - "heck 0.5.0", - "proc-macro2", + "heck", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "libp2p-tcp" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65346fb4d36035b23fec4e7be4c320436ba53537ce9b6be1d1db1f70c905cad0" +checksum = "65b4e030c52c46c8d01559b2b8ca9b7c4185f10576016853129ca1fe5cd1a644" dependencies = [ "futures", "futures-timer", "if-watch", "libc", "libp2p-core", - "socket2", + "socket2 0.5.10", "tokio", "tracing", ] [[package]] name = "libp2p-tls" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bbf5084fb44133267ad4caaa72a253d68d709edd2ed1cf9b42431a8ead8fd5" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" dependencies = [ "futures", "futures-rustls", @@ -5400,22 +5365,22 @@ dependencies = [ "libp2p-identity", "rcgen", "ring", - "rustls 0.23.27", - "rustls-webpki 0.101.7", - "thiserror 2.0.12", + "rustls 0.23.35", + "rustls-webpki 0.103.8", + "thiserror 2.0.17", "x509-parser", "yasna", ] [[package]] name = "libp2p-upnp" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d457b9ecceb66e7199f049926fad447f1f17f040e8d29d690c086b4cab8ed14a" +checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" dependencies = [ "futures", "futures-timer", - "igd-next 0.15.1", + "igd-next", "libp2p-core", "libp2p-swarm", "tokio", @@ -5431,19 +5396,19 @@ dependencies = [ "either", "futures", "libp2p-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "yamux 0.12.1", - "yamux 0.13.4", + "yamux 0.13.8", ] [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "libc", ] @@ -5460,9 +5425,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "pkg-config", @@ -5471,7 +5436,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "7.1.0-beta.0" +version = "8.0.1" dependencies = [ "account_manager", "account_utils", @@ -5482,20 +5447,25 @@ dependencies = [ "boot_node", "clap", "clap_utils", + "console-subscriber", "database_manager", "directory", "environment", - "eth1", "eth2", "eth2_network_config", "ethereum_hashing", "futures", "initialized_validators", "lighthouse_network", + "lighthouse_tracing", "lighthouse_version", "logging", "malloc_utils", "metrics", + "network_utils", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "sensitive_url", "serde", "serde_json", @@ -5506,9 +5476,9 @@ dependencies = [ "task_executor", "tempfile", "tracing", + "tracing-opentelemetry", "tracing-subscriber", "types", - "unused_port", "validator_client", "validator_dir", "validator_manager", @@ -5522,6 +5492,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "async-channel 1.9.0", + "bls", "bytes", "delay_map", "directory", @@ -5531,6 +5502,7 @@ dependencies = [ "eth2", "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "fnv", "futures", "hex", @@ -5541,14 +5513,14 @@ dependencies = [ "lighthouse_version", "local-ip-address", "logging", - "lru", + "lru 0.12.5", "lru_cache", "metrics", - "parking_lot 0.12.3", + "network_utils", + "parking_lot", "prometheus-client", - "quickcheck", - "quickcheck_macros", - "rand 0.8.5", + "proptest", + "rand 0.9.2", "regex", "serde", "sha2 0.9.9", @@ -5559,23 +5531,26 @@ dependencies = [ "superstruct", "task_executor", "tempfile", - "tiny-keccak", "tokio", - "tokio-io-timeout", "tokio-util", "tracing", "tracing-subscriber", + "typenum", "types", "unsigned-varint 0.8.0", - "unused_port", ] +[[package]] +name = "lighthouse_tracing" +version = "0.1.0" + [[package]] name = "lighthouse_validator_store" version = "0.1.0" dependencies = [ "account_utils", "beacon_node_fallback", + "bls", "doppelganger_service", "either", "environment", @@ -5583,7 +5558,7 @@ dependencies = [ "futures", "initialized_validators", "logging", - "parking_lot 0.12.3", + "parking_lot", "serde", "signing_method", "slashing_protection", @@ -5598,19 +5573,11 @@ dependencies = [ [[package]] name = "lighthouse_version" -version = "0.1.0" +version = "8.0.1" dependencies = [ - "git-version", "regex", - "target_info", ] -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -5619,15 +5586,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lmdb-rkv" @@ -5658,17 +5625,16 @@ checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" dependencies = [ "libc", "neli", - "thiserror 2.0.12", + "thiserror 2.0.17", "windows-sys 0.59.0", ] [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -5682,9 +5648,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "logging" @@ -5706,9 +5672,9 @@ dependencies = [ [[package]] name = "logroller" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90536db32a1cb3672665cdf3269bf030b0f395fabee863895c27b75b9f7a8a7d" +checksum = "83db12bbf439ebe64c0b0e4402f435b6f866db498fc1ae17e1b5d1a01625e2be" dependencies = [ "chrono", "flate2", @@ -5716,28 +5682,30 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lru_cache" version = "0.1.0" @@ -5748,20 +5716,31 @@ dependencies = [ [[package]] name = "mach2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "malloc_utils" version = "0.1.0" dependencies = [ "libc", "metrics", - "parking_lot 0.12.3", + "parking_lot", "tikv-jemalloc-ctl", "tikv-jemallocator", ] @@ -5773,12 +5752,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] -name = "matchers" -version = "0.1.0" +name = "match-lookup" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" dependencies = [ - "regex-automata 0.1.10", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", ] [[package]] @@ -5787,6 +5777,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "mdbx-sys" version = "0.11.6-4" @@ -5806,9 +5802,9 @@ checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -5826,8 +5822,7 @@ dependencies = [ "alloy-primitives", "ethereum_hashing", "fixed_bytes", - "quickcheck", - "quickcheck_macros", + "proptest", "safe_arith", ] @@ -5863,18 +5858,19 @@ dependencies = [ [[package]] name = "milhouse" -version = "0.5.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1ada1f56cc1c79f40517fdcbf57e19f60424a3a1ce372c3fe9b22e4fdd83eb" +checksum = "259dd9da2ae5e0278b95da0b7ecef9c18c309d0a2d9e6db57ed33b9e8910c5e7" dependencies = [ "alloy-primitives", "arbitrary", + "context_deserialize", "educe", "ethereum_hashing", "ethereum_ssz", "ethereum_ssz_derive", "itertools 0.13.0", - "parking_lot 0.12.3", + "parking_lot", "rayon", "serde", "smallvec", @@ -5908,22 +5904,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -5932,6 +5929,44 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0" +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "mockall_double" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "mockito" version = "1.7.0" @@ -5945,10 +5980,10 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "log", - "rand 0.9.1", + "rand 0.9.2", "regex", "serde_json", "serde_urlencoded", @@ -5958,21 +5993,20 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", - "parking_lot 0.12.3", + "equivalent", + "parking_lot", "portable-atomic", "rustc_version 0.4.1", "smallvec", "tagptr", - "thiserror 1.0.69", - "uuid 1.16.0", + "uuid 1.18.1", ] [[package]] @@ -6021,11 +6055,12 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" dependencies = [ "base-x", + "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -6066,7 +6101,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -6144,7 +6179,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -6172,29 +6207,33 @@ dependencies = [ "beacon_processor", "bls", "delay_map", - "derivative", + "educe", "eth2", "eth2_network_config", "ethereum_ssz", "execution_layer", + "fixed_bytes", "fnv", "futures", "genesis", "hex", - "igd-next 0.16.1", + "igd-next", "itertools 0.10.5", - "k256 0.13.4", + "k256", "kzg", "libp2p-gossipsub", "lighthouse_network", + "lighthouse_tracing", "logging", "lru_cache", "matches", "metrics", "operation_pool", - "parking_lot 0.12.3", + "parking_lot", "rand 0.8.5", + "rand 0.9.2", "rand_chacha 0.3.1", + "rand_chacha 0.9.0", "serde_json", "slot_clock", "smallvec", @@ -6206,9 +6245,25 @@ dependencies = [ "tokio-stream", "tracing", "tracing-subscriber", + "typenum", "types", ] +[[package]] +name = "network_utils" +version = "0.1.0" +dependencies = [ + "discv5", + "hex", + "libp2p-identity", + "lru_cache", + "metrics", + "multiaddr", + "parking_lot", + "serde", + "tiny-keccak", +] + [[package]] name = "nix" version = "0.24.3" @@ -6233,11 +6288,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -6287,12 +6342,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -6307,11 +6361,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -6361,28 +6414,69 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] [[package]] -name = "object" -version = "0.36.7" +name = "num_enum" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "memchr", + "num_enum_derive", + "rustversion", ] [[package]] -name = "oid-registry" -version = "0.7.1" +name = "num_enum_derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "nybbles" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ "asn1-rs", ] @@ -6392,12 +6486,22 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oneshot_broadcast" version = "0.1.0" dependencies = [ - "parking_lot 0.12.3", + "parking_lot", ] [[package]] @@ -6406,50 +6510,19 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "open-fastrlp" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" -dependencies = [ - "arrayvec", - "auto_impl 1.3.0", - "bytes", - "ethereum-types 0.14.1", - "open-fastrlp-derive", -] - -[[package]] -name = "open-fastrlp-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" -dependencies = [ - "bytes", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -6466,7 +6539,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -6477,18 +6550,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.0+3.5.0" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.108" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -6497,116 +6570,141 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http 1.3.1", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.17", + "tokio", + "tonic 0.13.1", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic 0.13.1", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "operation_pool" version = "0.2.0" dependencies = [ "beacon_chain", - "bitvec 1.0.1", - "derivative", + "bitvec", + "bls", + "educe", "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "itertools 0.10.5", "maplit", "metrics", - "parking_lot 0.12.3", - "rand 0.8.5", + "parking_lot", + "rand 0.9.2", "rayon", "serde", "state_processing", "store", + "superstruct", "tokio", + "typenum", "types", ] -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder", - "sha2 0.10.9", -] - [[package]] name = "pairing" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" dependencies = [ - "group 0.13.0", + "group", ] [[package]] name = "parity-scale-codec" -version = "2.3.1" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec", - "bitvec 0.20.4", - "byte-slice-cast", - "impl-trait-for-tuples", - "parity-scale-codec-derive 2.3.1", - "serde", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" -dependencies = [ - "arrayvec", - "bitvec 1.0.1", + "bitvec", "byte-slice-cast", "const_format", "impl-trait-for-tuples", - "parity-scale-codec-derive 3.7.4", + "parity-scale-codec-derive", "rustversion", "serde", ] [[package]] name = "parity-scale-codec-derive" -version = "2.3.1" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" -dependencies = [ - "proc-macro-crate 3.3.0", - "proc-macro2", - "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -6617,50 +6715,25 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.6" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.12", + "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -6703,50 +6776,30 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", + "serde_core", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.12", "ucd-trie", ] -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version 0.4.1", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -6764,7 +6817,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -6779,24 +6832,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", ] [[package]] @@ -6841,17 +6884,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -6861,7 +6903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures", - "opaque-debug 0.3.1", + "opaque-debug", "universal-hash", ] @@ -6873,15 +6915,24 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug 0.3.1", + "opaque-debug", "universal-hash", ] [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] [[package]] name = "powerfmt" @@ -6898,6 +6949,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_reqwest_error" version = "0.1.0" @@ -6908,34 +6985,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.101", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - -[[package]] -name = "primitive-types" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" -dependencies = [ - "fixed-hash 0.7.0", - "impl-codec 0.5.1", - "impl-rlp", - "impl-serde 0.3.2", - "uint 0.9.5", + "syn 2.0.110", ] [[package]] @@ -6944,79 +6999,70 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ - "fixed-hash 0.8.0", - "impl-codec 0.6.0", - "impl-rlp", - "impl-serde 0.4.0", - "scale-info", + "fixed-hash", + "impl-codec", "uint 0.9.5", ] [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] -name = "proc-macro-crate" -version = "3.3.0" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit 0.22.26", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ "proc-macro2", "quote", - "version_check", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "procfs" -version = "0.15.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 1.3.2", - "byteorder", - "chrono", - "flate2", + "bitflags 2.10.0", + "procfs-core", + "rustix 1.1.2", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.10.0", "hex", - "lazy_static", - "rustix 0.36.17", ] [[package]] @@ -7029,19 +7075,19 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.3", + "parking_lot", "thiserror 1.0.69", ] [[package]] name = "prometheus-client" -version = "0.22.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" +checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" dependencies = [ "dtoa", "itoa", - "parking_lot 0.12.3", + "parking_lot", "prometheus-client-derive-encode", ] @@ -7053,24 +7099,23 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "proptest" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.0", - "lazy_static", + "bitflags 2.10.0", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_xorshift", - "regex-syntax 0.8.5", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -7078,13 +7123,45 @@ dependencies = [ [[package]] name = "proptest-derive" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", ] [[package]] @@ -7093,6 +7170,7 @@ version = "0.2.0" dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "safe_arith", "serde", "serde_yaml", @@ -7147,33 +7225,11 @@ dependencies = [ "unsigned-varint 0.8.0", ] -[[package]] -name = "quickcheck" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" -dependencies = [ - "env_logger 0.8.4", - "log", - "rand 0.8.5", -] - -[[package]] -name = "quickcheck_macros" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -7182,9 +7238,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.27", - "socket2", - "thiserror 2.0.12", + "rustls 0.23.35", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -7192,19 +7248,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.11" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.2", - "rand 0.9.1", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.27", + "rustls 0.23.35", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -7212,32 +7269,32 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.12" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "r2d2" @@ -7246,7 +7303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot 0.12.3", + "parking_lot", "scheduled-thread-pool", ] @@ -7260,18 +7317,6 @@ dependencies = [ "rusqlite", ] -[[package]] -name = "radium" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" - -[[package]] -name = "radium" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" - [[package]] name = "radium" version = "0.7.0" @@ -7292,12 +7337,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", + "serde", ] [[package]] @@ -7335,7 +7381,8 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", + "serde", ] [[package]] @@ -7348,10 +7395,19 @@ dependencies = [ ] [[package]] -name = "rayon" -version = "1.10.0" +name = "rand_xorshift" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -7359,9 +7415,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -7382,29 +7438,20 @@ dependencies = [ [[package]] name = "redb" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34bc6763177194266fc3773e2b2bb3693f7b02fdf461e285aa33202e3164b74e" +checksum = "8eca1e9d98d5a7e9002d0013e18d5a9b000aee942eb134883a82f06ebffb6c01" dependencies = [ "libc", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" -dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] @@ -7418,86 +7465,91 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", "hyper-rustls", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "quinn", + "rustls 0.23.35", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration 0.5.1", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.4", "tokio-util", + "tower 0.5.2", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -7505,14 +7557,13 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots", - "winreg", ] [[package]] name = "reqwest-eventsource" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f529a5ff327743addc322af460761dff5b50e0c826b9e6ac44c3195c50bb2026" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" dependencies = [ "eventsource-stream", "futures-core", @@ -7526,20 +7577,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac 0.12.1", - "zeroize", -] +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" @@ -7565,15 +7605,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "rlp" version = "0.5.2" @@ -7584,17 +7615,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rlp-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rpassword" version = "5.0.1" @@ -7634,28 +7654,29 @@ dependencies = [ [[package]] name = "ruint" -version = "1.14.0" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a46eb779843b2c4f21fac5773e25d6d5b7c8f0922876c91541790d2ca27eef" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", + "ark-ff 0.5.0", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", "num-bigint", "num-integer", "num-traits", - "parity-scale-codec 3.7.4", - "primitive-types 0.12.2", + "parity-scale-codec", + "primitive-types", "proptest", "rand 0.8.5", - "rand 0.9.1", + "rand 0.9.2", "rlp", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -7682,24 +7703,21 @@ dependencies = [ [[package]] name = "rust_eth_kzg" -version = "0.5.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f83b5559e1dcd3f7721838909288faf4500fb466eff98eac99b67ac04335b93" +checksum = "1522b7a740cd7f5bc52ea49863618511c8de138dcdf3f8a80b15b3f764942a5b" dependencies = [ - "crate_crypto_internal_eth_kzg_bls12_381", - "crate_crypto_internal_eth_kzg_erasure_codes", - "crate_crypto_kzg_multi_open_fk20", + "eip4844", + "ekzg-bls12-381", + "ekzg-erasure-codes", + "ekzg-multi-open", + "ekzg-serialization", + "ekzg-trusted-setup", "hex", "serde", "serde_json", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -7733,7 +7751,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -7745,56 +7763,30 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.36.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.1.4", - "windows-sys 0.45.0", -] - [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", + "linux-raw-sys 0.11.0", + "windows-sys 0.52.0", ] [[package]] @@ -7813,25 +7805,29 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.2", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-native-certs" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ - "base64 0.21.7", + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -7845,24 +7841,14 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", ] -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.8" @@ -7876,9 +7862,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.2" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -7887,15 +7873,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -7923,6 +7909,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe_arith" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b147bb6111014916d3ef9d4c85173124a8e12193a67f6176d67244afd558d6c1" [[package]] name = "salsa20" @@ -7933,15 +7921,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "same-file" version = "1.0.6" @@ -7951,37 +7930,13 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scale-info" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" -dependencies = [ - "cfg-if", - "derive_more 1.0.0", - "parity-scale-codec 3.7.4", - "scale-info-derive", -] - -[[package]] -name = "scale-info-derive" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" -dependencies = [ - "proc-macro-crate 3.3.0", - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7990,7 +7945,31 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot 0.12.3", + "parking_lot", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] @@ -8013,68 +7992,67 @@ checksum = "879588d8f90906e73302547e20fffefdd240eb3e0e744e142321f5d49dea0518" dependencies = [ "hmac 0.11.0", "pbkdf2 0.8.0", - "salsa20 0.8.1", + "salsa20", "sha2 0.9.9", ] -[[package]] -name = "scrypt" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" -dependencies = [ - "hmac 0.12.1", - "pbkdf2 0.11.0", - "salsa20 0.10.2", - "sha2 0.10.9", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array 0.14.7", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array 0.14.7", - "pkcs8 0.10.2", + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -8082,9 +8060,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -8101,11 +8079,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -8117,15 +8096,11 @@ dependencies = [ "pest", ] -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - [[package]] name = "sensitive_url" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b0221fa9905eec4163dbf7660b1876cc95663af1deddc3e19ebe49167c58c" dependencies = [ "serde", "url", @@ -8133,34 +8108,14 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] -[[package]] -name = "serde-aux" -version = "4.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "207f67b28fe90fb596503a9bf0bf1ea5e831e21307658e177c5dfcdfc3ab8a0a" -dependencies = [ - "serde", - "serde-value", - "serde_json", -] - -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_array_query" version = "0.1.0" @@ -8172,26 +8127,36 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -8202,7 +8167,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -8217,19 +8182,60 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.0", "itoa", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -8241,18 +8247,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" version = "0.9.9" @@ -8263,7 +8257,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.1", + "opaque-debug", ] [[package]] @@ -8277,18 +8271,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha3" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "keccak", - "opaque-debug 0.3.1", -] - [[package]] name = "sha3" version = "0.10.8" @@ -8326,23 +8308,13 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "2.2.0" @@ -8357,18 +8329,26 @@ dependencies = [ name = "signing_method" version = "0.1.0" dependencies = [ + "bls", "eth2_keystore", "ethereum_serde_utils", "lockfile", - "parking_lot 0.12.3", + "parking_lot", "reqwest", "serde", "task_executor", + "tracing", "types", "url", "validator_metrics", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.7.0" @@ -8383,7 +8363,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -8392,52 +8372,50 @@ name = "simulator" version = "0.2.0" dependencies = [ "clap", - "env_logger 0.9.3", "environment", - "eth2_network_config", "execution_layer", "futures", "kzg", "logging", "node_test_rig", - "parking_lot 0.12.3", + "parking_lot", "rayon", "sensitive_url", "serde_json", "tokio", "tracing", "tracing-subscriber", + "typenum", "types", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slasher" version = "0.1.0" dependencies = [ "bincode", + "bls", "byteorder", - "derivative", + "educe", "ethereum_ssz", "ethereum_ssz_derive", "filesystem", + "fixed_bytes", "flate2", "libmdbx", "lmdb-rkv", "lmdb-rkv-sys", - "lru", + "lru 0.12.5", "maplit", "metrics", - "parking_lot 0.12.3", - "rand 0.8.5", + "parking_lot", + "rand 0.9.2", "rayon", "redb", "safe_arith", @@ -8448,6 +8426,7 @@ dependencies = [ "tracing", "tree_hash", "tree_hash_derive", + "typenum", "types", ] @@ -8473,8 +8452,11 @@ name = "slashing_protection" version = "0.1.0" dependencies = [ "arbitrary", + "bls", + "eip_3076", "ethereum_serde_utils", "filesystem", + "fixed_bytes", "r2d2", "r2d2_sqlite", "rayon", @@ -8491,17 +8473,18 @@ name = "slot_clock" version = "0.2.0" dependencies = [ "metrics", - "parking_lot 0.12.3", + "parking_lot", "types", ] [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "arbitrary", + "serde", ] [[package]] @@ -8529,30 +8512,30 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - [[package]] name = "spki" version = "0.7.3" @@ -8560,19 +8543,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.10", + "der", ] [[package]] name = "ssz_types" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad0fa7e9a85c06d0a6ba5100d733fff72e231eb6db2d86078225cf716fd2d95" +checksum = "1fc20a89bab2dabeee65e9c9eb96892dc222c23254b401e1319b85efd852fa31" dependencies = [ "arbitrary", + "context_deserialize", + "educe", "ethereum_serde_utils", "ethereum_ssz", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_derive", "smallvec", @@ -8582,9 +8567,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state_processing" @@ -8593,24 +8578,27 @@ dependencies = [ "arbitrary", "beacon_chain", "bls", - "derivative", - "env_logger 0.9.3", + "educe", "ethereum_hashing", "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "int_to_bytes", "integer-sqrt", "itertools 0.10.5", "merkle_proof", "metrics", - "rand 0.8.5", + "milhouse", + "rand 0.9.2", "rayon", "safe_arith", "smallvec", "ssz_types", "test_random_derive", "tokio", + "tracing", "tree_hash", + "typenum", "types", ] @@ -8619,7 +8607,9 @@ name = "state_transition_vectors" version = "0.1.0" dependencies = [ "beacon_chain", + "bls", "ethereum_ssz", + "fixed_bytes", "state_processing", "tokio", "types", @@ -8642,23 +8632,27 @@ dependencies = [ "directory", "ethereum_ssz", "ethereum_ssz_derive", + "fixed_bytes", "itertools 0.10.5", "leveldb", "logging", - "lru", + "lru 0.12.5", "metrics", - "parking_lot 0.12.3", - "rand 0.8.5", + "milhouse", + "parking_lot", + "rand 0.9.2", "redb", "safe_arith", "serde", "smallvec", + "ssz_types", "state_processing", "strum", "superstruct", "tempfile", "tracing", "tracing-subscriber", + "typenum", "types", "xdelta3", "zstd 0.13.3", @@ -8678,24 +8672,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.24.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.24.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", - "rustversion", - "syn 1.0.109", + "syn 2.0.110", ] [[package]] @@ -8706,16 +8699,16 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "superstruct" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" +checksum = "3b986e4a629907f20a2c2a639a75bc22a8b5d99b444e0d83c395f4cb309022bf" dependencies = [ - "darling 0.13.4", - "itertools 0.10.5", + "darling 0.20.11", + "itertools 0.13.0", "proc-macro2", "quote", "smallvec", - "syn 1.0.109", + "syn 2.0.110", ] [[package]] @@ -8741,9 +8734,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -8751,10 +8744,25 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "0.1.2" +name = "syn-solidity" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -8764,7 +8772,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -8782,36 +8790,15 @@ dependencies = [ "winapi", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", - "core-foundation", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", ] [[package]] @@ -8829,7 +8816,9 @@ name = "system_health" version = "0.1.0" dependencies = [ "lighthouse_network", - "parking_lot 0.12.3", + "metrics", + "network_utils", + "parking_lot", "serde", "sysinfo", "types", @@ -8854,12 +8843,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "target_info" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" - [[package]] name = "task_executor" version = "0.1.0" @@ -8867,48 +8850,47 @@ dependencies = [ "async-channel 1.9.0", "futures", "metrics", + "num_cpus", + "rayon", "tokio", "tracing", ] [[package]] name = "tempfile" -version = "3.19.1" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", + "rustix 1.1.2", + "windows-sys 0.52.0", ] [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.60.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "test_random_derive" version = "0.2.0" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.110", ] [[package]] @@ -8922,11 +8904,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -8937,28 +8919,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -8972,9 +8953,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-ctl" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b" +checksum = "661f1f6a57b3a36dc9174a2c10f19513b4866816e13425d3e418b11cc37bc24c" dependencies = [ "libc", "paste", @@ -8983,9 +8964,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" dependencies = [ "cc", "libc", @@ -8993,9 +8974,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" dependencies = [ "libc", "tikv-jemalloc-sys", @@ -9003,9 +8984,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -9018,15 +8999,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -9073,9 +9054,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -9093,9 +9074,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -9108,41 +9089,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "tracing", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -9155,16 +9126,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -9176,6 +9137,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.35", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -9190,9 +9161,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -9203,43 +9174,157 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ - "indexmap 2.9.0", - "toml_datetime", - "winnow 0.5.40", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.0", "toml_datetime", - "winnow 0.7.10", + "toml_parser", + "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.4", + "tokio-stream", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 2.12.0", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -9272,35 +9357,25 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -9312,6 +9387,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" @@ -9324,14 +9417,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -9345,9 +9438,9 @@ dependencies = [ [[package]] name = "tree_hash" -version = "0.9.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c58eb0f518840670270d90d97ffee702d8662d9c5494870c9e1e9e0fa00f668" +checksum = "2db21caa355767db4fd6129876e5ae278a8699f4a6959b1e3e7aff610b532d52" dependencies = [ "alloy-primitives", "ethereum_hashing", @@ -9358,14 +9451,14 @@ dependencies = [ [[package]] name = "tree_hash_derive" -version = "0.9.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699e7fb6b3fdfe0c809916f251cf5132d64966858601695c3736630a87e7166a" +checksum = "711cc655fcbb48384a87dc2bf641b991a15c5ad9afc3caa0b1ab1df3b436f70f" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -9380,9 +9473,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" dependencies = [ "serde", "stable_deref_trait", @@ -9396,9 +9489,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "types" @@ -9410,11 +9503,9 @@ dependencies = [ "beacon_chain", "bls", "compare_fields", - "compare_fields_derive", "context_deserialize", - "context_deserialize_derive", "criterion", - "derivative", + "educe", "eth2_interop_keypairs", "ethereum_hashing", "ethereum_serde_utils", @@ -9429,10 +9520,10 @@ dependencies = [ "merkle_proof", "metastruct", "milhouse", - "parking_lot 0.12.3", + "parking_lot", "paste", - "rand 0.8.5", - "rand_xorshift", + "rand 0.9.2", + "rand_xorshift 0.4.0", "rayon", "regex", "rpds", @@ -9452,6 +9543,7 @@ dependencies = [ "tracing", "tree_hash", "tree_hash_derive", + "typenum", ] [[package]] @@ -9504,25 +9596,19 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -9568,31 +9654,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "unused_port" -version = "0.1.0" -dependencies = [ - "lru_cache", - "parking_lot 0.12.3", -] - [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -9617,16 +9690,18 @@ dependencies = [ [[package]] name = "uuid" -version = "1.16.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", ] [[package]] name = "validator_client" -version = "0.3.5" +version = "8.0.1" dependencies = [ "account_utils", "beacon_node_fallback", @@ -9639,12 +9714,12 @@ dependencies = [ "eth2", "fdlimit", "graffiti_file", - "hyper 1.6.0", + "hyper 1.8.1", "initialized_validators", "lighthouse_validator_store", "metrics", "monitoring_api", - "parking_lot 0.12.3", + "parking_lot", "reqwest", "sensitive_url", "serde", @@ -9666,12 +9741,12 @@ version = "0.1.0" dependencies = [ "bls", "deposit_contract", - "derivative", + "educe", "eth2_keystore", "filesystem", "hex", "lockfile", - "rand 0.8.5", + "rand 0.9.2", "tempfile", "tree_hash", "types", @@ -9692,6 +9767,7 @@ dependencies = [ "eth2_keystore", "ethereum_serde_utils", "filesystem", + "fixed_bytes", "futures", "graffiti_file", "health_metrics", @@ -9700,14 +9776,15 @@ dependencies = [ "lighthouse_validator_store", "lighthouse_version", "logging", - "parking_lot 0.12.3", - "rand 0.8.5", + "parking_lot", + "rand 0.9.2", "sensitive_url", "serde", "serde_json", "signing_method", "slashing_protection", "slot_clock", + "ssz_types", "sysinfo", "system_health", "task_executor", @@ -9715,6 +9792,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "typenum", "types", "url", "validator_dir", @@ -9735,7 +9813,7 @@ dependencies = [ "logging", "malloc_utils", "metrics", - "parking_lot 0.12.3", + "parking_lot", "serde", "slot_clock", "tracing", @@ -9751,18 +9829,22 @@ name = "validator_manager" version = "0.1.0" dependencies = [ "account_utils", + "beacon_chain", + "bls", "clap", "clap_utils", - "derivative", + "educe", "environment", "eth2", "eth2_network_config", "eth2_wallet", "ethereum_serde_utils", "hex", + "http_api", "regex", "serde", "serde_json", + "slot_clock", "tempfile", "tokio", "tree_hash", @@ -9789,7 +9871,7 @@ dependencies = [ "futures", "graffiti_file", "logging", - "parking_lot 0.12.3", + "parking_lot", "safe_arith", "slot_clock", "task_executor", @@ -9805,6 +9887,7 @@ dependencies = [ name = "validator_store" version = "0.1.0" dependencies = [ + "bls", "eth2", "slashing_protection", "types", @@ -9892,7 +9975,7 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project", - "rustls-pemfile 2.2.0", + "rustls-pemfile", "scoped-tls", "serde", "serde_json", @@ -9922,50 +10005,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -9976,9 +10046,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9986,22 +10056,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.101", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -10020,25 +10090,24 @@ dependencies = [ ] [[package]] -name = "wasm-timer" -version = "0.2.5" +name = "wasmtimer" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ "futures", "js-sys", - "parking_lot 0.11.2", + "parking_lot", "pin-utils", + "slab", "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -10060,21 +10129,24 @@ version = "0.1.0" dependencies = [ "account_utils", "async-channel 1.9.0", + "bls", "environment", "eth2", "eth2_keystore", "eth2_network_config", + "fixed_bytes", "futures", "initialized_validators", "lighthouse_validator_store", "logging", - "parking_lot 0.12.3", + "parking_lot", "reqwest", "serde", "serde_json", "serde_yaml", "slashing_protection", "slot_clock", + "ssz_types", "task_executor", "tempfile", "tokio", @@ -10086,9 +10158,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "which" @@ -10110,9 +10185,9 @@ checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -10132,11 +10207,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -10155,16 +10230,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-acl" version = "0.3.0" @@ -10189,79 +10254,44 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" -dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.4.1", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" @@ -10274,50 +10304,22 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -10346,18 +10348,21 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -10384,7 +10389,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -10392,10 +10397,21 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] [[package]] name = "windows_aarch64_gnullvm" @@ -10410,10 +10426,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -10428,10 +10444,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -10445,6 +10461,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -10452,10 +10474,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -10470,10 +10492,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -10488,10 +10510,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -10506,10 +10528,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -10524,19 +10546,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.5.40" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -10552,58 +10571,24 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "workspace_members" version = "0.1.0" dependencies = [ - "cargo_metadata 0.19.2", + "cargo_metadata", "quote", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "ws_stream_wasm" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version 0.4.1", - "send_wrapper", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -10628,9 +10613,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" dependencies = [ "asn1-rs", "data-encoding", @@ -10639,14 +10624,14 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", ] [[package]] name = "xdelta3" version = "0.1.5" -source = "git+http://github.com/sigp/xdelta3-rs?rev=4db64086bb02e9febb584ba93b9d16bb2ae3825a#4db64086bb02e9febb584ba93b9d16bb2ae3825a" +source = "git+https://github.com/sigp/xdelta3-rs?rev=4db64086bb02e9febb584ba93b9d16bb2ae3825a#4db64086bb02e9febb584ba93b9d16bb2ae3825a" dependencies = [ "bindgen", "cc", @@ -10659,9 +10644,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmltree" @@ -10692,7 +10677,7 @@ dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot 0.12.3", + "parking_lot", "pin-project", "rand 0.8.5", "static_assertions", @@ -10700,16 +10685,16 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.4" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17610762a1207ee816c6fadc29220904753648aba0a9ed61c7b8336e80a559c4" +checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot 0.12.3", + "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.9.2", "static_assertions", "web-time", ] @@ -10725,11 +10710,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -10737,34 +10721,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -10784,15 +10768,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "serde", "zeroize_derive", @@ -10806,14 +10790,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -10822,13 +10817,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.110", ] [[package]] @@ -10890,9 +10885,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 86cca0a259..d5d1687c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,27 @@ [workspace] members = [ "account_manager", - "beacon_node", "beacon_node/beacon_chain", "beacon_node/beacon_processor", "beacon_node/builder_client", "beacon_node/client", - "beacon_node/eth1", "beacon_node/execution_layer", "beacon_node/genesis", "beacon_node/http_api", "beacon_node/http_metrics", "beacon_node/lighthouse_network", + "beacon_node/lighthouse_tracing", "beacon_node/network", "beacon_node/operation_pool", "beacon_node/store", "beacon_node/timer", - "boot_node", - "common/account_utils", "common/clap_utils", - "common/compare_fields", - "common/compare_fields_derive", "common/deposit_contract", "common/directory", + "common/eip_3076", "common/eth2", "common/eth2_config", "common/eth2_interop_keypairs", @@ -40,57 +36,43 @@ members = [ "common/malloc_utils", "common/metrics", "common/monitoring_api", + "common/network_utils", "common/oneshot_broadcast", "common/pretty_reqwest_error", - "common/sensitive_url", "common/slot_clock", "common/system_health", "common/target_check", "common/task_executor", "common/test_random_derive", - "common/unused_port", "common/validator_dir", "common/warp_utils", "common/workspace_members", - - "consensus/context_deserialize", - "consensus/context_deserialize_derive", "consensus/fixed_bytes", "consensus/fork_choice", "consensus/int_to_bytes", "consensus/merkle_proof", "consensus/proto_array", - "consensus/safe_arith", "consensus/state_processing", "consensus/swap_or_not_shuffle", "consensus/types", - "crypto/bls", "crypto/eth2_key_derivation", "crypto/eth2_keystore", "crypto/eth2_wallet", "crypto/kzg", - "database_manager", - "lcli", - "lighthouse", "lighthouse/environment", - "slasher", "slasher/service", - "testing/ef_tests", - "testing/eth1_test_rig", "testing/execution_engine_integration", "testing/node_test_rig", "testing/simulator", "testing/state_transition_vectors", "testing/validator_test_rig", "testing/web3signer_tests", - - "validator_client", "validator_client/beacon_node_fallback", "validator_client/doppelganger_service", @@ -103,80 +85,130 @@ members = [ "validator_client/slashing_protection", "validator_client/validator_metrics", "validator_client/validator_services", - "validator_manager", ] resolver = "2" [workspace.package] -edition = "2021" +edition = "2024" +version = "8.0.1" [workspace.dependencies] -alloy-primitives = { version = "0.8", features = ["rlp", "getrandom"] } -alloy-rlp = "0.3.4" -alloy-consensus = "0.3.0" +account_utils = { path = "common/account_utils" } +alloy-consensus = { version = "1", default-features = false } +alloy-dyn-abi = { version = "1", default-features = false } +alloy-json-abi = { version = "1", default-features = false } +alloy-network = { version = "1", default-features = false } +alloy-primitives = { version = "1", default-features = false, features = ["rlp", "getrandom"] } +alloy-provider = { version = "1", default-features = false, features = ["reqwest"] } +alloy-rlp = { version = "0.3", default-features = false } +alloy-rpc-types-eth = { version = "1", default-features = false, features = ["serde"] } +alloy-signer-local = { version = "1", default-features = false } anyhow = "1" arbitrary = { version = "1", features = ["derive"] } async-channel = "1.9.0" axum = "0.7.7" +beacon_chain = { path = "beacon_node/beacon_chain" } +beacon_node = { path = "beacon_node" } +beacon_node_fallback = { path = "validator_client/beacon_node_fallback" } +beacon_processor = { path = "beacon_node/beacon_processor" } bincode = "1" bitvec = "1" +bls = { path = "crypto/bls" } byteorder = "1" bytes = "1" -cargo_metadata = "0.19" -clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } # Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable # feature ourselves when desired. -c-kzg = { version = "1", default-features = false } -compare_fields_derive = { path = "common/compare_fields_derive" } -context_deserialize = { path = "consensus/context_deserialize" } -context_deserialize_derive = { path = "consensus/context_deserialize_derive" } +c-kzg = { version = "2.1", default-features = false } +cargo_metadata = "0.19" +clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } +clap_utils = { path = "common/clap_utils" } +compare_fields = "0.1" +console-subscriber = "0.4" +context_deserialize = "0.2" criterion = "0.5" delay_map = "0.4" -derivative = "2" +deposit_contract = { path = "common/deposit_contract" } +directory = { path = "common/directory" } dirs = "3" +discv5 = { version = "0.10", features = ["libp2p"] } +doppelganger_service = { path = "validator_client/doppelganger_service" } +educe = "0.6" +eip_3076 = { path = "common/eip_3076" } either = "1.9" -rust_eth_kzg = "0.5.4" -discv5 = { version = "0.9", features = ["libp2p"] } -env_logger = "0.9" -ethereum_hashing = "0.7.0" -ethereum_serde_utils = "0.7" -ethereum_ssz = "0.8.2" -ethereum_ssz_derive = "0.8.2" -ethers-core = "1" -ethers-providers = { version = "1", default-features = false } -ethers-signers = { version = "1", default-features = false } -ethers-middleware = { version = "1", default-features = false } +environment = { path = "lighthouse/environment" } +eth2 = { path = "common/eth2" } +eth2_config = { path = "common/eth2_config" } +eth2_key_derivation = { path = "crypto/eth2_key_derivation" } +eth2_keystore = { path = "crypto/eth2_keystore" } +eth2_network_config = { path = "common/eth2_network_config" } +eth2_wallet = { path = "crypto/eth2_wallet" } +ethereum_hashing = "0.8.0" +ethereum_serde_utils = "0.8.0" +ethereum_ssz = { version = "0.10.0", features = ["context_deserialize"] } +ethereum_ssz_derive = "0.10.0" +execution_layer = { path = "beacon_node/execution_layer" } exit-future = "0.2" +filesystem = { path = "common/filesystem" } +fixed_bytes = { path = "consensus/fixed_bytes" } fnv = "1" +fork_choice = { path = "consensus/fork_choice" } fs2 = "0.4" futures = "0.3" +genesis = { path = "beacon_node/genesis" } +# This is tracking the sigp-gossipsub branch on sigp/rust-libp2p commit: Aug 20 2025 +gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/sigp/rust-libp2p.git", rev = "5acdf89a65d64098f9346efa5769e57bcd19dea9", "features" = ["metrics"] } graffiti_file = { path = "validator_client/graffiti_file" } -gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/sigp/rust-libp2p.git", rev = "61b2820" } -hex = "0.4" hashlink = "0.9.0" +health_metrics = { path = "common/health_metrics" } +hex = "0.4" +http_api = { path = "beacon_node/http_api" } hyper = "1" +initialized_validators = { path = "validator_client/initialized_validators" } +int_to_bytes = { path = "consensus/int_to_bytes" } itertools = "0.10" +kzg = { path = "crypto/kzg" } libsecp256k1 = "0.7" +lighthouse_network = { path = "beacon_node/lighthouse_network" } +lighthouse_tracing = { path = "beacon_node/lighthouse_tracing" } +lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } +lighthouse_version = { path = "common/lighthouse_version" } +lockfile = { path = "common/lockfile" } log = "0.4" -logroller = "0.1.4" +logging = { path = "common/logging" } +logroller = "0.1.8" lru = "0.12" +lru_cache = { path = "common/lru_cache" } +malloc_utils = { path = "common/malloc_utils" } maplit = "1" -milhouse = "0.5" +merkle_proof = { path = "consensus/merkle_proof" } +metrics = { path = "common/metrics" } +milhouse = { version = "0.9", default-features = false, features = ["context_deserialize"] } +mockall = "0.13" +mockall_double = "0.3" mockito = "1.5.0" +monitoring_api = { path = "common/monitoring_api" } +network = { path = "beacon_node/network" } +network_utils = { path = "common/network_utils" } +node_test_rig = { path = "testing/node_test_rig" } num_cpus = "1" once_cell = "1.17.1" +opentelemetry = "0.30.0" +opentelemetry-otlp = { version = "0.30.0", features = ["grpc-tonic", "tls-roots"] } +opentelemetry_sdk = "0.30.0" +operation_pool = { path = "beacon_node/operation_pool" } parking_lot = "0.12" paste = "1" +pretty_reqwest_error = { path = "common/pretty_reqwest_error" } prometheus = { version = "0.13", default-features = false } -quickcheck = "1" -quickcheck_macros = "1" +proptest = "1" +proto_array = { path = "consensus/proto_array" } quote = "1" r2d2 = "0.8" -rand = "0.8" +rand = "0.9.0" rayon = "1.7" regex = "1" -reqwest = { version = "0.11", default-features = false, features = [ +reqwest = { version = "0.12", default-features = false, features = [ "blocking", "json", "stream", @@ -186,18 +218,30 @@ reqwest = { version = "0.11", default-features = false, features = [ ring = "0.17" rpds = "0.11" rusqlite = { version = "0.28", features = ["bundled"] } +rust_eth_kzg = "0.9" +safe_arith = "0.1" +sensitive_url = { version = "0.1", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_repr = "0.1" serde_yaml = "0.9" sha2 = "0.9" +signing_method = { path = "validator_client/signing_method" } +slasher = { path = "slasher", default-features = false } +slashing_protection = { path = "validator_client/slashing_protection" } +slot_clock = { path = "common/slot_clock" } smallvec = { version = "1.11.2", features = ["arbitrary"] } snap = "1" -ssz_types = "0.10" -strum = { version = "0.24", features = ["derive"] } -superstruct = "0.8" -syn = "1" +ssz_types = { version = "0.14.0", features = ["context_deserialize", "runtime_types"] } +state_processing = { path = "consensus/state_processing" } +store = { path = "beacon_node/store" } +strum = { version = "0.27", features = ["derive"] } +superstruct = "0.10" +swap_or_not_shuffle = { path = "consensus/swap_or_not_shuffle" } +syn = "2" sysinfo = "0.26" +system_health = { path = "common/system_health" } +task_executor = { path = "common/task_executor" } tempfile = "3" tokio = { version = "1", features = [ "rt-multi-thread", @@ -211,75 +255,14 @@ tracing = "0.1.40" tracing-appender = "0.2" tracing-core = "0.1" tracing-log = "0.2" +tracing-opentelemetry = "0.31.0" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -tree_hash = "0.9" -tree_hash_derive = "0.9" +tree_hash = "0.12.0" +tree_hash_derive = "0.12.0" +typenum = "1" +types = { path = "consensus/types" } url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } -warp = { version = "0.3.7", default-features = false, features = ["tls"] } -zeroize = { version = "1", features = ["zeroize_derive", "serde"] } -zip = "0.6" - -# Local crates. -account_utils = { path = "common/account_utils" } -beacon_chain = { path = "beacon_node/beacon_chain" } -beacon_node = { path = "beacon_node" } -beacon_node_fallback = { path = "validator_client/beacon_node_fallback" } -beacon_processor = { path = "beacon_node/beacon_processor" } -bls = { path = "crypto/bls" } -clap_utils = { path = "common/clap_utils" } -compare_fields = { path = "common/compare_fields" } -deposit_contract = { path = "common/deposit_contract" } -directory = { path = "common/directory" } -doppelganger_service = { path = "validator_client/doppelganger_service" } -environment = { path = "lighthouse/environment" } -eth1 = { path = "beacon_node/eth1" } -eth1_test_rig = { path = "testing/eth1_test_rig" } -eth2 = { path = "common/eth2" } -eth2_config = { path = "common/eth2_config" } -eth2_key_derivation = { path = "crypto/eth2_key_derivation" } -eth2_keystore = { path = "crypto/eth2_keystore" } -eth2_network_config = { path = "common/eth2_network_config" } -eth2_wallet = { path = "crypto/eth2_wallet" } -execution_layer = { path = "beacon_node/execution_layer" } -fixed_bytes = { path = "consensus/fixed_bytes" } -filesystem = { path = "common/filesystem" } -fork_choice = { path = "consensus/fork_choice" } -genesis = { path = "beacon_node/genesis" } -health_metrics = { path = "common/health_metrics" } -http_api = { path = "beacon_node/http_api" } -initialized_validators = { path = "validator_client/initialized_validators" } -int_to_bytes = { path = "consensus/int_to_bytes" } -kzg = { path = "crypto/kzg" } -metrics = { path = "common/metrics" } -lighthouse_network = { path = "beacon_node/lighthouse_network" } -lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } -lighthouse_version = { path = "common/lighthouse_version" } -workspace_members = { path = "common/workspace_members" } -lockfile = { path = "common/lockfile" } -logging = { path = "common/logging" } -lru_cache = { path = "common/lru_cache" } -malloc_utils = { path = "common/malloc_utils" } -merkle_proof = { path = "consensus/merkle_proof" } -monitoring_api = { path = "common/monitoring_api" } -network = { path = "beacon_node/network" } -node_test_rig = { path = "testing/node_test_rig" } -operation_pool = { path = "beacon_node/operation_pool" } -pretty_reqwest_error = { path = "common/pretty_reqwest_error" } -proto_array = { path = "consensus/proto_array" } -safe_arith = { path = "consensus/safe_arith" } -sensitive_url = { path = "common/sensitive_url" } -signing_method = { path = "validator_client/signing_method" } -slasher = { path = "slasher", default-features = false } -slashing_protection = { path = "validator_client/slashing_protection" } -slot_clock = { path = "common/slot_clock" } -state_processing = { path = "consensus/state_processing" } -store = { path = "beacon_node/store" } -swap_or_not_shuffle = { path = "consensus/swap_or_not_shuffle" } -system_health = { path = "common/system_health" } -task_executor = { path = "common/task_executor" } -types = { path = "consensus/types" } -unused_port = { path = "common/unused_port" } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } @@ -288,8 +271,12 @@ validator_metrics = { path = "validator_client/validator_metrics" } validator_services = { path = "validator_client/validator_services" } validator_store = { path = "validator_client/validator_store" } validator_test_rig = { path = "testing/validator_test_rig" } +warp = { version = "0.3.7", default-features = false, features = ["tls"] } warp_utils = { path = "common/warp_utils" } -xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "4db64086bb02e9febb584ba93b9d16bb2ae3825a" } +workspace_members = { path = "common/workspace_members" } +xdelta3 = { git = "https://github.com/sigp/xdelta3-rs", rev = "4db64086bb02e9febb584ba93b9d16bb2ae3825a" } +zeroize = { version = "1", features = ["zeroize_derive", "serde"] } +zip = "0.6" zstd = "0.13" [profile.maxperf] @@ -298,12 +285,9 @@ lto = "fat" codegen-units = 1 incremental = false -[profile.reproducible] +[profile.release-debug] inherits = "release" -debug = false -panic = "abort" -codegen-units = 1 -overflow-checks = true +debug = true [patch.crates-io] quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "681f413312404ab6e51f0b46f39b0075c6f4ebfd" } diff --git a/Dockerfile b/Dockerfile index 437c864c30..8cc20ab000 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,19 @@ -FROM rust:1.84.0-bullseye AS builder +FROM rust:1.88.0-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev -COPY . lighthouse ARG FEATURES ARG PROFILE=release ARG CARGO_USE_GIT_CLI=true ENV FEATURES=$FEATURES ENV PROFILE=$PROFILE ENV CARGO_NET_GIT_FETCH_WITH_CLI=$CARGO_USE_GIT_CLI -RUN cd lighthouse && make +ENV CARGO_INCREMENTAL=1 + +WORKDIR /lighthouse +COPY . . +# Persist the registry and target file across builds. See: https://docs.docker.com/build/cache/optimize/#use-cache-mounts +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/lighthouse/target \ + make FROM ubuntu:22.04 RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \ diff --git a/Dockerfile.reproducible b/Dockerfile.reproducible index df57616874..903515373f 100644 --- a/Dockerfile.reproducible +++ b/Dockerfile.reproducible @@ -1,44 +1,24 @@ -# Define the Rust image as an argument with a default to x86_64 Rust 1.82 image based on Debian Bullseye -ARG RUST_IMAGE="rust:1.82-bullseye@sha256:ac7fe7b0c9429313c0fe87d3a8993998d1fe2be9e3e91b5e2ec05d3a09d87128" +# Define the Rust image as an argument with a default to x86_64 Rust 1.88 image based on Debian Bullseye +ARG RUST_IMAGE="rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816" FROM ${RUST_IMAGE} AS builder # Install specific version of the build dependencies -RUN apt-get update && apt-get install -y libclang-dev=1:11.0-51+nmu5 cmake=3.18.4-2+deb11u1 +RUN apt-get update && apt-get install -y libclang-dev=1:11.0-51+nmu5 cmake=3.18.4-2+deb11u1 libjemalloc-dev=5.2.1-3 -# Add target architecture argument with default value ARG RUST_TARGET="x86_64-unknown-linux-gnu" # Copy the project to the container -COPY . /app +COPY ./ /app WORKDIR /app -# Get the latest commit timestamp and set SOURCE_DATE_EPOCH (default it to 0 if not passed) -ARG SOURCE_DATE=0 - -# Set environment variables for reproducibility -ARG RUSTFLAGS="-C link-arg=-Wl,--build-id=none -C metadata='' --remap-path-prefix $(pwd)=." -ENV SOURCE_DATE_EPOCH=$SOURCE_DATE \ - CARGO_INCREMENTAL=0 \ - LC_ALL=C \ - TZ=UTC \ - RUSTFLAGS="${RUSTFLAGS}" - -# Set the default features if not provided -ARG FEATURES="gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc" - -# Set the default profile if not provided -ARG PROFILE="reproducible" - # Build the project with the reproducible settings -RUN cargo build --bin lighthouse \ - --features "${FEATURES}" \ - --profile "${PROFILE}" \ - --locked \ - --target "${RUST_TARGET}" +RUN make build-reproducible -RUN mv /app/target/${RUST_TARGET}/${PROFILE}/lighthouse /lighthouse +# Move the binary to a standard location +RUN mv /app/target/${RUST_TARGET}/release/lighthouse /lighthouse # Create a minimal final image with just the binary FROM gcr.io/distroless/cc-debian12:nonroot-6755e21ccd99ddead6edc8106ba03888cbeed41a COPY --from=builder /lighthouse /lighthouse + ENTRYPOINT [ "/lighthouse" ] diff --git a/Makefile b/Makefile index d27e7edd13..9d08c3ebe1 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ EF_TESTS = "testing/ef_tests" STATE_TRANSITION_VECTORS = "testing/state_transition_vectors" EXECUTION_ENGINE_INTEGRATION = "testing/execution_engine_integration" -GIT_TAG := $(shell git describe --tags --candidates 1) +GIT_TAG = $(shell git describe --tags --candidates 1) BIN_DIR = "bin" X86_64_TAG = "x86_64-unknown-linux-gnu" @@ -16,7 +16,7 @@ BUILD_PATH_RISCV64 = "target/$(RISCV64_TAG)/release" PINNED_NIGHTLY ?= nightly # List of features to use when cross-compiling. Can be overridden via the environment. -CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc,beacon-node-leveldb,beacon-node-redb +CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,beacon-node-leveldb,beacon-node-redb # Cargo profile for Cross builds. Default is for local builds, CI uses an override. CROSS_PROFILE ?= release @@ -30,9 +30,13 @@ TEST_FEATURES ?= # Cargo profile for regular builds. PROFILE ?= release -# List of all hard forks. This list is used to set env variables for several tests so that +# List of all hard forks up to gloas. This list is used to set env variables for several tests so that # they run for different forks. -FORKS=phase0 altair bellatrix capella deneb electra eip7805 fulu +# TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead +RECENT_FORKS_BEFORE_GLOAS=electra fulu + +# List of all recent hard forks. This list is used to set env variables for http_api tests +RECENT_FORKS=electra fulu gloas # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -82,36 +86,67 @@ build-lcli-aarch64: build-lcli-riscv64: cross build --bin lcli --target riscv64gc-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked -# extracts the current source date for reproducible builds -SOURCE_DATE := $(shell git log -1 --pretty=%ct) +# Environment variables for reproducible builds +# Initialize RUSTFLAGS +RUST_BUILD_FLAGS = +# Remove build ID from the binary to ensure reproducibility across builds +RUST_BUILD_FLAGS += -C link-arg=-Wl,--build-id=none +# Remove metadata hash from symbol names to ensure reproducible builds +RUST_BUILD_FLAGS += -C metadata='' -# Default image for x86_64 -RUST_IMAGE_AMD64 ?= rust:1.82-bullseye@sha256:ac7fe7b0c9429313c0fe87d3a8993998d1fe2be9e3e91b5e2ec05d3a09d87128 +# Set timestamp from last git commit for reproducible builds +SOURCE_DATE ?= $(shell git log -1 --pretty=%ct) -# Reproducible build for x86_64 -build-reproducible-x86_64: +# Disable incremental compilation to avoid non-deterministic artifacts +CARGO_INCREMENTAL_VAL = 0 +# Set C locale for consistent string handling and sorting +LOCALE_VAL = C +# Set UTC timezone for consistent time handling across builds +TZ_VAL = UTC + +# Features for reproducible builds +FEATURES_REPRODUCIBLE = $(CROSS_FEATURES),jemalloc-unprefixed + +# Derive the architecture-specific library path from RUST_TARGET +JEMALLOC_LIB_ARCH = $(word 1,$(subst -, ,$(RUST_TARGET))) +JEMALLOC_OVERRIDE = /usr/lib/$(JEMALLOC_LIB_ARCH)-linux-gnu/libjemalloc.a + +# Default target architecture +RUST_TARGET ?= x86_64-unknown-linux-gnu + +# Default images for different architectures +RUST_IMAGE_AMD64 ?= rust:1.88-bullseye@sha256:8e3c421122bf4cd3b2a866af41a4dd52d87ad9e315fd2cb5100e87a7187a9816 +RUST_IMAGE_ARM64 ?= rust:1.88-bullseye@sha256:8b22455a7ce2adb1355067638284ee99d21cc516fab63a96c4514beaf370aa94 + +.PHONY: build-reproducible +build-reproducible: ## Build the lighthouse binary into `target` directory with reproducible builds + SOURCE_DATE_EPOCH=$(SOURCE_DATE) \ + RUSTFLAGS="${RUST_BUILD_FLAGS} --remap-path-prefix $$(pwd)=." \ + CARGO_INCREMENTAL=${CARGO_INCREMENTAL_VAL} \ + LC_ALL=${LOCALE_VAL} \ + TZ=${TZ_VAL} \ + JEMALLOC_OVERRIDE=${JEMALLOC_OVERRIDE} \ + cargo build --bin lighthouse --features "$(FEATURES_REPRODUCIBLE)" --profile "$(PROFILE)" --locked --target $(RUST_TARGET) + +.PHONY: build-reproducible-x86_64 +build-reproducible-x86_64: ## Build reproducible x86_64 Docker image DOCKER_BUILDKIT=1 docker build \ --build-arg RUST_TARGET="x86_64-unknown-linux-gnu" \ --build-arg RUST_IMAGE=$(RUST_IMAGE_AMD64) \ - --build-arg SOURCE_DATE=$(SOURCE_DATE) \ -f Dockerfile.reproducible \ -t lighthouse:reproducible-amd64 . -# Default image for arm64 -RUST_IMAGE_ARM64 ?= rust:1.82-bullseye@sha256:3c1b8b6487513ad4e753d008b960260f5bcc81bf110883460f6ed3cd72bf439b - -# Reproducible build for aarch64 -build-reproducible-aarch64: +.PHONY: build-reproducible-aarch64 +build-reproducible-aarch64: ## Build reproducible aarch64 Docker image DOCKER_BUILDKIT=1 docker build \ --platform linux/arm64 \ --build-arg RUST_TARGET="aarch64-unknown-linux-gnu" \ --build-arg RUST_IMAGE=$(RUST_IMAGE_ARM64) \ - --build-arg SOURCE_DATE=$(SOURCE_DATE) \ -f Dockerfile.reproducible \ -t lighthouse:reproducible-arm64 . -# Build both architectures -build-reproducible-all: build-reproducible-x86_64 build-reproducible-aarch64 +.PHONY: build-reproducible-all +build-reproducible-all: build-reproducible-x86_64 build-reproducible-aarch64 ## Build both x86_64 and aarch64 reproducible Docker images # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary @@ -136,29 +171,20 @@ build-release-tarballs: $(call tarball_release_binary,$(BUILD_PATH_RISCV64),$(RISCV64_TAG),"") + # Runs the full workspace tests in **release**, without downloading any additional # test vectors. test-release: - cargo test --workspace --release --features "$(TEST_FEATURES)" \ - --exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network - -# Runs the full workspace tests in **release**, without downloading any additional -# test vectors, using nextest. -nextest-release: cargo nextest run --workspace --release --features "$(TEST_FEATURES)" \ - --exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network + --exclude ef_tests --exclude beacon_chain --exclude slasher --exclude network \ + --exclude http_api + # Runs the full workspace tests in **debug**, without downloading any additional test # vectors. test-debug: - cargo test --workspace --features "$(TEST_FEATURES)" \ - --exclude ef_tests --exclude beacon_chain --exclude network - -# Runs the full workspace tests in **debug**, without downloading any additional test -# vectors, using nextest. -nextest-debug: cargo nextest run --workspace --features "$(TEST_FEATURES)" \ - --exclude ef_tests --exclude beacon_chain --exclude network + --exclude ef_tests --exclude beacon_chain --exclude network --exclude http_api # Runs cargo-fmt (linter). cargo-fmt: @@ -168,28 +194,30 @@ cargo-fmt: check-benches: cargo check --workspace --benches --features "$(TEST_FEATURES)" -# Runs only the ef-test vectors. -run-ef-tests: - rm -rf $(EF_TESTS)/.accessed_file_log.txt - cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" - cargo test --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" - ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests -# Runs EF test vectors with nextest -nextest-run-ef-tests: +# Runs EF test vectors +run-ef-tests: rm -rf $(EF_TESTS)/.accessed_file_log.txt cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES)" cargo nextest run --release -p ef_tests --features "ef_tests,$(EF_TEST_FEATURES),fake_crypto" ./$(EF_TESTS)/check_all_files_accessed.py $(EF_TESTS)/.accessed_file_log.txt $(EF_TESTS)/consensus-spec-tests # Run the tests in the `beacon_chain` crate for all known forks. -test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(FORKS)) +# TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead +test-beacon-chain: $(patsubst %,test-beacon-chain-%,$(RECENT_FORKS_BEFORE_GLOAS)) test-beacon-chain-%: env FORK_NAME=$* cargo nextest run --release --features "fork_from_env,slasher/lmdb,$(TEST_FEATURES)" -p beacon_chain +# Run the tests in the `http_api` crate for recent forks. +test-http-api: $(patsubst %,test-http-api-%,$(RECENT_FORKS_BEFORE_GLOAS)) + +test-http-api-%: + env FORK_NAME=$* cargo nextest run --release --features "beacon_chain/fork_from_env" -p http_api + + # Run the tests in the `operation_pool` crate for all known forks. -test-op-pool: $(patsubst %,test-op-pool-%,$(FORKS)) +test-op-pool: $(patsubst %,test-op-pool-%,$(RECENT_FORKS_BEFORE_GLOAS)) test-op-pool-%: env FORK_NAME=$* cargo nextest run --release \ @@ -197,7 +225,8 @@ test-op-pool-%: -p operation_pool # Run the tests in the `network` crate for all known forks. -test-network: $(patsubst %,test-network-%,$(FORKS)) +# TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead +test-network: $(patsubst %,test-network-%,$(RECENT_FORKS_BEFORE_GLOAS)) test-network-%: env FORK_NAME=$* cargo nextest run --release \ @@ -218,8 +247,8 @@ run-state-transition-tests: # Downloads and runs the EF test vectors. test-ef: make-ef-tests run-ef-tests -# Downloads and runs the EF test vectors with nextest. -nextest-ef: make-ef-tests nextest-run-ef-tests +# Downloads and runs the nightly EF test vectors. +test-ef-nightly: make-ef-tests-nightly run-ef-tests # Runs tests checking interop between Lighthouse and execution clients. test-exec-engine: @@ -254,6 +283,7 @@ lint: -D clippy::fn_to_numeric_cast_any \ -D clippy::manual_let_else \ -D clippy::large_stack_frames \ + -D clippy::disallowed_methods \ -D warnings \ -A clippy::derive_partial_eq_without_eq \ -A clippy::upper-case-acronyms \ @@ -264,7 +294,7 @@ lint: # Lints the code using Clippy and automatically fix some simple compiler warnings. lint-fix: - EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint + EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint-full # Also run the lints on the optimized-only tests lint-full: @@ -278,6 +308,10 @@ lint-full: make-ef-tests: make -C $(EF_TESTS) +# Download/extract the nightly EF test vectors. +make-ef-tests-nightly: + CONSENSUS_SPECS_TEST_VERSION=nightly make -C $(EF_TESTS) + # Verifies that crates compile with fuzzing features enabled arbitrary-fuzz: cargo check -p state_processing --features arbitrary-fuzz,$(TEST_FEATURES) @@ -292,6 +326,15 @@ install-audit: audit-CI: cargo audit +# Runs cargo deny (check for banned crates, duplicate versions, and source restrictions) +deny: install-deny deny-CI + +install-deny: + cargo install --force cargo-deny --version 0.18.2 + +deny-CI: + cargo deny check bans sources + # Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose. vendor: cargo vendor diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 071e2681dd..8dd50cbc6e 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "account_manager" -version = "0.3.5" +version = { workspace = true } authors = [ "Paul Hauner ", "Luke Anderson ", diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 3db8c3f152..427ca9fa13 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,15 +1,15 @@ use crate::common::read_wallet_name_from_cli; use crate::{SECRETS_DIR_FLAG, WALLETS_DIR_FLAG}; use account_utils::{ - random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText, - STDIN_INPUTS_FLAG, + PlainText, STDIN_INPUTS_FLAG, random_password, read_password_from_user, strip_off_newlines, + validator_definitions, }; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; +use directory::{DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR, parse_path_or_default_with_flag}; use environment::Environment; use eth2_wallet_manager::WalletManager; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use std::ffi::OsStr; use std::fs; use std::fs::create_dir_all; @@ -148,7 +148,9 @@ pub fn cli_run( return Err(format!( "No wallet directory at {:?}. Use the `lighthouse --network {} {} {} {}` command to create a wallet", wallet_base_dir, - matches.get_one::("network").unwrap_or(&String::from("")), + matches + .get_one::("network") + .unwrap_or(&String::from("")), crate::CMD, crate::wallet::CMD, crate::wallet::create::CMD diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 1393d0f152..5ea77f284e 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -4,8 +4,8 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; use environment::Environment; use eth2::{ - types::{GenesisData, StateId, ValidatorData, ValidatorId, ValidatorStatus}, BeaconNodeHttpClient, Timeouts, + types::{GenesisData, StateId, ValidatorData, ValidatorId, ValidatorStatus}, }; use eth2_keystore::Keystore; use eth2_network_config::Eth2NetworkConfig; @@ -239,9 +239,11 @@ async fn publish_voluntary_exit( let withdrawal_epoch = validator_data.validator.withdrawable_epoch; let current_epoch = get_current_epoch::(genesis_data.genesis_time, spec) .ok_or("Failed to get current epoch. Please check your system time")?; - eprintln!("Voluntary exit has been accepted into the beacon chain, but not yet finalized. \ + eprintln!( + "Voluntary exit has been accepted into the beacon chain, but not yet finalized. \ Finalization may take several minutes or longer. Before finalization there is a low \ - probability that the exit may be reverted."); + probability that the exit may be reverted." + ); eprintln!( "Current epoch: {}, Exit epoch: {}, Withdrawable epoch: {}", current_epoch, exit_epoch, withdrawal_epoch @@ -401,7 +403,7 @@ mod tests { use eth2_keystore::KeystoreBuilder; use std::fs::File; use std::io::Write; - use tempfile::{tempdir, TempDir}; + use tempfile::{TempDir, tempdir}; const PASSWORD: &str = "cats"; const KEYSTORE_NAME: &str = "keystore-m_12381_3600_0_0_0-1595406747.json"; diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 4d2353b553..6afdd81b71 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -1,17 +1,17 @@ use crate::wallet::create::PASSWORD_FLAG; use account_utils::validator_definitions::SigningDefinition; use account_utils::{ + STDIN_INPUTS_FLAG, eth2_keystore::Keystore, read_password_from_user, validator_definitions::{ - recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition, - ValidatorDefinitions, CONFIG_FILENAME, + CONFIG_FILENAME, PasswordStorage, ValidatorDefinition, ValidatorDefinitions, + recursively_find_voting_keystores, }, - STDIN_INPUTS_FLAG, }; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use std::fs; use std::path::PathBuf; use std::thread::sleep; @@ -32,7 +32,7 @@ pub fn cli_app() -> Command { .about( "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ requesting passwords interactively. The directory flag provides a convenient \ - method for importing a directory of keys generated by the eth2-deposit-cli \ + method for importing a directory of keys generated by the ethstaker-deposit-cli \ Python utility.", ) .arg( @@ -133,7 +133,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin return Err(format!( "Must supply either --{} or --{}", KEYSTORE_FLAG, DIR_FLAG - )) + )); } }; @@ -227,19 +227,20 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin if let Some(ValidatorDefinition { signing_definition: SigningDefinition::LocalKeystore { - voting_keystore_password: ref mut old_passwd, + voting_keystore_password: old_passwd, .. }, .. }) = old_validator_def_opt + && old_passwd.is_none() + && password_opt.is_some() { - if old_passwd.is_none() && password_opt.is_some() { - *old_passwd = password_opt; - defs.save(&validator_dir) - .map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?; - eprintln!("Password updated for public key {}", voting_pubkey); - } + *old_passwd = password_opt; + defs.save(&validator_dir) + .map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?; + eprintln!("Password updated for public key {}", voting_pubkey); } + eprintln!( "Skipping import of keystore for existing public key: {:?}", src_keystore diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index b699301cde..5a6c9439a6 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -8,7 +8,7 @@ pub mod slashing_protection; use crate::{VALIDATOR_DIR_FLAG, VALIDATOR_DIR_FLAG_ALIAS}; use clap::{Arg, ArgAction, ArgMatches, Command}; -use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR}; +use directory::{DEFAULT_VALIDATOR_DIR, parse_path_or_default_with_flag}; use environment::Environment; use std::path::PathBuf; use types::EthSpec; diff --git a/account_manager/src/validator/modify.rs b/account_manager/src/validator/modify.rs index 571cd28bf5..36f6b53d85 100644 --- a/account_manager/src/validator/modify.rs +++ b/account_manager/src/validator/modify.rs @@ -69,7 +69,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin return Err(format!( "{} does not have a {} command. See --help", CMD, unknown - )) + )); } _ => return Err(format!("No command provided for {}. See --help", CMD)), }; diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index 19d161a468..a61d19d7b6 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,13 +1,13 @@ use super::create::STORE_WITHDRAW_FLAG; -use crate::validator::create::COUNT_FLAG; use crate::SECRETS_DIR_FLAG; -use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; -use account_utils::{random_password, read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; +use crate::validator::create::COUNT_FLAG; +use account_utils::eth2_keystore::{Keystore, KeystoreBuilder, keypair_from_secret}; +use account_utils::{STDIN_INPUTS_FLAG, random_password, read_mnemonic_from_cli}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; +use directory::{DEFAULT_SECRET_DIR, parse_path_or_default_with_flag}; use eth2_wallet::bip39::Seed; -use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; +use eth2_wallet::{KeyType, ValidatorKeystores, recover_validator_secret_from_mnemonic}; use std::fs::create_dir_all; use std::path::PathBuf; use validator_dir::Builder as ValidatorDirBuilder; @@ -97,7 +97,9 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin .map_err(|e| format!("Could not create secrets dir at {secrets_dir:?}: {e:?}"))?; eprintln!(); - eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."); + eprintln!( + "WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING." + ); eprintln!(); let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index bcd860a484..96098ccbbd 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -1,13 +1,14 @@ +use bls::PublicKeyBytes; use clap::{Arg, ArgAction, ArgMatches, Command}; use environment::Environment; use slashing_protection::{ - interchange::Interchange, InterchangeError, InterchangeImportOutcome, SlashingDatabase, - SLASHING_PROTECTION_FILENAME, + InterchangeError, InterchangeImportOutcome, SLASHING_PROTECTION_FILENAME, SlashingDatabase, + interchange::Interchange, }; use std::fs::File; use std::path::PathBuf; use std::str::FromStr; -use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; +use types::{Epoch, EthSpec, Slot}; pub const CMD: &str = "slashing-protection"; pub const IMPORT_CMD: &str = "import"; @@ -90,7 +91,7 @@ pub fn cli_run( let slashing_protection_database = SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| { format!( - "Unable to open database at {}: {:?}", + "Unable to open slashing protection database at {}: {:?}", slashing_protection_db_path.display(), e ) @@ -198,7 +199,7 @@ pub fn cli_run( let slashing_protection_database = SlashingDatabase::open(&slashing_protection_db_path) .map_err(|e| { format!( - "Unable to open database at {}: {:?}", + "Unable to open slashing protection database at {}: {:?}", slashing_protection_db_path.display(), e ) diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index 6369646929..052e4bf217 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,13 +1,13 @@ -use crate::common::read_wallet_name_from_cli; use crate::WALLETS_DIR_FLAG; +use crate::common::read_wallet_name_from_cli; use account_utils::{ - is_password_sufficiently_complex, random_password, read_password_from_user, strip_off_newlines, - STDIN_INPUTS_FLAG, + STDIN_INPUTS_FLAG, is_password_sufficiently_complex, random_password, read_password_from_user, + strip_off_newlines, }; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2_wallet::{ - bip39::{Language, Mnemonic, MnemonicType}, PlainText, + bip39::{Language, Mnemonic, MnemonicType}, }; use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; use filesystem::create_with_600_perms; diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index f6f3bb0419..5f8d3948a0 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -4,7 +4,7 @@ pub mod recover; use crate::WALLETS_DIR_FLAG; use clap::{Arg, ArgAction, ArgMatches, Command}; -use directory::{parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use directory::{DEFAULT_WALLET_DIR, parse_path_or_default_with_flag}; use std::fs::create_dir_all; use std::path::PathBuf; diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index 766d5dbe0c..6d3b635090 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,6 +1,6 @@ use crate::wallet::create::create_wallet_from_mnemonic; use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; -use account_utils::{read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; +use account_utils::{STDIN_INPUTS_FLAG, read_mnemonic_from_cli}; use clap::{Arg, ArgAction, ArgMatches, Command}; use std::path::PathBuf; @@ -63,7 +63,9 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); eprintln!(); - eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."); + eprintln!( + "WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING." + ); eprintln!(); let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 30d6846964..5352814dd5 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "7.1.0-beta.0" +version = { workspace = true } authors = [ "Paul Hauner ", "Age Manning ", "Age Manning ( num_of_blobs: usize, spec: &ChainSpec, ) -> (SignedBeaconBlock, BlobsList, KzgProofs) { - let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); + let mut block = BeaconBlock::Fulu(BeaconBlockFulu::empty(spec)); let mut body = block.body_mut(); let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); *blob_kzg_commitments = @@ -26,8 +26,11 @@ fn create_test_block_and_blobs( let blobs = (0..num_of_blobs) .map(|_| Blob::::default()) .collect::>() - .into(); - let proofs = vec![KzgProof::empty(); num_of_blobs * spec.number_of_columns as usize].into(); + .try_into() + .unwrap(); + let proofs = vec![KzgProof::empty(); num_of_blobs * E::number_of_columns()] + .try_into() + .unwrap(); (signed_block, blobs, proofs) } @@ -55,7 +58,7 @@ fn all_benches(c: &mut Criterion) { b.iter(|| { black_box(reconstruct_data_columns( &kzg, - &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2].to_vec(), spec.as_ref(), )) }) diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 7d88268cf9..554cd431b3 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -9,13 +9,13 @@ use state_processing::per_epoch_processing::altair::{ process_inactivity_updates_slow, process_justification_and_finalization, }; use state_processing::per_epoch_processing::base::rewards_and_penalties::{ - get_attestation_component_delta, get_attestation_deltas_all, get_attestation_deltas_subset, - get_inactivity_penalty_delta, get_inclusion_delay_delta, ProposerRewardCalculation, + ProposerRewardCalculation, get_attestation_component_delta, get_attestation_deltas_all, + get_attestation_deltas_subset, get_inactivity_penalty_delta, get_inclusion_delay_delta, }; use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo; use state_processing::per_epoch_processing::base::{ - process_justification_and_finalization as process_justification_and_finalization_base, TotalBalances, ValidatorStatus, ValidatorStatuses, + process_justification_and_finalization as process_justification_and_finalization_base, }; use state_processing::{ common::altair::BaseRewardPerIncrement, diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index d69667f3de..faa396966f 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -35,10 +35,10 @@ mod batch; use crate::{ - metrics, + BeaconChain, BeaconChainError, BeaconChainTypes, metrics, observed_aggregates::{ObserveOutcome, ObservedAttestationKey}, observed_attesters::Error as ObservedAttestersError, - BeaconChain, BeaconChainError, BeaconChainTypes, + single_attestation::single_attestation_to_attestation, }; use bls::verify_signature_sets; use itertools::Itertools; @@ -57,7 +57,7 @@ use state_processing::{ }; use std::borrow::Cow; use strum::AsRefStr; -use tracing::debug; +use tracing::{debug, error}; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, @@ -202,12 +202,6 @@ pub enum Error { /// /// The peer has sent an invalid message. NoCommitteeForSlotAndIndex { slot: Slot, index: CommitteeIndex }, - /// The unaggregated attestation doesn't have only one aggregation bit set. - /// - /// ## Peer scoring - /// - /// The peer has sent an invalid message. - NotExactlyOneAggregationBitSet(usize), /// The attestation doesn't have only one aggregation bit set. /// /// ## Peer scoring @@ -273,6 +267,14 @@ pub enum Error { /// We were unable to process this attestation due to an internal error. It's unclear if the /// attestation is valid. BeaconChainError(Box), + /// A critical error occurred while converting SSZ types. + /// This can only occur when a VariableList was not able to be constructed from a single + /// attestation. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + SszTypesError(ssz_types::Error), } impl From for Error { @@ -281,6 +283,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz_types::Error) -> Self { + Self::SszTypesError(e) + } +} + /// Used to avoid double-checking signatures. #[derive(Copy, Clone)] enum CheckAttestationSignature { @@ -304,9 +312,9 @@ struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { /// /// These attestations have *not* undergone signature verification. struct IndexedUnaggregatedAttestation<'a, T: BeaconChainTypes> { - attestation: AttestationRef<'a, T::EthSpec>, + attestation: &'a SingleAttestation, indexed_attestation: IndexedAttestation, - subnet_id: SubnetId, + subnet_id: Option, validator_index: u64, } @@ -323,12 +331,13 @@ impl VerifiedAggregatedAttestation<'_, T> { } } +#[derive(Clone)] /// Wraps an `Attestation` that has been fully verified for propagation on the gossip network. pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> { - attestation: AttestationRef<'a, T::EthSpec>, + attestation: Attestation, + single_attestation: &'a SingleAttestation, indexed_attestation: IndexedAttestation, subnet_id: SubnetId, - validator_index: usize, } impl VerifiedUnaggregatedAttestation<'_, T> { @@ -336,13 +345,8 @@ impl VerifiedUnaggregatedAttestation<'_, T> { self.indexed_attestation } - pub fn single_attestation(&self) -> Option { - Some(SingleAttestation { - committee_index: self.attestation.committee_index()?, - attester_index: self.validator_index as u64, - data: self.attestation.data().clone(), - signature: self.attestation.signature().clone(), - }) + pub fn single_attestation(&self) -> SingleAttestation { + self.single_attestation.clone() } } @@ -362,7 +366,7 @@ impl Clone for IndexedUnaggregatedAttestation<'_, T> { /// A helper trait implemented on wrapper types that can be progressed to a state where they can be /// verified for application to fork choice. pub trait VerifiedAttestation: Sized { - fn attestation(&self) -> AttestationRef; + fn attestation(&self) -> AttestationRef<'_, T::EthSpec>; fn indexed_attestation(&self) -> &IndexedAttestation; @@ -375,7 +379,7 @@ pub trait VerifiedAttestation: Sized { } impl VerifiedAttestation for VerifiedAggregatedAttestation<'_, T> { - fn attestation(&self) -> AttestationRef { + fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.attestation() } @@ -385,8 +389,8 @@ impl VerifiedAttestation for VerifiedAggregatedAttestati } impl VerifiedAttestation for VerifiedUnaggregatedAttestation<'_, T> { - fn attestation(&self) -> AttestationRef { - self.attestation + fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { + self.attestation.to_ref() } fn indexed_attestation(&self) -> &IndexedAttestation { @@ -400,6 +404,8 @@ pub enum AttestationSlashInfo<'a, T: BeaconChainTypes, TErr> { SignatureNotChecked(AttestationRef<'a, T::EthSpec>, TErr), /// As for `SignatureNotChecked`, but we know the `IndexedAttestation`. SignatureNotCheckedIndexed(IndexedAttestation, TErr), + /// As for `SignatureNotChecked`, but for the `SingleAttestation`. + SignatureNotCheckedSingle(&'a SingleAttestation, TErr), /// The attestation's signature is invalid, so it will never be slashable. SignatureInvalid(TErr), /// The signature is valid but the attestation is invalid in some other way. @@ -421,11 +427,12 @@ fn process_slash_info( if let Some(slasher) = chain.slasher.as_ref() { let (indexed_attestation, check_signature, err) = match slash_info { SignatureNotChecked(attestation, err) => { - if let Error::UnknownHeadBlock { .. } = err { - if attestation.data().beacon_block_root == attestation.data().target.root { - return err; - } + if let Error::UnknownHeadBlock { .. } = err + && attestation.data().beacon_block_root == attestation.data().target.root + { + return err; } + match obtain_indexed_attestation_and_committees_per_slot(chain, attestation) { Ok((indexed, _)) => (indexed, true, err), Err(e) => { @@ -438,19 +445,43 @@ fn process_slash_info( } } } + SignatureNotCheckedSingle(attestation, err) => { + if let Error::UnknownHeadBlock { .. } = err + && attestation.data.beacon_block_root == attestation.data.target.root + { + return err; + } + + let fork_name = chain + .spec + .fork_name_at_slot::(attestation.data.slot); + + let indexed_attestation = match attestation.to_indexed(fork_name) { + Ok(indexed) => indexed, + Err(e) => { + error!( + attestation_root = ?attestation.data.tree_hash_root(), + error = ?e, + "Unable to construct VariableList from a single attestation. \ + This indicates a serious bug in SSZ handling" + ); + return Error::SszTypesError(e); + } + }; + (indexed_attestation, true, err) + } SignatureNotCheckedIndexed(indexed, err) => (indexed, true, err), SignatureInvalid(e) => return e, SignatureValid(indexed, err) => (indexed, false, err), }; - if check_signature { - if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) { - debug!( - error = ?e, - "Signature verification for slasher failed" - ); - return err; - } + if check_signature && let Err(e) = verify_attestation_signature(chain, &indexed_attestation) + { + debug!( + error = ?e, + "Signature verification for slasher failed" + ); + return err; } // Supply to slasher. @@ -461,6 +492,7 @@ fn process_slash_info( match slash_info { SignatureNotChecked(_, e) | SignatureNotCheckedIndexed(_, e) + | SignatureNotCheckedSingle(_, e) | SignatureInvalid(e) | SignatureValid(_, e) => e, } @@ -561,7 +593,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. - let head_block = verify_head_block_is_known(chain, attestation, None)?; + let head_block = verify_head_block_is_known(chain, attestation.data(), None)?; // Check the attestation target root is consistent with the head root. // @@ -570,7 +602,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // // Whilst this attestation *technically* could be used to add value to a block, it is // invalid in the spirit of the protocol. Here we choose safety over profit. - verify_attestation_target_root::(&head_block, attestation)?; + verify_attestation_target_root::(&head_block, attestation.data())?; // Ensure that the attestation has participants. if attestation.is_aggregation_bits_zero() { @@ -593,7 +625,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { return Err(SignatureNotChecked( signed_aggregate.message().aggregate(), e, - )) + )); } }; @@ -669,7 +701,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { return Err(SignatureNotChecked( signed_aggregate.message().aggregate(), e, - )) + )); } }; Ok(IndexedAggregatedAttestation { @@ -813,16 +845,16 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Run the checks that happen before an indexed attestation is constructed. pub fn verify_early_checks( - attestation: AttestationRef, + attestation: &'a SingleAttestation, chain: &BeaconChain, ) -> Result<(), Error> { - let attestation_epoch = attestation.data().slot.epoch(T::EthSpec::slots_per_epoch()); + let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); // Check the attestation's epoch matches its target. - if attestation_epoch != attestation.data().target.epoch { + if attestation_epoch != attestation.data.target.epoch { return Err(Error::InvalidTargetEpoch { - slot: attestation.data().slot, - epoch: attestation.data().target.epoch, + slot: attestation.data.slot, + epoch: attestation.data.target.epoch, }); } @@ -832,61 +864,44 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { // We do not queue future attestations for later processing. verify_propagation_slot_range::<_, T::EthSpec>( &chain.slot_clock, - attestation.data(), + &attestation.data, &chain.spec, )?; - // Check to ensure that the attestation is "unaggregated". I.e., it has exactly one - // aggregation bit set. - let num_aggregation_bits = attestation.num_set_aggregation_bits(); - if num_aggregation_bits != 1 { - return Err(Error::NotExactlyOneAggregationBitSet(num_aggregation_bits)); + let fork_name = chain + .spec + .fork_name_at_slot::(attestation.data.slot); + if fork_name.electra_enabled() { + // [New in Electra:EIP7549] + if attestation.data.index != 0 { + return Err(Error::CommitteeIndexNonZero( + attestation.data.index as usize, + )); + } } - // [New in Electra:EIP7549] - verify_committee_index(attestation)?; - // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. // // Enforce a maximum skip distance for unaggregated attestations. - let head_block = - verify_head_block_is_known(chain, attestation, chain.config.import_max_skip_slots)?; + let head_block = verify_head_block_is_known( + chain, + &attestation.data, + chain.config.import_max_skip_slots, + )?; // Check the attestation target root is consistent with the head root. - verify_attestation_target_root::(&head_block, attestation)?; + verify_attestation_target_root::(&head_block, &attestation.data)?; Ok(()) } /// Run the checks that apply to the indexed attestation before the signature is checked. pub fn verify_middle_checks( - attestation: AttestationRef, - indexed_attestation: &IndexedAttestation, - committees_per_slot: u64, - subnet_id: Option, + attestation: &'a SingleAttestation, chain: &BeaconChain, - ) -> Result<(u64, SubnetId), Error> { - let expected_subnet_id = SubnetId::compute_subnet_for_attestation::( - attestation, - committees_per_slot, - &chain.spec, - ) - .map_err(BeaconChainError::from)?; - - // If a subnet was specified, ensure that subnet is correct. - if let Some(subnet_id) = subnet_id { - if subnet_id != expected_subnet_id { - return Err(Error::InvalidSubnetId { - received: subnet_id, - expected: expected_subnet_id, - }); - } - }; - - let validator_index = *indexed_attestation - .attesting_indices_first() - .ok_or(Error::NotExactlyOneAggregationBitSet(0))?; + ) -> Result { + let validator_index = attestation.attester_index; /* * The attestation is the first valid attestation received for the participating validator @@ -895,16 +910,16 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { if chain .observed_gossip_attesters .read() - .validator_has_been_observed(attestation.data().target.epoch, validator_index as usize) + .validator_has_been_observed(attestation.data.target.epoch, validator_index as usize) .map_err(BeaconChainError::from)? { return Err(Error::PriorAttestationKnown { validator_index, - epoch: attestation.data().target.epoch, + epoch: attestation.data.target.epoch, }); } - Ok((validator_index, expected_subnet_id)) + Ok(validator_index) } /// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip @@ -913,11 +928,11 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// `subnet_id` is the subnet from which we received this attestation. This function will /// verify that it was received on the correct subnet. pub fn verify( - attestation: &'a Attestation, + attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result { - Self::verify_slashable(attestation.to_ref(), subnet_id, chain) + Self::verify_slashable(attestation, subnet_id, chain) .inspect(|verified_unaggregated| { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(verified_unaggregated.indexed_attestation.clone()); @@ -928,31 +943,25 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Verify the attestation, producing extra information about whether it might be slashable. pub fn verify_slashable( - attestation: AttestationRef<'a, T::EthSpec>, + attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result> { use AttestationSlashInfo::*; if let Err(e) = Self::verify_early_checks(attestation, chain) { - return Err(SignatureNotChecked(attestation, e)); + return Err(SignatureNotCheckedSingle(attestation, e)); } - let (indexed_attestation, committees_per_slot) = - match obtain_indexed_attestation_and_committees_per_slot(chain, attestation) { - Ok(x) => x, - Err(e) => { - return Err(SignatureNotChecked(attestation, e)); - } - }; + let fork_name = chain + .spec + .fork_name_at_slot::(attestation.data.slot); - let (validator_index, expected_subnet_id) = match Self::verify_middle_checks( - attestation, - &indexed_attestation, - committees_per_slot, - subnet_id, - chain, - ) { + let indexed_attestation = attestation + .to_indexed(fork_name) + .map_err(|e| SignatureNotCheckedSingle(attestation, Error::SszTypesError(e)))?; + + let validator_index = match Self::verify_middle_checks(attestation, chain) { Ok(t) => t, Err(e) => return Err(SignatureNotCheckedIndexed(indexed_attestation, e)), }; @@ -960,7 +969,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { Ok(Self { attestation, indexed_attestation, - subnet_id: expected_subnet_id, + subnet_id, validator_index, }) } @@ -977,10 +986,55 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// Run the checks that apply after the signature has been checked. fn verify_late_checks( - attestation: AttestationRef, + attestation: &'a SingleAttestation, validator_index: u64, + subnet_id: Option, chain: &BeaconChain, - ) -> Result<(), Error> { + ) -> Result<(Attestation, SubnetId), Error> { + // Check that the attester is a member of the committee + let (committee_opt, committees_per_slot) = chain.with_committee_cache( + attestation.data.target.root, + attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()), + |committee_cache, _| { + let committee_opt = committee_cache + .get_beacon_committee(attestation.data.slot, attestation.committee_index) + .map(|beacon_committee| beacon_committee.committee.to_vec()); + + Ok((committee_opt, committee_cache.committees_per_slot())) + }, + )?; + + let Some(committee) = committee_opt else { + return Err(Error::NoCommitteeForSlotAndIndex { + slot: attestation.data.slot, + index: attestation.committee_index, + }); + }; + + if !committee.contains(&(attestation.attester_index as usize)) { + return Err(Error::AttesterNotInCommittee { + attester_index: attestation.attester_index, + committee_index: attestation.committee_index, + slot: attestation.data.slot, + }); + } + + let expected_subnet_id = SubnetId::compute_subnet_for_single_attestation::( + attestation, + committees_per_slot, + &chain.spec, + ) + .map_err(BeaconChainError::from)?; + + // If a subnet was specified, ensure that subnet is correct. + if let Some(subnet_id) = subnet_id + && subnet_id != expected_subnet_id + { + return Err(Error::InvalidSubnetId { + received: subnet_id, + expected: expected_subnet_id, + }); + }; // Now that the attestation has been fully verified, store that we have received a valid // attestation from this validator. // @@ -990,20 +1044,28 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { if chain .observed_gossip_attesters .write() - .observe_validator(attestation.data().target.epoch, validator_index as usize) + .observe_validator(attestation.data.target.epoch, validator_index as usize) .map_err(BeaconChainError::from)? { return Err(Error::PriorAttestationKnown { validator_index, - epoch: attestation.data().target.epoch, + epoch: attestation.data.target.epoch, }); } - Ok(()) + + let fork_name = chain + .spec + .fork_name_at_slot::(attestation.data.slot); + + let unaggregated_attestation = + single_attestation_to_attestation(attestation, &committee, fork_name)?; + + Ok((unaggregated_attestation, expected_subnet_id)) } /// Verify the `unaggregated_attestation`. pub fn verify( - unaggregated_attestation: &'a Attestation, + unaggregated_attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result { @@ -1054,15 +1116,17 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { CheckAttestationSignature::No => (), }; - if let Err(e) = Self::verify_late_checks(attestation, validator_index, chain) { - return Err(SignatureValid(indexed_attestation, e)); - } + let (unaggregated_attestation, subnet_id) = + match Self::verify_late_checks(attestation, validator_index, subnet_id, chain) { + Ok(a) => a, + Err(e) => return Err(SignatureValid(indexed_attestation, e)), + }; Ok(Self { - attestation, + single_attestation: attestation, + attestation: unaggregated_attestation, indexed_attestation, subnet_id, - validator_index: validator_index as usize, }) } @@ -1071,11 +1135,6 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { self.subnet_id } - /// Returns the wrapped `attestation`. - pub fn attestation(&self) -> AttestationRef { - self.attestation - } - /// Returns the wrapped `indexed_attestation`. pub fn indexed_attestation(&self) -> &IndexedAttestation { &self.indexed_attestation @@ -1102,40 +1161,40 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// already finalized. fn verify_head_block_is_known( chain: &BeaconChain, - attestation: AttestationRef, + attestation_data: &AttestationData, max_skip_slots: Option, ) -> Result { let block_opt = chain .canonical_head .fork_choice_read_lock() - .get_block(&attestation.data().beacon_block_root) + .get_block(&attestation_data.beacon_block_root) .or_else(|| { chain .early_attester_cache - .get_proto_block(attestation.data().beacon_block_root) + .get_proto_block(attestation_data.beacon_block_root) }); if let Some(block) = block_opt { // Reject any block that exceeds our limit on skipped slots. - if let Some(max_skip_slots) = max_skip_slots { - if attestation.data().slot > block.slot + max_skip_slots { - return Err(Error::TooManySkippedSlots { - head_block_slot: block.slot, - attestation_slot: attestation.data().slot, - }); - } + if let Some(max_skip_slots) = max_skip_slots + && attestation_data.slot > block.slot + max_skip_slots + { + return Err(Error::TooManySkippedSlots { + head_block_slot: block.slot, + attestation_slot: attestation_data.slot, + }); } - if !verify_attestation_is_finalized_checkpoint_or_descendant(attestation.data(), chain) { + if !verify_attestation_is_finalized_checkpoint_or_descendant(attestation_data, chain) { return Err(Error::HeadBlockFinalized { - beacon_block_root: attestation.data().beacon_block_root, + beacon_block_root: attestation_data.beacon_block_root, }); } Ok(block) - } else if chain.is_pre_finalization_block(attestation.data().beacon_block_root)? { + } else if chain.is_pre_finalization_block(attestation_data.beacon_block_root)? { Err(Error::HeadBlockFinalized { - beacon_block_root: attestation.data().beacon_block_root, + beacon_block_root: attestation_data.beacon_block_root, }) } else { // The block is either: @@ -1145,7 +1204,7 @@ fn verify_head_block_is_known( // 2) A post-finalization block that we don't know about yet. We'll queue // the attestation until the block becomes available (or we time out). Err(Error::UnknownHeadBlock { - beacon_block_root: attestation.data().beacon_block_root, + beacon_block_root: attestation_data.beacon_block_root, }) } } @@ -1237,11 +1296,11 @@ pub fn verify_attestation_signature( /// `attestation.data.beacon_block_root`. pub fn verify_attestation_target_root( head_block: &ProtoBlock, - attestation: AttestationRef, + attestation_data: &AttestationData, ) -> Result<(), Error> { // Check the attestation target root. let head_block_epoch = head_block.slot.epoch(E::slots_per_epoch()); - let attestation_epoch = attestation.data().slot.epoch(E::slots_per_epoch()); + let attestation_epoch = attestation_data.slot.epoch(E::slots_per_epoch()); if head_block_epoch > attestation_epoch { // The epoch references an invalid head block from a future epoch. // @@ -1254,7 +1313,7 @@ pub fn verify_attestation_target_root( // Reference: // https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659 return Err(Error::InvalidTargetRoot { - attestation: attestation.data().target.root, + attestation: attestation_data.target.root, // It is not clear what root we should expect in this case, since the attestation is // fundamentally invalid. expected: None, @@ -1273,9 +1332,9 @@ pub fn verify_attestation_target_root( }; // Reject any attestation with an invalid target root. - if target_root != attestation.data().target.root { + if target_root != attestation_data.target.root { return Err(Error::InvalidTargetRoot { - attestation: attestation.data().target.root, + attestation: attestation_data.target.root, expected: Some(target_root), }); } @@ -1312,7 +1371,7 @@ pub fn verify_signed_aggregate_signatures( .spec .fork_at_epoch(indexed_attestation.data().target.epoch); - let signature_sets = vec![ + let signature_sets = [ signed_aggregate_selection_proof_signature_set( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), signed_aggregate, diff --git a/beacon_node/beacon_chain/src/attestation_verification/batch.rs b/beacon_node/beacon_chain/src/attestation_verification/batch.rs index 5f856140ba..c1087ef77e 100644 --- a/beacon_node/beacon_chain/src/attestation_verification/batch.rs +++ b/beacon_node/beacon_chain/src/attestation_verification/batch.rs @@ -13,7 +13,7 @@ use super::{ CheckAttestationSignature, Error, IndexedAggregatedAttestation, IndexedUnaggregatedAttestation, VerifiedAggregatedAttestation, VerifiedUnaggregatedAttestation, }; -use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use bls::verify_signature_sets; use state_processing::signature_sets::{ indexed_attestation_signature_set_from_pubkeys, signed_aggregate_selection_proof_signature_set, @@ -136,7 +136,7 @@ pub fn batch_verify_unaggregated_attestations<'a, T, I>( ) -> Result, Error>>, Error> where T: BeaconChainTypes, - I: Iterator, Option)> + ExactSizeIterator, + I: Iterator)> + ExactSizeIterator, { let mut num_partially_verified = 0; let mut num_failed = 0; diff --git a/beacon_node/beacon_chain/src/attester_cache.rs b/beacon_node/beacon_chain/src/attester_cache.rs index ae715afcd0..26a3389812 100644 --- a/beacon_node/beacon_chain/src/attester_cache.rs +++ b/beacon_node/beacon_chain/src/attester_cache.rs @@ -10,17 +10,18 @@ //! and penalties can be computed and the `state.current_justified_checkpoint` can be updated. use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use fixed_bytes::FixedBytesExtended; use parking_lot::RwLock; -use state_processing::state_advance::{partial_state_advance, Error as StateAdvanceError}; +use state_processing::state_advance::{Error as StateAdvanceError, partial_state_advance}; use std::collections::HashMap; use std::ops::Range; use types::{ - attestation::Error as AttestationError, + BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, RelativeEpoch, + Slot, + attestation::AttestationError, beacon_state::{ compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count, }, - BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, FixedBytesExtended, - Hash256, RelativeEpoch, Slot, }; type JustifiedCheckpoint = Checkpoint; @@ -365,11 +366,7 @@ impl AttesterCache { value: AttesterCacheValue, ) { while cache.len() >= MAX_CACHE_LEN { - if let Some(oldest) = cache - .iter() - .map(|(key, _)| *key) - .min_by_key(|key| key.epoch) - { + if let Some(oldest) = cache.keys().copied().min_by_key(|key| key.epoch) { cache.remove(&oldest); } else { break; diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index ecaa4f45e7..ac4ed2ab67 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -15,8 +15,8 @@ use state_processing::{ }; use std::collections::HashSet; use store::{ - consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, RelativeEpoch, + consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, }; use tracing::error; use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, EthSpec}; diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index 5339b12826..95144e65ec 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -1,14 +1,14 @@ -use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, BlockProcessStatus, metrics}; use execution_layer::{ExecutionLayer, ExecutionPayloadBodyV1}; use logging::crit; use std::collections::HashMap; use std::sync::Arc; use store::{DatabaseBlock, ExecutionPayloadDeneb}; use tokio::sync::{ - mpsc::{self, UnboundedSender}, RwLock, + mpsc::{self, UnboundedSender}, }; -use tokio_stream::{wrappers::UnboundedReceiverStream, Stream}; +use tokio_stream::{Stream, wrappers::UnboundedReceiverStream}; use tracing::{debug, error}; use types::{ ChainSpec, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, SignedBeaconBlock, @@ -16,7 +16,7 @@ use types::{ }; use types::{ ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadEip7805, - ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadHeader, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeader, }; #[derive(PartialEq)] @@ -102,12 +102,13 @@ fn reconstruct_default_header_block( ForkName::Electra => ExecutionPayloadElectra::default().into(), ForkName::Eip7805 => ExecutionPayloadEip7805::default().into(), ForkName::Fulu => ExecutionPayloadFulu::default().into(), + ForkName::Gloas => ExecutionPayloadGloas::default().into(), ForkName::Base | ForkName::Altair => { return Err(Error::PayloadReconstruction(format!( "Block with fork variant {} has execution payload", fork )) - .into()) + .into()); } }; @@ -404,7 +405,7 @@ impl BeaconBlockStreamer { if self.check_caches == CheckCaches::Yes { match self.beacon_chain.get_block_process_status(&root) { BlockProcessStatus::Unknown => None, - BlockProcessStatus::NotValidated(block) + BlockProcessStatus::NotValidated(block, _) | BlockProcessStatus::ExecutionValidated(block) => { metrics::inc_counter(&metrics::BEACON_REQRESP_PRE_IMPORT_CACHE_HITS); Some(block) @@ -684,14 +685,14 @@ impl From for BeaconChainError { #[cfg(test)] mod tests { use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; - use crate::test_utils::{test_spec, BeaconChainHarness, EphemeralHarnessType}; + use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}; + use bls::Keypair; use execution_layer::test_utils::Block; + use fixed_bytes::FixedBytesExtended; use std::sync::Arc; use std::sync::LazyLock; use tokio::sync::mpsc; - use types::{ - ChainSpec, Epoch, EthSpec, FixedBytesExtended, Hash256, Keypair, MinimalEthSpec, Slot, - }; + use types::{ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot}; const VALIDATOR_COUNT: usize = 48; @@ -715,6 +716,7 @@ mod tests { harness } + // TODO(EIP-7732) Extend this test for gloas #[tokio::test] async fn check_all_blocks_from_altair_to_fulu() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b99bdc1674..8494a54b44 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1,25 +1,27 @@ use crate::attestation_verification::{ - batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, Error as AttestationError, VerifiedAggregatedAttestation, VerifiedAttestation, - VerifiedUnaggregatedAttestation, + VerifiedUnaggregatedAttestation, batch_verify_aggregated_attestations, + batch_verify_unaggregated_attestations, }; use crate::attester_cache::{AttesterCache, AttesterCacheKey}; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; -use crate::beacon_proposer_cache::compute_proposer_duties_from_head; -use crate::beacon_proposer_cache::BeaconProposerCache; +use crate::beacon_proposer_cache::{ + BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch, +}; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::POS_PANDA_BANNER; use crate::block_verification::{ + BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, check_block_is_finalized_checkpoint_or_descendant, check_block_relevancy, - signature_verify_chain_segment, verify_header_signature, BlockError, ExecutionPendingBlock, - GossipVerifiedBlock, IntoExecutionPendingBlock, + signature_verify_chain_segment, verify_header_signature, }; use crate::block_verification_types::{ AsBlock, AvailableExecutedBlock, BlockImportData, ExecutedBlock, RpcBlock, }; pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; +use crate::custody_context::CustodyContextSsz; use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, DataAvailabilityChecker, DataColumnReconstructionResult, @@ -27,15 +29,12 @@ use crate::data_availability_checker::{ use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; -use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; -use crate::eth1_finalization_cache::{Eth1FinalizationCache, Eth1FinalizationData}; use crate::events::ServerSentEventHandler; -use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, PreparePayloadHandle}; +use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; -use crate::graffiti_calculator::GraffitiCalculator; -use crate::inclusion_list_verification::GossipInclusionListError; -use crate::inclusion_list_verification::GossipVerifiedInclusionList; +use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; +use crate::inclusion_list_verification::{GossipInclusionListError, GossipVerifiedInclusionList}; use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate, @@ -60,6 +59,7 @@ use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; use crate::persisted_beacon_chain::PersistedBeaconChain; +use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; @@ -67,28 +67,32 @@ use crate::sync_committee_verification::{ Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution, }; use crate::validator_monitor::{ - get_slot_delay_ms, timestamp_now, ValidatorMonitor, - HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS, + HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS, ValidatorMonitor, get_slot_delay_ms, + timestamp_now, }; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ - kzg_utils, metrics, AvailabilityPendingExecutedBlock, BeaconChainError, BeaconForkChoiceStore, - BeaconSnapshot, CachedHead, + AvailabilityPendingExecutedBlock, BeaconChainError, BeaconForkChoiceStore, BeaconSnapshot, + CachedHead, metrics, }; +use bls::{PublicKey, PublicKeyBytes, Signature}; +use eth2::beacon_response::ForkVersionedResponse; use eth2::types::{ - EventKind, SseBlobSidecar, SseBlock, SseExtendedPayloadAttributes, SseInclusionList, + EventKind, SseBlobSidecar, SseBlock, SseDataColumnSidecar, SseExtendedPayloadAttributes, + SseInclusionList, }; use execution_layer::{ BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; +use fixed_bytes::FixedBytesExtended; use fork_choice::{ AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, InvalidationOperation, PayloadVerificationStatus, ResetPayloadStatuses, }; use futures::channel::mpsc::Sender; -use itertools::process_results; use itertools::Itertools; +use itertools::process_results; use kzg::Kzg; use logging::crit; use operation_pool::{ @@ -102,16 +106,16 @@ use slasher::Slasher; use slot_clock::SlotClock; use ssz::Encode; use state_processing::{ + BlockSignatureStrategy, ConsensusContext, SigVerifiedOp, VerifyBlockRoot, VerifyOperation, common::get_attesting_indices_from_state, epoch_cache::initialize_epoch_cache, per_block_processing, per_block_processing::{ - errors::AttestationValidationError, get_expected_withdrawals, - verify_attestation_for_block_inclusion, VerifySignatures, + VerifySignatures, errors::AttestationValidationError, get_expected_withdrawals, + verify_attestation_for_block_inclusion, }, per_slot_processing, state_advance::{complete_state_advance, partial_state_advance}, - BlockSignatureStrategy, ConsensusContext, SigVerifiedOp, VerifyBlockRoot, VerifyOperation, }; use std::borrow::Cow; use std::cmp::Ordering; @@ -123,12 +127,12 @@ use std::sync::Arc; use std::time::Duration; use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator}; use store::{ - BlobSidecarListFromRoot, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary, + BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; -use task_executor::{ShutdownReason, TaskExecutor}; +use task_executor::{RayonPoolType, ShutdownReason, TaskExecutor}; use tokio_stream::Stream; -use tracing::{debug, error, info, trace, warn}; +use tracing::{Span, debug, debug_span, error, info, info_span, instrument, trace, warn}; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; use types::data_column_sidecar::ColumnIndex; @@ -143,7 +147,6 @@ type HashBlockTuple = (Hash256, RpcBlock); // These keys are all zero because they get stored in different columns, see `DBColumn` type. pub const BEACON_CHAIN_DB_KEY: Hash256 = Hash256::ZERO; pub const OP_POOL_DB_KEY: Hash256 = Hash256::ZERO; -pub const ETH1_CACHE_DB_KEY: Hash256 = Hash256::ZERO; pub const FORK_CHOICE_DB_KEY: Hash256 = Hash256::ZERO; /// Defines how old a block can be before it's no longer a candidate for the early attester cache. @@ -312,7 +315,6 @@ pub trait BeaconChainTypes: Send + Sync + 'static { type HotStore: store::ItemStore; type ColdStore: store::ItemStore; type SlotClock: slot_clock::SlotClock; - type Eth1Chain: Eth1ChainBackend; type EthSpec: types::EthSpec; } @@ -338,16 +340,12 @@ pub enum BlockProcessStatus { /// Block is not in any pre-import cache. Block may be in the data-base or in the fork-choice. Unknown, /// Block is currently processing but not yet validated. - NotValidated(Arc>), + NotValidated(Arc>, BlockImportSource), /// Block is fully valid, but not yet imported. It's cached in the da_checker while awaiting /// missing block components. ExecutionValidated(Arc>), } -pub struct BeaconChainMetrics { - pub reqresp_pre_import_cache_len: usize, -} - pub type LightClientProducerEvent = (Hash256, Slot, SyncAggregate); pub type BeaconForkChoice = ForkChoice< @@ -367,9 +365,6 @@ pub type BeaconStore = Arc< >, >; -/// Cache gossip verified blocks to serve over ReqResp before they are imported -type ReqRespPreImportCache = HashMap>>; - /// Represents the "Beacon Chain" component of Ethereum 2.0. Allows import of blocks and block /// operations and chooses a canonical head. pub struct BeaconChain { @@ -436,8 +431,6 @@ pub struct BeaconChain { /// Maintains a record of which validators we've seen BLS to execution changes for. pub observed_bls_to_execution_changes: Mutex>, - /// Provides information from the Ethereum 1 (PoW) chain. - pub eth1_chain: Option>, /// Interfaces with the execution client. pub execution_layer: Option>, /// Stores information about the canonical head and finalized/justified checkpoints of the @@ -460,8 +453,6 @@ pub struct BeaconChain { pub event_handler: Option>, /// Caches the attester shuffling for a given epoch and shuffling key root. pub shuffling_cache: RwLock, - /// A cache of eth1 deposit data at epoch boundaries for deposit finalization - pub eth1_finalization_cache: RwLock, /// Caches the beacon block proposer shuffling for a given epoch and shuffling key root. pub beacon_proposer_cache: Arc>, /// Caches a map of `validator_index -> validator_pubkey`. @@ -472,8 +463,6 @@ pub struct BeaconChain { pub early_attester_cache: EarlyAttesterCache, /// A cache used to store verified/equivocating inclusion lists. pub inclusion_list_cache: RwLock>, - /// Cache gossip verified blocks to serve over ReqResp before they are imported - pub reqresp_pre_import_cache: Arc>>, /// A cache used to keep track of various block timings. pub block_times_cache: Arc>, /// A cache used to track pre-finalization block roots for quick rejection. @@ -628,12 +617,15 @@ impl BeaconChain { reset_payload_statuses: ResetPayloadStatuses, spec: &ChainSpec, ) -> Result>, Error> { - let Some(persisted_fork_choice) = - store.get_item::(&FORK_CHOICE_DB_KEY)? + let Some(persisted_fork_choice_bytes) = store + .hot_db + .get_bytes(DBColumn::ForkChoice, FORK_CHOICE_DB_KEY.as_slice())? else { return Ok(None); }; + let persisted_fork_choice = + PersistedForkChoice::from_bytes(&persisted_fork_choice_bytes, store.get_config())?; let fc_store = BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, store)?; @@ -662,15 +654,24 @@ impl BeaconChain { Ok(()) } - /// Persists `self.eth1_chain` and its caches to disk. - pub fn persist_eth1_cache(&self) -> Result<(), Error> { - let _timer = metrics::start_timer(&metrics::PERSIST_ETH1_CACHE); - - if let Some(eth1_chain) = self.eth1_chain.as_ref() { - self.store - .put_item(Ð1_CACHE_DB_KEY, ð1_chain.as_ssz_container())?; + /// Persists the custody information to disk. + pub fn persist_custody_context(&self) -> Result<(), Error> { + if !self.spec.is_peer_das_scheduled() { + return Ok(()); } + let custody_context: CustodyContextSsz = self + .data_availability_checker + .custody_context() + .as_ref() + .into(); + debug!(?custody_context, "Persisting custody context to store"); + + persist_custody_context::( + self.store.clone(), + custody_context, + )?; + Ok(()) } @@ -889,6 +890,12 @@ impl BeaconChain { return Ok(None); } + // Fast-path for the split slot (which usually corresponds to the finalized slot). + let split = self.store.get_split_info(); + if request_slot == split.slot { + return Ok(Some(split.state_root)); + } + // Try an optimized path of reading the root directly from the head state. let fast_lookup: Option = self.with_head(|head| { if head.beacon_block.slot() <= request_slot { @@ -979,10 +986,10 @@ impl BeaconChain { Ordering::Greater => state.get_block_root(request_slot).ok().copied(), }; - if let Some(request_root) = request_root_opt { - if let Ok(prev_root) = state.get_block_root(prev_slot) { - return Ok(Some((*prev_root != request_root).then_some(request_root))); - } + if let Some(request_root) = request_root_opt + && let Ok(prev_root) = state.get_block_root(prev_slot) + { + return Ok(Some((*prev_root != request_root).then_some(request_root))); } // Fast lookup is not possible. @@ -992,6 +999,14 @@ impl BeaconChain { return Ok(root_opt); } + // Do not try to access the previous slot if it's older than the oldest block root + // stored in the database. Instead, load just the block root at `oldest_block_slot`, + // under the assumption that the `oldest_block_slot` *is not* a skipped slot (should be + // true because it is set by the oldest *block*). + if request_slot == self.store.get_anchor_info().oldest_block_slot { + return self.block_root_at_slot_skips_prev(request_slot); + } + if let Some(((prev_root, _), (curr_root, curr_slot))) = process_results( self.forwards_iter_block_roots_until(prev_slot, request_slot)?, |iter| iter.tuple_windows().next(), @@ -1237,10 +1252,10 @@ impl BeaconChain { if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { if let Some(columns) = self.store.get_data_columns(block_root)? { - let num_required_columns = self.spec.number_of_columns / 2; - let reconstruction_possible = columns.len() >= num_required_columns as usize; + let num_required_columns = T::EthSpec::number_of_columns() / 2; + let reconstruction_possible = columns.len() >= num_required_columns; if reconstruction_possible { - reconstruct_blobs(&self.kzg, &columns, None, &block, &self.spec) + reconstruct_blobs(&self.kzg, columns, None, &block, &self.spec) .map(Some) .map_err(Error::FailedToReconstructBlobs) } else { @@ -1279,18 +1294,8 @@ impl BeaconChain { /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. pub fn get_block_process_status(&self, block_root: &Hash256) -> BlockProcessStatus { - if let Some(block) = self - .data_availability_checker - .get_execution_valid_block(block_root) - { - return BlockProcessStatus::ExecutionValidated(block); - } - - if let Some(block) = self.reqresp_pre_import_cache.read().get(block_root) { - // A block is on the `reqresp_pre_import_cache` but NOT in the - // `data_availability_checker` only if it is actively processing. We can expect a future - // event with the result of processing - return BlockProcessStatus::NotValidated(block.clone()); + if let Some(cached_block) = self.data_availability_checker.get_cached_block(block_root) { + return cached_block; } BlockProcessStatus::Unknown @@ -1414,10 +1419,10 @@ impl BeaconChain { /// /// Returns `(block_root, block_slot)`. pub fn heads(&self) -> Vec<(Hash256, Slot)> { - self.canonical_head - .fork_choice_read_lock() + let fork_choice = self.canonical_head.fork_choice_read_lock(); + fork_choice .proto_array() - .heads_descended_from_finalization::() + .heads_descended_from_finalization::(fork_choice.finalized_checkpoint()) .iter() .map(|node| (node.root, node.slot)) .collect() @@ -1427,6 +1432,7 @@ impl BeaconChain { /// /// Returns `None` when the state is not found in the database or there is an error skipping /// to a future state. + #[instrument(level = "debug", skip_all)] pub fn state_at_slot( &self, slot: Slot, @@ -2035,7 +2041,7 @@ impl BeaconChain { return Err(Error::HeadBlockNotFullyVerified { beacon_block_root, execution_status, - }) + }); } None => return Err(Error::HeadMissingFromForkChoice(beacon_block_root)), }; @@ -2108,7 +2114,7 @@ impl BeaconChain { /// May return an error if the `request_slot` is too far behind the head state. pub async fn produce_inclusion_list( self: &Arc, - request_slot: Slot, + _request_slot: Slot, ) -> Result>, Error> { let execution_layer = self .execution_layer @@ -2203,7 +2209,7 @@ impl BeaconChain { AttestationError, > where - I: Iterator, Option)> + ExactSizeIterator, + I: Iterator)> + ExactSizeIterator, { batch_verify_unaggregated_attestations(attestations, self) } @@ -2215,7 +2221,7 @@ impl BeaconChain { /// aggregation bit set. pub fn verify_unaggregated_attestation_for_gossip<'a>( &self, - unaggregated_attestation: &'a Attestation, + unaggregated_attestation: &'a SingleAttestation, subnet_id: Option, ) -> Result, AttestationError> { metrics::inc_counter(&metrics::UNAGGREGATED_ATTESTATION_PROCESSING_REQUESTS); @@ -2231,13 +2237,9 @@ impl BeaconChain { .spec .fork_name_at_slot::(v.attestation().data().slot); if current_fork.electra_enabled() { - // I don't see a situation where this could return None. The upstream unaggregated attestation checks - // should have already verified that this is an attestation with a single committee bit set. - if let Some(single_attestation) = v.single_attestation() { - event_handler.register(EventKind::SingleAttestation(Box::new( - single_attestation, - ))); - } + event_handler.register(EventKind::SingleAttestation(Box::new( + v.single_attestation(), + ))); } } @@ -2282,12 +2284,12 @@ impl BeaconChain { VerifiedAggregatedAttestation::verify(signed_aggregate, self).inspect(|v| { // This method is called for API and gossip attestations, so this covers all aggregated attestation events - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_attestation_subscribers() { - event_handler.register(EventKind::Attestation(Box::new( - v.attestation().clone_as_attestation(), - ))); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_attestation_subscribers() + { + event_handler.register(EventKind::Attestation(Box::new( + v.attestation().clone_as_attestation(), + ))); } metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_PROCESSING_SUCCESSES); }) @@ -2317,12 +2319,12 @@ impl BeaconChain { metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::SYNC_CONTRIBUTION_GOSSIP_VERIFICATION_TIMES); VerifiedSyncContribution::verify(sync_contribution, self).inspect(|v| { - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_contribution_subscribers() { - event_handler.register(EventKind::ContributionAndProof(Box::new( - v.aggregate().clone(), - ))); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_contribution_subscribers() + { + event_handler.register(EventKind::ContributionAndProof(Box::new( + v.aggregate().clone(), + ))); } metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_PROCESSING_SUCCESSES); }) @@ -2344,10 +2346,11 @@ impl BeaconChain { }) } + #[instrument(skip_all, level = "trace")] pub fn verify_data_column_sidecar_for_gossip( self: &Arc, data_column_sidecar: Arc>, - subnet_id: u64, + subnet_id: DataColumnSubnetId, ) -> Result, GossipDataColumnError> { metrics::inc_counter(&metrics::DATA_COLUMN_SIDECAR_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES); @@ -2356,6 +2359,7 @@ impl BeaconChain { }) } + #[instrument(skip_all, level = "trace")] pub fn verify_blob_sidecar_for_gossip( self: &Arc, blob_sidecar: Arc>, @@ -2548,13 +2552,10 @@ impl BeaconChain { // If there's no eth1 chain then it's impossible to produce blocks and therefore // useless to put things in the op pool. - if self.eth1_chain.is_some() { - let (attestation, attesting_indices) = - verified_attestation.into_attestation_and_indices(); - self.op_pool - .insert_attestation(attestation, attesting_indices) - .map_err(Error::from)?; - } + let (attestation, attesting_indices) = verified_attestation.into_attestation_and_indices(); + self.op_pool + .insert_attestation(attestation, attesting_indices) + .map_err(Error::from)?; Ok(()) } @@ -2570,11 +2571,9 @@ impl BeaconChain { // If there's no eth1 chain then it's impossible to produce blocks and therefore // useless to put things in the op pool. - if self.eth1_chain.is_some() { - self.op_pool - .insert_sync_contribution(contribution.contribution()) - .map_err(Error::from)?; - } + self.op_pool + .insert_sync_contribution(contribution.contribution()) + .map_err(Error::from)?; Ok(()) } @@ -2698,21 +2697,18 @@ impl BeaconChain { .verify_and_observe_at(exit, wall_clock_epoch, head_state, &self.spec) .inspect(|exit| { // this method is called for both API and gossip exits, so this covers all exit events - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_exit_subscribers() { - if let ObservationOutcome::New(exit) = exit.clone() { - event_handler.register(EventKind::VoluntaryExit(exit.into_inner())); - } - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_exit_subscribers() + && let ObservationOutcome::New(exit) = exit.clone() + { + event_handler.register(EventKind::VoluntaryExit(exit.into_inner())); } })?) } /// Accept a pre-verified exit and queue it for inclusion in an appropriate block. pub fn import_voluntary_exit(&self, exit: SigVerifiedOp) { - if self.eth1_chain.is_some() { - self.op_pool.insert_voluntary_exit(exit) - } + self.op_pool.insert_voluntary_exit(exit) } /// Verify a proposer slashing before allowing it to propagate on the gossip network. @@ -2734,17 +2730,15 @@ impl BeaconChain { &self, proposer_slashing: SigVerifiedOp, ) { - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_proposer_slashing_subscribers() { - event_handler.register(EventKind::ProposerSlashing(Box::new( - proposer_slashing.clone().into_inner(), - ))); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_proposer_slashing_subscribers() + { + event_handler.register(EventKind::ProposerSlashing(Box::new( + proposer_slashing.clone().into_inner(), + ))); } - if self.eth1_chain.is_some() { - self.op_pool.insert_proposer_slashing(proposer_slashing) - } + self.op_pool.insert_proposer_slashing(proposer_slashing) } /// Verify an attester slashing before allowing it to propagate on the gossip network. @@ -2774,18 +2768,16 @@ impl BeaconChain { .fork_choice_write_lock() .on_attester_slashing(attester_slashing.as_inner().to_ref()); - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_attester_slashing_subscribers() { - event_handler.register(EventKind::AttesterSlashing(Box::new( - attester_slashing.clone().into_inner(), - ))); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_attester_slashing_subscribers() + { + event_handler.register(EventKind::AttesterSlashing(Box::new( + attester_slashing.clone().into_inner(), + ))); } // Add to the op pool (if we have the ability to propose blocks). - if self.eth1_chain.is_some() { - self.op_pool.insert_attester_slashing(attester_slashing) - } + self.op_pool.insert_attester_slashing(attester_slashing) } /// Verify a signed BLS to execution change before allowing it to propagate on the gossip network. @@ -2849,20 +2841,16 @@ impl BeaconChain { bls_to_execution_change: SigVerifiedOp, received_pre_capella: ReceivedPreCapella, ) -> bool { - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_bls_to_execution_change_subscribers() { - event_handler.register(EventKind::BlsToExecutionChange(Box::new( - bls_to_execution_change.clone().into_inner(), - ))); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_bls_to_execution_change_subscribers() + { + event_handler.register(EventKind::BlsToExecutionChange(Box::new( + bls_to_execution_change.clone().into_inner(), + ))); } - if self.eth1_chain.is_some() { - self.op_pool - .insert_bls_to_execution_change(bls_to_execution_change, received_pre_capella) - } else { - false - } + self.op_pool + .insert_bls_to_execution_change(bls_to_execution_change, received_pre_capella) } /// Attempt to obtain sync committee duties from the head. @@ -3028,8 +3016,12 @@ impl BeaconChain { // Filter uninteresting blocks from the chain segment in a blocking task. let chain = self.clone(); + let filter_chain_segment = debug_span!("filter_chain_segment"); let filtered_chain_segment_future = self.spawn_blocking_handle( - move || chain.filter_chain_segment(chain_segment), + move || { + let _guard = filter_chain_segment.enter(); + chain.filter_chain_segment(chain_segment) + }, "filter_chain_segment", ); let mut filtered_chain_segment = match filtered_chain_segment_future.await { @@ -3039,7 +3031,7 @@ impl BeaconChain { return ChainSegmentResult::Failed { imported_blocks, error: BlockError::BeaconChainError(error.into()), - } + }; } }; @@ -3060,8 +3052,12 @@ impl BeaconChain { std::mem::swap(&mut blocks, &mut filtered_chain_segment); let chain = self.clone(); + let current_span = Span::current(); let signature_verification_future = self.spawn_blocking_handle( - move || signature_verify_chain_segment(blocks, &chain), + move || { + let _guard = current_span.enter(); + signature_verify_chain_segment(blocks, &chain) + }, "signature_verify_chain_segment", ); @@ -3136,16 +3132,6 @@ impl BeaconChain { ChainSegmentResult::Successful { imported_blocks } } - /// Updates fork-choice node into a permanent `available` state so it can become a viable head. - /// Only completed sampling results are received. Blocks are unavailable by default and should - /// be pruned on finalization, on a timeout or by a max count. - pub async fn process_sampling_completed(self: &Arc, block_root: Hash256) { - // TODO(das): update fork-choice, act on sampling result, adjust log level - // NOTE: It is possible that sampling complets before block is imported into fork choice, - // in that case we may need to update availability cache. - info!(%block_root, "Sampling completed"); - } - /// Returns `Ok(GossipVerifiedBlock)` if the supplied `block` should be forwarded onto the /// gossip network. The block is not imported into the chain, it is just partially verified. /// @@ -3159,17 +3145,18 @@ impl BeaconChain { pub async fn verify_block_for_gossip( self: &Arc, block: Arc>, - custody_columns_count: usize, ) -> Result, BlockError> { let chain = self.clone(); + let span = Span::current(); self.task_executor .clone() .spawn_blocking_handle( move || { + let _guard = span.enter(); let slot = block.slot(); let graffiti_string = block.message().body().graffiti().as_utf8_lossy(); - match GossipVerifiedBlock::new(block, &chain, custody_columns_count) { + match GossipVerifiedBlock::new(block, &chain) { Ok(verified) => { let commitments_formatted = verified.block.commitments_formatted(); debug!( @@ -3194,7 +3181,7 @@ impl BeaconChain { } } }, - "payload_verification_handle", + "gossip_block_verification_handle", ) .ok_or(BeaconChainError::RuntimeShutdown)? .await @@ -3203,6 +3190,7 @@ impl BeaconChain { /// Cache the blob in the processing cache, process it, then evict it from the cache if it was /// imported or errors. + #[instrument(skip_all, level = "debug")] pub async fn process_gossip_blob( self: &Arc, blob: GossipVerifiedBlob, @@ -3226,12 +3214,12 @@ impl BeaconChain { self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); - let r = self.check_gossip_blob_availability_and_import(blob).await; - self.remove_notified(&block_root, r) + self.check_gossip_blob_availability_and_import(blob).await } /// Cache the data columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. + #[instrument(skip_all, level = "debug")] pub async fn process_gossip_data_columns( self: &Arc, data_columns: Vec>, @@ -3258,19 +3246,23 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - let r = self - .check_gossip_data_columns_availability_and_import( - slot, - block_root, - data_columns, - publish_fn, - ) - .await; - self.remove_notified(&block_root, r) + self.emit_sse_data_column_sidecar_events( + &block_root, + data_columns.iter().map(|column| column.as_data_column()), + ); + + self.check_gossip_data_columns_availability_and_import( + slot, + block_root, + data_columns, + publish_fn, + ) + .await } /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was /// imported or errors. + #[instrument(skip_all, level = "debug")] pub async fn process_rpc_blobs( self: &Arc, slot: Slot, @@ -3294,22 +3286,18 @@ impl BeaconChain { .iter() .filter_map(|b| b.as_ref().map(|b| b.block_parent_root())) .next() - { - if !self + && !self .canonical_head .fork_choice_read_lock() .contains_block(&parent_root) - { - return Err(BlockError::ParentUnknown { parent_root }); - } + { + return Err(BlockError::ParentUnknown { parent_root }); } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); - let r = self - .check_rpc_blob_availability_and_import(slot, block_root, blobs) - .await; - self.remove_notified(&block_root, r) + self.check_rpc_blob_availability_and_import(slot, block_root, blobs) + .await } /// Process blobs retrieved from the EL and returns the `AvailabilityProcessingStatus`. @@ -3317,7 +3305,7 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: EngineGetBlobsOutput, ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. @@ -3329,35 +3317,64 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - // process_engine_blobs is called for both pre and post PeerDAS. However, post PeerDAS - // consumers don't expect the blobs event to fire erratically. - if let EngineGetBlobsOutput::Blobs(blobs) = &engine_get_blobs_output { - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + match &engine_get_blobs_output { + EngineGetBlobsOutput::Blobs(blobs) => { + self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().map(|b| b.as_blob())); + } + EngineGetBlobsOutput::CustodyColumns(columns) => { + self.emit_sse_data_column_sidecar_events( + &block_root, + columns.iter().map(|column| column.as_data_column()), + ); + } } - let r = self - .check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) - .await; - self.remove_notified(&block_root, r) + self.check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) + .await } fn emit_sse_blob_sidecar_events<'a, I>(self: &Arc, block_root: &Hash256, blobs_iter: I) where I: Iterator>, { - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_blob_sidecar_subscribers() { - let imported_blobs = self - .data_availability_checker - .cached_blob_indexes(block_root) - .unwrap_or_default(); - let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index)); + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_blob_sidecar_subscribers() + { + let imported_blobs = self + .data_availability_checker + .cached_blob_indexes(block_root) + .unwrap_or_default(); + let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index)); - for blob in new_blobs { - event_handler.register(EventKind::BlobSidecar( - SseBlobSidecar::from_blob_sidecar(blob), - )); - } + for blob in new_blobs { + event_handler.register(EventKind::BlobSidecar(SseBlobSidecar::from_blob_sidecar( + blob, + ))); + } + } + } + + fn emit_sse_data_column_sidecar_events<'a, I>( + self: &Arc, + block_root: &Hash256, + data_columns_iter: I, + ) where + I: Iterator>, + { + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_data_column_sidecar_subscribers() + { + let imported_data_columns = self + .data_availability_checker + .cached_data_column_indexes(block_root) + .unwrap_or_default(); + let new_data_columns = + data_columns_iter.filter(|b| !imported_data_columns.contains(&b.index)); + + for data_column in new_data_columns { + event_handler.register(EventKind::DataColumnSidecar( + SseDataColumnSidecar::from_data_column_sidecar(data_column), + )); } } } @@ -3392,20 +3409,22 @@ impl BeaconChain { // Reject RPC columns referencing unknown parents. Otherwise we allow potentially invalid data // into the da_checker, where invalid = descendant of invalid blocks. // Note: custody_columns should have at least one item and all items have the same parent root. - if let Some(parent_root) = custody_columns.iter().map(|c| c.block_parent_root()).next() { - if !self + if let Some(parent_root) = custody_columns.iter().map(|c| c.block_parent_root()).next() + && !self .canonical_head .fork_choice_read_lock() .contains_block(&parent_root) - { - return Err(BlockError::ParentUnknown { parent_root }); - } + { + return Err(BlockError::ParentUnknown { parent_root }); } - let r = self - .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) - .await; - self.remove_notified(&block_root, r) + self.emit_sse_data_column_sidecar_events( + &block_root, + custody_columns.iter().map(|column| column.as_ref()), + ); + + self.check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) + .await } pub async fn reconstruct_data_columns( @@ -3431,15 +3450,15 @@ impl BeaconChain { let data_availability_checker = self.data_availability_checker.clone(); + let current_span = Span::current(); let result = self .task_executor - .spawn_blocking_handle( - move || data_availability_checker.reconstruct_data_columns(&block_root), - "reconstruct_data_columns", - ) - .ok_or(BeaconChainError::RuntimeShutdown)? + .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { + let _guard = current_span.enter(); + data_availability_checker.reconstruct_data_columns(&block_root) + }) .await - .map_err(BeaconChainError::TokioJoin)??; + .map_err(|_| BeaconChainError::RuntimeShutdown)??; match result { DataColumnReconstructionResult::Success((availability, data_columns_to_publish)) => { @@ -3448,10 +3467,8 @@ impl BeaconChain { return Ok(None); }; - let r = self - .process_availability(slot, availability, || Ok(())) - .await; - self.remove_notified(&block_root, r) + self.process_availability(slot, availability, || Ok(())) + .await .map(|availability_processing_status| { Some((availability_processing_status, data_columns_to_publish)) }) @@ -3468,46 +3485,6 @@ impl BeaconChain { } } - /// Remove any block components from the *processing cache* if we no longer require them. If the - /// block was imported full or erred, we no longer require them. - fn remove_notified( - &self, - block_root: &Hash256, - r: Result, - ) -> Result { - let has_missing_components = - matches!(r, Ok(AvailabilityProcessingStatus::MissingComponents(_, _))); - if !has_missing_components { - self.reqresp_pre_import_cache.write().remove(block_root); - } - r - } - - /// Wraps `process_block` in logic to cache the block's commitments in the processing cache - /// and evict if the block was imported or errored. - pub async fn process_block_with_early_caching>( - self: &Arc, - block_root: Hash256, - unverified_block: B, - block_source: BlockImportSource, - notify_execution_layer: NotifyExecutionLayer, - ) -> Result { - self.reqresp_pre_import_cache - .write() - .insert(block_root, unverified_block.block_cloned()); - - let r = self - .process_block( - block_root, - unverified_block, - notify_execution_layer, - block_source, - || Ok(()), - ) - .await; - self.remove_notified(&block_root, r) - } - /// Check for known and configured invalid block roots before processing. pub fn check_invalid_block_roots(&self, block_root: Hash256) -> Result<(), BlockError> { if self.config.invalid_block_roots.contains(&block_root) { @@ -3530,6 +3507,7 @@ impl BeaconChain { /// /// Returns an `Err` if the given block was invalid, or an error was encountered during /// verification. + #[instrument(skip_all, fields(block_root = ?block_root, block_source = %block_source))] pub async fn process_block>( self: &Arc, block_root: Hash256, @@ -3538,12 +3516,6 @@ impl BeaconChain { block_source: BlockImportSource, publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { - // Start the Prometheus timer. - let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); - - // Increment the Prometheus counter for block processing requests. - metrics::inc_counter(&metrics::BLOCK_PROCESSING_REQUESTS); - let block_slot = unverified_block.block().slot(); // Set observed time if not already set. Usually this should be set by gossip or RPC, @@ -3558,6 +3530,18 @@ impl BeaconChain { ); } + self.data_availability_checker.put_pre_execution_block( + block_root, + unverified_block.block_cloned(), + block_source, + )?; + + // Start the Prometheus timer. + let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); + + // Increment the Prometheus counter for block processing requests. + metrics::inc_counter(&metrics::BLOCK_PROCESSING_REQUESTS); + // A small closure to group the verification and import errors. let chain = self.clone(); let import_block = async move { @@ -3575,7 +3559,18 @@ impl BeaconChain { .set_time_consensus_verified(block_root, block_slot, timestamp) } - let executed_block = chain.into_executed_block(execution_pending).await?; + let executed_block = chain + .into_executed_block(execution_pending) + .await + .inspect_err(|_| { + // If the block fails execution for whatever reason (e.g. engine offline), + // and we keep it in the cache, then the node will NOT perform lookup and + // reprocess this block until the block is evicted from DA checker, causing the + // chain to get stuck temporarily if the block is canonical. Therefore we remove + // it from the cache if execution fails. + self.data_availability_checker + .remove_block_on_execution_error(&block_root); + })?; // Record the *additional* time it took to wait for execution layer verification. if let Some(timestamp) = self.slot_clock.now_duration() { @@ -3646,6 +3641,7 @@ impl BeaconChain { /// get a fully `ExecutedBlock`. /// /// An error is returned if the verification handle couldn't be awaited. + #[instrument(skip_all, level = "debug")] pub async fn into_executed_block( self: Arc, execution_pending_block: ExecutionPendingBlock, @@ -3694,14 +3690,13 @@ impl BeaconChain { /// Checks if the block is available, and imports immediately if so, otherwise caches the block /// in the data availability checker. + #[instrument(skip_all)] async fn check_block_availability_and_import( self: &Arc, block: AvailabilityPendingExecutedBlock, ) -> Result { let slot = block.block.slot(); - let availability = self - .data_availability_checker - .put_pending_executed_block(block)?; + let availability = self.data_availability_checker.put_executed_block(block)?; self.process_availability(slot, availability, || Ok(())) .await } @@ -3716,7 +3711,9 @@ impl BeaconChain { if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(blob.signed_block_header()); } - let availability = self.data_availability_checker.put_gossip_blob(blob)?; + let availability = self + .data_availability_checker + .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; self.process_availability(slot, availability, || Ok(())) .await @@ -3739,34 +3736,37 @@ impl BeaconChain { let availability = self .data_availability_checker - .put_gossip_data_columns(block_root, data_columns)?; + .put_gossip_verified_data_columns(block_root, slot, data_columns)?; self.process_availability(slot, availability, publish_fn) .await } - fn check_blobs_for_slashability( + fn check_blob_header_signature_and_slashability<'a>( self: &Arc, block_root: Hash256, - blobs: &FixedBlobSidecarList, + blobs: impl IntoIterator>, ) -> Result<(), BlockError> { let mut slashable_cache = self.observed_slashable.write(); for header in blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| b.signed_block_header.clone())) + .into_iter() + .map(|b| b.signed_block_header.clone()) .unique() { - if verify_header_signature::(self, &header).is_ok() { - slashable_cache - .observe_slashable( - header.message.slot, - header.message.proposer_index, - block_root, - ) - .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(header); - } + // Return an error if *any* header signature is invalid, we do not want to import this + // list of blobs into the DA checker. However, we will process any valid headers prior + // to the first invalid header in the slashable cache & slasher. + verify_header_signature::(self, &header)?; + + slashable_cache + .observe_slashable( + header.message.slot, + header.message.proposer_index, + block_root, + ) + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; + if let Some(slasher) = self.slasher.as_ref() { + slasher.accept_block_header(header); } } Ok(()) @@ -3780,7 +3780,10 @@ impl BeaconChain { block_root: Hash256, blobs: FixedBlobSidecarList, ) -> Result { - self.check_blobs_for_slashability(block_root, &blobs)?; + self.check_blob_header_signature_and_slashability( + block_root, + blobs.iter().flatten().map(Arc::as_ref), + )?; let availability = self .data_availability_checker .put_rpc_blobs(block_root, blobs)?; @@ -3793,18 +3796,24 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: EngineGetBlobsOutput, ) -> Result { let availability = match engine_get_blobs_output { EngineGetBlobsOutput::Blobs(blobs) => { - self.check_blobs_for_slashability(block_root, &blobs)?; + self.check_blob_header_signature_and_slashability( + block_root, + blobs.iter().map(|b| b.as_blob()), + )?; self.data_availability_checker - .put_engine_blobs(block_root, blobs)? + .put_kzg_verified_blobs(block_root, blobs)? } EngineGetBlobsOutput::CustodyColumns(data_columns) => { - self.check_columns_for_slashability(block_root, &data_columns)?; + self.check_data_column_sidecar_header_signature_and_slashability( + block_root, + data_columns.iter().map(|c| c.as_data_column()), + )?; self.data_availability_checker - .put_engine_data_columns(block_root, data_columns)? + .put_kzg_verified_custody_data_columns(block_root, data_columns)? } }; @@ -3820,38 +3829,51 @@ impl BeaconChain { block_root: Hash256, custody_columns: DataColumnSidecarList, ) -> Result { - self.check_columns_for_slashability(block_root, &custody_columns)?; + self.check_data_column_sidecar_header_signature_and_slashability( + block_root, + custody_columns.iter().map(|c| c.as_ref()), + )?; // This slot value is purely informative for the consumers of // `AvailabilityProcessingStatus::MissingComponents` to log an error with a slot. - let availability = self - .data_availability_checker - .put_rpc_custody_columns(block_root, custody_columns)?; + let availability = self.data_availability_checker.put_rpc_custody_columns( + block_root, + slot, + custody_columns, + )?; self.process_availability(slot, availability, || Ok(())) .await } - fn check_columns_for_slashability( + fn check_data_column_sidecar_header_signature_and_slashability<'a>( self: &Arc, block_root: Hash256, - custody_columns: &DataColumnSidecarList, + custody_columns: impl IntoIterator>, ) -> Result<(), BlockError> { let mut slashable_cache = self.observed_slashable.write(); - // Assumes all items in custody_columns are for the same block_root - if let Some(column) = custody_columns.first() { - let header = &column.signed_block_header; - if verify_header_signature::(self, header).is_ok() { - slashable_cache - .observe_slashable( - header.message.slot, - header.message.proposer_index, - block_root, - ) - .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(header.clone()); - } + // Process all unique block headers - previous logic assumed all headers were identical and + // only processed the first one. However, we should not make assumptions about data received + // from RPC. + for header in custody_columns + .into_iter() + .map(|c| c.signed_block_header.clone()) + .unique() + { + // Return an error if *any* header signature is invalid, we do not want to import this + // list of blobs into the DA checker. However, we will process any valid headers prior + // to the first invalid header in the slashable cache & slasher. + verify_header_signature::(self, &header)?; + + slashable_cache + .observe_slashable( + header.message.slot, + header.message.proposer_index, + block_root, + ) + .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; + if let Some(slasher) = self.slasher.as_ref() { + slasher.accept_block_header(header); } } Ok(()) @@ -3879,6 +3901,7 @@ impl BeaconChain { } } + #[instrument(skip_all)] pub async fn import_available_block( self: &Arc, block: Box>, @@ -3893,7 +3916,6 @@ impl BeaconChain { block_root, state, parent_block, - parent_eth1_finalization_data, consensus_context, } = import_data; @@ -3908,37 +3930,27 @@ impl BeaconChain { // TODO(das) record custody column available timestamp - // import - let chain = self.clone(); - let block_root = self - .spawn_blocking_handle( + let block_root = { + // Capture the current span before moving into the blocking task + let current_span = tracing::Span::current(); + let chain = self.clone(); + self.spawn_blocking_handle( move || { + // Enter the captured span in the blocking thread + let _guard = current_span.enter(); chain.import_block( block, block_root, state, payload_verification_outcome.payload_verification_status, parent_block, - parent_eth1_finalization_data, consensus_context, ) }, "payload_verification_handle", ) - .await??; - - // Remove block components from da_checker AFTER completing block import. Then we can assert - // the following invariant: - // > A valid unfinalized block is either in fork-choice or da_checker. - // - // If we remove the block when it becomes available, there's some time window during - // `import_block` where the block is nowhere. Consumers of the da_checker can handle the - // extend time a block may exist in the da_checker. - // - // If `import_block` errors (only errors with internal errors), the pending components will - // be pruned on data_availability_checker maintenance as finality advances. - self.data_availability_checker - .remove_pending_components(block_root); + .await?? + }; Ok(AvailabilityProcessingStatus::Imported(block_root)) } @@ -3949,6 +3961,7 @@ impl BeaconChain { /// An error is returned if the block was unable to be imported. It may be partially imported /// (i.e., this function is not atomic). #[allow(clippy::too_many_arguments)] + #[instrument(skip_all)] fn import_block( &self, signed_block: AvailableBlock, @@ -3956,7 +3969,6 @@ impl BeaconChain { mut state: BeaconState, payload_verification_status: PayloadVerificationStatus, parent_block: SignedBlindedBeaconBlock, - parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, ) -> Result { // ----------------------------- BLOCK NOT YET ATTESTABLE ---------------------------------- @@ -3987,6 +3999,13 @@ impl BeaconChain { // Only take a write lock if there are new keys to import. if state.validators().len() > pubkey_cache.len() { + let _pubkey_span = debug_span!( + "pubkey_cache_update", + new_validators = tracing::field::Empty, + cache_len_before = pubkey_cache.len() + ) + .entered(); + parking_lot::RwLockUpgradableReadGuard::upgrade(pubkey_cache) .import_new_pubkeys(&state)? } else { @@ -4000,14 +4019,22 @@ impl BeaconChain { // However, latency between the VC and the BN might cause the VC to produce attestations at // a previous slot. if state.current_epoch().saturating_add(1_u64) >= current_epoch { + let _attester_span = debug_span!("attester_cache_update").entered(); self.attester_cache .maybe_cache_state(&state, block_root, &self.spec) .map_err(BeaconChainError::from)?; } + // Take an upgradable read lock on fork choice so we can check if this block has already + // been imported. We don't want to repeat work importing a block that is already imported. + let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); + if fork_choice_reader.contains_block(&block_root) { + return Err(BlockError::DuplicateFullyImported(block_root)); + } + // Take an exclusive write-lock on fork choice. It's very important to prevent deadlocks by // avoiding taking other locks whilst holding this lock. - let mut fork_choice = self.canonical_head.fork_choice_write_lock(); + let mut fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader); // Do not import a block that doesn't descend from the finalized root. let signed_block = @@ -4120,7 +4147,7 @@ impl BeaconChain { // See https://github.com/sigp/lighthouse/issues/2028 let (_, signed_block, block_data) = signed_block.deconstruct(); - match self.get_blobs_or_columns_store_op(block_root, block_data) { + match self.get_blobs_or_columns_store_op(block_root, signed_block.slot(), block_data) { Ok(Some(blobs_or_columns_store_op)) => { ops.push(blobs_or_columns_store_op); } @@ -4144,7 +4171,7 @@ impl BeaconChain { ops.push(StoreOp::PutBlock(block_root, signed_block.clone())); ops.push(StoreOp::PutState(block.state_root(), &state)); - let txn_lock = self.store.hot_db.begin_rw_transaction(); + let db_span = info_span!("persist_blocks_and_blobs").entered(); if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { error!( @@ -4157,7 +4184,8 @@ impl BeaconChain { .err() .unwrap_or(e.into())); } - drop(txn_lock); + + drop(db_span); // The fork choice write-lock is dropped *after* the on-disk database has been updated. // This prevents inconsistency between the two at the expense of concurrency. @@ -4167,12 +4195,6 @@ impl BeaconChain { // about it. let block_time_imported = timestamp_now(); - let current_eth1_finalization_data = Eth1FinalizationData { - eth1_data: state.eth1_data().clone(), - eth1_deposit_index: state.eth1_deposit_index(), - }; - let current_finalized_checkpoint = state.finalized_checkpoint(); - // compute state proofs for light client updates before inserting the state into the // snapshot cache. if self.config.enable_light_client_server { @@ -4191,17 +4213,6 @@ impl BeaconChain { metrics::inc_counter(&metrics::BLOCK_PROCESSING_SUCCESSES); - // Update the deposit contract cache. - self.import_block_update_deposit_contract_finalization( - block, - block_root, - current_epoch, - current_finalized_checkpoint, - current_eth1_finalization_data, - parent_eth1_finalization_data, - parent_block.slot(), - ); - // Inform the unknown block cache, in case it was waiting on this block. self.pre_finalization_block_cache .block_processed(block_root); @@ -4275,41 +4286,41 @@ impl BeaconChain { // This ensures we only perform the check once. if current_head_finalized_checkpoint.epoch < wss_checkpoint.epoch && wss_checkpoint.epoch <= new_finalized_checkpoint.epoch - { - if let Err(e) = + && let Err(e) = self.verify_weak_subjectivity_checkpoint(wss_checkpoint, block_root, state) - { - let mut shutdown_sender = self.shutdown_sender(); - crit!( - ?block_root, - parent_root = ?block.parent_root(), - old_finalized_epoch = ?current_head_finalized_checkpoint.epoch, - new_finalized_epoch = ?new_finalized_checkpoint.epoch, - weak_subjectivity_epoch = ?wss_checkpoint.epoch, - error = ?e, - "Weak subjectivity checkpoint verification failed while importing block!" - ); - crit!( - "You must use the `--purge-db` flag to clear the database and restart sync. \ + { + let mut shutdown_sender = self.shutdown_sender(); + crit!( + ?block_root, + parent_root = ?block.parent_root(), + old_finalized_epoch = ?current_head_finalized_checkpoint.epoch, + new_finalized_epoch = ?new_finalized_checkpoint.epoch, + weak_subjectivity_epoch = ?wss_checkpoint.epoch, + error = ?e, + "Weak subjectivity checkpoint verification failed while importing block!" + ); + crit!( + "You must use the `--purge-db` flag to clear the database and restart sync. \ You may be on a hostile network." - ); - shutdown_sender - .try_send(ShutdownReason::Failure( - "Weak subjectivity checkpoint verification failed. \ + ); + shutdown_sender + .try_send(ShutdownReason::Failure( + "Weak subjectivity checkpoint verification failed. \ Provided block root is not a checkpoint.", + )) + .map_err(|err| { + BlockError::BeaconChainError(Box::new( + BeaconChainError::WeakSubjectivtyShutdownError(err), )) - .map_err(|err| { - BlockError::BeaconChainError(Box::new( - BeaconChainError::WeakSubjectivtyShutdownError(err), - )) - })?; - return Err(BlockError::WeakSubjectivityConflict); - } + })?; + return Err(BlockError::WeakSubjectivityConflict); } + Ok(()) } /// Process a block for the validator monitor, including all its constituent messages. + #[instrument(skip_all, level = "debug")] fn import_block_update_validator_monitor( &self, block: BeaconBlockRef, @@ -4404,6 +4415,7 @@ impl BeaconChain { /// Iterate through the attestations in the block and register them as "observed". /// /// This will stop us from propagating them on the gossip network. + #[instrument(skip_all, level = "debug")] fn import_block_observe_attestations( &self, block: BeaconBlockRef, @@ -4466,6 +4478,7 @@ impl BeaconChain { } /// If a slasher is configured, provide the attestations from the block. + #[instrument(skip_all, level = "debug")] fn import_block_update_slasher( &self, block: BeaconBlockRef, @@ -4530,40 +4543,38 @@ impl BeaconChain { ); } - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_block_subscribers() { - event_handler.register(EventKind::Block(SseBlock { - slot: block.slot(), - block: block_root, - execution_optimistic: payload_verification_status.is_optimistic(), - })); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_block_subscribers() + { + event_handler.register(EventKind::Block(SseBlock { + slot: block.slot(), + block: block_root, + execution_optimistic: payload_verification_status.is_optimistic(), + })); } // Do not trigger light_client server update producer for old blocks, to extra work // during sync. if self.config.enable_light_client_server && block_delay_total < self.slot_clock.slot_duration() * 32 + && let Some(mut light_client_server_tx) = self.light_client_server_tx.clone() + && let Ok(sync_aggregate) = block.body().sync_aggregate() + && let Err(e) = light_client_server_tx.try_send(( + block.parent_root(), + block.slot(), + sync_aggregate.clone(), + )) { - if let Some(mut light_client_server_tx) = self.light_client_server_tx.clone() { - if let Ok(sync_aggregate) = block.body().sync_aggregate() { - if let Err(e) = light_client_server_tx.try_send(( - block.parent_root(), - block.slot(), - sync_aggregate.clone(), - )) { - warn!( - error = ?e, - "Failed to send light_client server event" - ); - } - } - } + warn!( + error = ?e, + "Failed to send light_client server event" + ); } } // For the current and next epoch of this state, ensure we have the shuffling from this // block in our cache. + #[instrument(skip_all, level = "debug")] fn import_block_update_shuffling_cache( &self, block_root: Hash256, @@ -4598,66 +4609,8 @@ impl BeaconChain { Ok(()) } - #[allow(clippy::too_many_arguments)] - fn import_block_update_deposit_contract_finalization( - &self, - block: BeaconBlockRef, - block_root: Hash256, - current_epoch: Epoch, - current_finalized_checkpoint: Checkpoint, - current_eth1_finalization_data: Eth1FinalizationData, - parent_eth1_finalization_data: Eth1FinalizationData, - parent_block_slot: Slot, - ) { - // Do not write to eth1 finalization cache for blocks older than 5 epochs. - if block.epoch() + 5 < current_epoch { - return; - } - - let parent_block_epoch = parent_block_slot.epoch(T::EthSpec::slots_per_epoch()); - if parent_block_epoch < current_epoch { - // we've crossed epoch boundary, store Eth1FinalizationData - let (checkpoint, eth1_finalization_data) = - if block.slot() % T::EthSpec::slots_per_epoch() == 0 { - // current block is the checkpoint - ( - Checkpoint { - epoch: current_epoch, - root: block_root, - }, - current_eth1_finalization_data, - ) - } else { - // parent block is the checkpoint - ( - Checkpoint { - epoch: current_epoch, - root: block.parent_root(), - }, - parent_eth1_finalization_data, - ) - }; - - let finalized_eth1_data = { - let mut cache = self.eth1_finalization_cache.write(); - cache.insert(checkpoint, eth1_finalization_data); - cache.finalize(¤t_finalized_checkpoint) - }; - if let Some(finalized_eth1_data) = finalized_eth1_data { - if let Some(eth1_chain) = self.eth1_chain.as_ref() { - let finalized_deposit_count = finalized_eth1_data.deposit_count; - eth1_chain.finalize_eth1_data(finalized_eth1_data); - debug!( - epoch = %current_finalized_checkpoint.epoch, - deposit_count = %finalized_deposit_count, - "called eth1_chain.finalize_eth1_data()" - ); - } - } - } - } - /// If configured, wait for the fork choice run at the start of the slot to complete. + #[instrument(level = "debug", skip_all)] fn wait_for_fork_choice_before_block_production( self: &Arc, slot: Slot, @@ -4709,7 +4662,7 @@ impl BeaconChain { self: &Arc, randao_reveal: Signature, slot: Slot, - validator_graffiti: Option, + graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, builder_boost_factor: Option, block_production_version: BlockProductionVersion, @@ -4720,10 +4673,15 @@ impl BeaconChain { // // Load the parent state from disk. let chain = self.clone(); + let span = Span::current(); let (state, state_root_opt) = self .task_executor .spawn_blocking_handle( - move || chain.load_state_for_block_production(slot), + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, "load_state_for_block_production", ) .ok_or(BlockProductionError::ShuttingDown)? @@ -4738,7 +4696,7 @@ impl BeaconChain { state_root_opt, slot, randao_reveal, - validator_graffiti, + graffiti_settings, verification, builder_boost_factor, block_production_version, @@ -4810,6 +4768,7 @@ impl BeaconChain { /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. /// /// This function will return `None` if proposer re-orgs are disabled. + #[instrument(skip_all, level = "debug")] fn get_state_for_re_org( &self, slot: Slot, @@ -4930,65 +4889,59 @@ impl BeaconChain { // Compute the proposer index. let head_epoch = cached_head.head_slot().epoch(T::EthSpec::slots_per_epoch()); - let shuffling_decision_root = if head_epoch == proposal_epoch { - cached_head - .snapshot - .beacon_state - .proposer_shuffling_decision_root(proposer_head)? - } else { - proposer_head - }; - let cached_proposer = self - .beacon_proposer_cache - .lock() - .get_slot::(shuffling_decision_root, proposal_slot); - let proposer_index = if let Some(proposer) = cached_proposer { - proposer.index as u64 - } else { - if head_epoch + self.config.sync_tolerance_epochs < proposal_epoch { - warn!( - msg = "this is a non-critical issue that can happen on unhealthy nodes or \ - networks.", - %proposal_epoch, - %head_epoch, - "Skipping proposer preparation" - ); + let shuffling_decision_root = cached_head + .snapshot + .beacon_state + .proposer_shuffling_decision_root_at_epoch(proposal_epoch, proposer_head, &self.spec)?; - // Don't skip the head forward more than two epochs. This avoids burdening an - // unhealthy node. - // - // Although this node might miss out on preparing for a proposal, they should still - // be able to propose. This will prioritise beacon chain health over efficient - // packing of execution blocks. - return Ok(None); + let Some(proposer_index) = self.with_proposer_cache( + shuffling_decision_root, + proposal_epoch, + |proposers| proposers.get_slot::(proposal_slot).map(|p| p.index as u64), + || { + if head_epoch + self.config.sync_tolerance_epochs < proposal_epoch { + warn!( + msg = "this is a non-critical issue that can happen on unhealthy nodes or \ + networks", + %proposal_epoch, + %head_epoch, + "Skipping proposer preparation" + ); + + // Don't skip the head forward too many epochs. This avoids burdening an + // unhealthy node. + // + // Although this node might miss out on preparing for a proposal, they should + // still be able to propose. This will prioritise beacon chain health over + // efficient packing of execution blocks. + Err(Error::SkipProposerPreparation) + } else { + debug!( + ?shuffling_decision_root, + epoch = %proposal_epoch, + "Proposer shuffling cache miss for proposer prep" + ); + let head = self.canonical_head.cached_head(); + Ok(( + head.head_state_root(), + head.snapshot.beacon_state.clone(), + )) + } + }, + ).map_or_else(|e| { + match e { + Error::ProposerCacheIncorrectState { .. } => { + warn!("Head changed during proposer preparation"); + Ok(None) + } + Error::SkipProposerPreparation => { + // Warning logged for this above. + Ok(None) + } + e => Err(e) } - - let (proposers, decision_root, _, fork) = - compute_proposer_duties_from_head(proposal_epoch, self)?; - - let proposer_offset = (proposal_slot % T::EthSpec::slots_per_epoch()).as_usize(); - let proposer = *proposers - .get(proposer_offset) - .ok_or(BeaconChainError::NoProposerForSlot(proposal_slot))?; - - self.beacon_proposer_cache.lock().insert( - proposal_epoch, - decision_root, - proposers, - fork, - )?; - - // It's possible that the head changes whilst computing these duties. If so, abandon - // this routine since the change of head would have also spawned another instance of - // this routine. - // - // Exit now, after updating the cache. - if decision_root != shuffling_decision_root { - warn!("Head changed during proposer preparation"); - return Ok(None); - } - - proposer as u64 + }, |value| Ok(Some(value)))? else { + return Ok(None); }; // Get the `prev_randao` and parent block number. @@ -5148,14 +5101,19 @@ impl BeaconChain { // Only attempt a re-org if we have a proposer registered for the re-org slot. let proposing_at_re_org_slot = { - // The proposer shuffling has the same decision root as the next epoch attestation - // shuffling. We know our re-org block is not on the epoch boundary, so it has the - // same proposer shuffling as the head (but not necessarily the parent which may lie - // in the previous epoch). - let shuffling_decision_root = info - .head_node - .next_epoch_shuffling_id - .shuffling_decision_block; + // We know our re-org block is not on the epoch boundary, so it has the same proposer + // shuffling as the head (but not necessarily the parent which may lie in the previous + // epoch). + let shuffling_decision_root = if self + .spec + .fork_name_at_slot::(re_org_block_slot) + .fulu_enabled() + { + info.head_node.current_epoch_shuffling_id + } else { + info.head_node.next_epoch_shuffling_id + } + .shuffling_decision_block; let proposer_index = self .beacon_proposer_cache .lock() @@ -5264,13 +5222,14 @@ impl BeaconChain { /// equal to the root of `state`. Providing this value will serve as an optimization to avoid /// performing a tree hash in some scenarios. #[allow(clippy::too_many_arguments)] + #[instrument(level = "debug", skip_all)] pub async fn produce_block_on_state( self: &Arc, state: BeaconState, state_root_opt: Option, produce_at_slot: Slot, randao_reveal: Signature, - validator_graffiti: Option, + graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, builder_boost_factor: Option, block_production_version: BlockProductionVersion, @@ -5281,12 +5240,15 @@ impl BeaconChain { let chain = self.clone(); let graffiti = self .graffiti_calculator - .get_graffiti(validator_graffiti) + .get_graffiti(graffiti_settings) .await; + let span = Span::current(); let mut partial_beacon_block = self .task_executor .spawn_blocking_handle( move || { + let _guard = + debug_span!(parent: span, "produce_partial_beacon_block").entered(); chain.produce_partial_beacon_block( state, state_root_opt, @@ -5322,10 +5284,14 @@ impl BeaconChain { match block_contents_type { BlockProposalContentsType::Full(block_contents) => { let chain = self.clone(); + let span = Span::current(); let beacon_block_response = self .task_executor .spawn_blocking_handle( move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block") + .entered(); chain.complete_partial_beacon_block( partial_beacon_block, Some(block_contents), @@ -5342,10 +5308,14 @@ impl BeaconChain { } BlockProposalContentsType::Blinded(block_contents) => { let chain = self.clone(); + let span = Span::current(); let beacon_block_response = self .task_executor .spawn_blocking_handle( move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block") + .entered(); chain.complete_partial_beacon_block( partial_beacon_block, Some(block_contents), @@ -5363,10 +5333,13 @@ impl BeaconChain { } } else { let chain = self.clone(); + let span = Span::current(); let beacon_block_response = self .task_executor .spawn_blocking_handle( move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block").entered(); chain.complete_partial_beacon_block( partial_beacon_block, None, @@ -5394,11 +5367,6 @@ impl BeaconChain { builder_boost_factor: Option, block_production_version: BlockProductionVersion, ) -> Result, BlockProductionError> { - let eth1_chain = self - .eth1_chain - .as_ref() - .ok_or(BlockProductionError::NoEth1ChainConnection)?; - // It is invalid to try to produce a block using a state from a future slot. if state.slot() > produce_at_slot { return Err(BlockProductionError::StateSlotTooHigh { @@ -5460,64 +5428,71 @@ impl BeaconChain { None }; + let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = self.op_pool.get_slashings_and_exits(&state, &self.spec); + drop(slashings_and_exits_span); - let eth1_data = eth1_chain.eth1_data_for_block_production(&state, &self.spec)?; + let eth1_data = state.eth1_data().clone(); - let deposits = eth1_chain.deposits_for_block_inclusion(&state, ð1_data, &self.spec)?; + let deposits = vec![]; + let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered(); let bls_to_execution_changes = self .op_pool .get_bls_to_execution_changes(&state, &self.spec); + drop(bls_changes_span); // Iterate through the naive aggregation pool and ensure all the attestations from there // are included in the operation pool. - let unagg_import_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); - for attestation in self.naive_aggregation_pool.read().iter() { - let import = |attestation: &Attestation| { - let attesting_indices = - get_attesting_indices_from_state(&state, attestation.to_ref())?; - self.op_pool - .insert_attestation(attestation.clone(), attesting_indices) - }; - if let Err(e) = import(attestation) { - // Don't stop block production if there's an error, just create a log. - error!( - reason = ?e, - "Attestation did not transfer to op pool" - ); + { + let _guard = debug_span!("import_naive_aggregation_pool").entered(); + let _unagg_import_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + for attestation in self.naive_aggregation_pool.read().iter() { + let import = |attestation: &Attestation| { + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; + self.op_pool + .insert_attestation(attestation.clone(), attesting_indices) + }; + if let Err(e) = import(attestation) { + // Don't stop block production if there's an error, just create a log. + error!( + reason = ?e, + "Attestation did not transfer to op pool" + ); + } } - } - drop(unagg_import_timer); - - let attestation_packing_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); - - // Epoch cache and total balance cache are required for op pool packing. - state.build_total_active_balance_cache(&self.spec)?; - initialize_epoch_cache(&mut state, &self.spec)?; - - let mut prev_filter_cache = HashMap::new(); - let prev_attestation_filter = |att: &CompactAttestationRef| { - self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) - }; - let mut curr_filter_cache = HashMap::new(); - let curr_attestation_filter = |att: &CompactAttestationRef| { - self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) }; - let mut attestations = self - .op_pool - .get_attestations( - &state, - prev_attestation_filter, - curr_attestation_filter, - &self.spec, - ) - .map_err(BlockProductionError::OpPoolError)?; - drop(attestation_packing_timer); + let mut attestations = { + let _guard = debug_span!("pack_attestations").entered(); + let _attestation_packing_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + + let mut prev_filter_cache = HashMap::new(); + let prev_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) + }; + let mut curr_filter_cache = HashMap::new(); + let curr_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) + }; + + self.op_pool + .get_attestations( + &state, + prev_attestation_filter, + curr_attestation_filter, + &self.spec, + ) + .map_err(BlockProductionError::OpPoolError)? + }; // If paranoid mode is enabled re-check the signatures of every included message. // This will be a lot slower but guards against bugs in block production and can be @@ -5686,11 +5661,21 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_base.into(), - attestations: attestations_base.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, _phantom: PhantomData, }, }), @@ -5707,11 +5692,21 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_base.into(), - attestations: attestations_base.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, _phantom: PhantomData, @@ -5734,11 +5729,21 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_base.into(), - attestations: attestations_base.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: block_proposal_contents @@ -5766,18 +5771,30 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_base.into(), - attestations: attestations_base.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: block_proposal_contents .to_payload() .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, - bls_to_execution_changes: bls_to_execution_changes.into(), + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, }, }), None, @@ -5805,17 +5822,29 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_base.into(), - attestations: attestations_base.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_base + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: payload .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, - bls_to_execution_changes: bls_to_execution_changes.into(), + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, blob_kzg_commitments: kzg_commitments.ok_or( BlockProductionError::MissingKzgCommitment( "Kzg commitments missing from block contents".to_string(), @@ -5848,17 +5877,29 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_electra.into(), - attestations: attestations_electra.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: payload .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, - bls_to_execution_changes: bls_to_execution_changes.into(), + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, execution_requests: maybe_requests @@ -5891,17 +5932,29 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_electra.into(), - attestations: attestations_electra.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: payload .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, - bls_to_execution_changes: bls_to_execution_changes.into(), + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, execution_requests: maybe_requests @@ -5933,17 +5986,29 @@ impl BeaconChain { randao_reveal, eth1_data, graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings_electra.into(), - attestations: attestations_electra.into(), - deposits: deposits.into(), - voluntary_exits: voluntary_exits.into(), + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations_electra + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, execution_payload: payload .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, - bls_to_execution_changes: bls_to_execution_changes.into(), + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, execution_requests: maybe_requests @@ -5954,6 +6019,7 @@ impl BeaconChain { execution_payload_value, ) } + BeaconState::Gloas(_) => return Err(BlockProductionError::GloasNotImplemented), }; let block = SignedBeaconBlock::from_block( @@ -6002,8 +6068,6 @@ impl BeaconChain { let (mut block, _) = block.deconstruct(); *block.state_root_mut() = state_root; - let blobs_verification_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES); let blob_items = match maybe_blobs_and_proofs { Some((blobs, proofs)) => { let expected_kzg_commitments = @@ -6022,37 +6086,11 @@ impl BeaconChain { ))); } - let kzg_proofs = Vec::from(proofs); - - let kzg = self.kzg.as_ref(); - if self - .spec - .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) - { - kzg_utils::validate_blobs_and_cell_proofs::( - kzg, - blobs.iter().collect(), - &kzg_proofs, - expected_kzg_commitments, - ) - .map_err(BlockProductionError::KzgError)?; - } else { - kzg_utils::validate_blobs::( - kzg, - expected_kzg_commitments, - blobs.iter().collect(), - &kzg_proofs, - ) - .map_err(BlockProductionError::KzgError)?; - } - - Some((kzg_proofs.into(), blobs)) + Some((proofs, blobs)) } None => None, }; - drop(blobs_verification_timer); - metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); trace!( @@ -6317,21 +6355,21 @@ impl BeaconChain { }; // Push a server-sent event (probably to a block builder or relay). - if let Some(event_handler) = &self.event_handler { - if event_handler.has_payload_attributes_subscribers() { - event_handler.register(EventKind::PayloadAttributes(ForkVersionedResponse { - data: SseExtendedPayloadAttributes { - proposal_slot: prepare_slot, - proposer_index: proposer, - parent_block_root: head_root, - parent_block_number: pre_payload_attributes.parent_block_number, - parent_block_hash: forkchoice_update_params.head_hash.unwrap_or_default(), - payload_attributes: payload_attributes.into(), - }, - metadata: Default::default(), - version: self.spec.fork_name_at_slot::(prepare_slot), - })); - } + if let Some(event_handler) = &self.event_handler + && event_handler.has_payload_attributes_subscribers() + { + event_handler.register(EventKind::PayloadAttributes(ForkVersionedResponse { + data: SseExtendedPayloadAttributes { + proposal_slot: prepare_slot, + proposer_index: proposer, + parent_block_root: head_root, + parent_block_number: pre_payload_attributes.parent_block_number, + parent_block_hash: forkchoice_update_params.head_hash.unwrap_or_default(), + payload_attributes: payload_attributes.into(), + }, + metadata: Default::default(), + version: self.spec.fork_name_at_slot::(prepare_slot), + })); } let Some(till_prepare_slot) = self.slot_clock.duration_to_slot(prepare_slot) else { @@ -6791,14 +6829,14 @@ impl BeaconChain { self.task_executor.clone().spawn_blocking( move || { // Signal block proposal for the next slot (if it happens to be waiting). - if let Some(tx) = &chain.fork_choice_signal_tx { - if let Err(e) = tx.notify_fork_choice_complete(slot) { - warn!( - error = ?e, - %slot, - "Error signalling fork choice waiter" - ); - } + if let Some(tx) = &chain.fork_choice_signal_tx + && let Err(e) = tx.notify_fork_choice_complete(slot) + { + warn!( + error = ?e, + %slot, + "Error signalling fork choice waiter" + ); } }, "per_slot_task_fc_signal_tx", @@ -6806,6 +6844,91 @@ impl BeaconChain { } } + /// This function provides safe and efficient multi-threaded access to the beacon proposer cache. + /// + /// The arguments are: + /// + /// - `shuffling_decision_block`: The block root of the decision block for the desired proposer + /// shuffling. This should be computed using one of the methods for computing proposer + /// shuffling decision roots, e.g. `BeaconState::proposer_shuffling_decision_root_at_epoch`. + /// - `proposal_epoch`: The epoch at which the proposer shuffling is required. + /// - `accessor`: A closure to run against the proposers for the selected epoch. Usually this + /// closure just grabs a single proposer, or takes the vec of proposers for the epoch. + /// - `state_provider`: A closure to compute a state suitable for determining the shuffling. + /// This closure is evaluated lazily ONLY in the case that a cache miss occurs. It is + /// recommended for code that wants to keep track of cache misses to produce a log and/or + /// increment a metric inside this closure . + /// + /// This function makes use of closures in order to efficiently handle concurrent accesses to + /// the cache. + /// + /// The error type is polymorphic, if in doubt you can use `BeaconChainError`. You might need + /// to use a turbofish if type inference can't work it out. + pub fn with_proposer_cache + From>( + &self, + shuffling_decision_block: Hash256, + proposal_epoch: Epoch, + accessor: impl Fn(&EpochBlockProposers) -> Result, + state_provider: impl FnOnce() -> Result<(Hash256, BeaconState), E>, + ) -> Result { + let cache_entry = self + .beacon_proposer_cache + .lock() + .get_or_insert_key(proposal_epoch, shuffling_decision_block); + + // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. + // This prevents duplication of work across multiple threads. + // + // If it is already initialised, then `get_or_try_init` will return immediately without + // executing the initialisation code at all. + let epoch_block_proposers = cache_entry.get_or_try_init(|| { + // Fetch the state on-demand if the required epoch was missing from the cache. + // If the caller wants to not compute the state they must return an error here and then + // catch it at the call site. + let (state_root, mut state) = state_provider()?; + + // Ensure the state can compute proposer duties for `epoch`. + ensure_state_can_determine_proposers_for_epoch( + &mut state, + state_root, + proposal_epoch, + &self.spec, + )?; + + // Sanity check the state. + let latest_block_root = state.get_latest_block_root(state_root); + let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( + proposal_epoch, + latest_block_root, + &self.spec, + )?; + if state_decision_block_root != shuffling_decision_block { + return Err(Error::ProposerCacheIncorrectState { + state_decision_block_root, + requested_decision_block_root: shuffling_decision_block, + } + .into()); + } + + let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?; + + // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have + // advanced the state completely into the new epoch. + let fork = self.spec.fork_at_epoch(proposal_epoch); + + debug!( + ?shuffling_decision_block, + epoch = %proposal_epoch, + "Priming proposer shuffling cache" + ); + + Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) + })?; + + // Run the accessor function on the computed epoch proposers. + accessor(epoch_block_proposers).map_err(Into::into) + } + /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head /// `head_block_root`. The `map_fn` will be supplied two values: /// @@ -6995,13 +7118,22 @@ impl BeaconChain { #[allow(clippy::type_complexity)] pub fn chain_dump( &self, + ) -> Result>>, Error> { + self.chain_dump_from_slot(Slot::new(0)) + } + + /// As for `chain_dump` but dumping only the portion of the chain newer than `from_slot`. + #[allow(clippy::type_complexity)] + pub fn chain_dump_from_slot( + &self, + from_slot: Slot, ) -> Result>>, Error> { let mut dump = vec![]; let mut prev_block_root = None; let mut prev_beacon_state = None; - for res in self.forwards_iter_block_roots(Slot::new(0))? { + for res in self.forwards_iter_block_roots(from_slot)? { let (beacon_block_root, _) = res?; // Do not include snapshots at skipped slots. @@ -7057,17 +7189,165 @@ impl BeaconChain { .enr_fork_id::(slot, self.genesis_validators_root) } - /// Calculates the `Duration` to the next fork if it exists and returns it - /// with it's corresponding `ForkName`. - pub fn duration_to_next_fork(&self) -> Option<(ForkName, Duration)> { + /// Returns the fork_digest corresponding to an epoch. + /// See [`ChainSpec::compute_fork_digest`] + pub fn compute_fork_digest(&self, epoch: Epoch) -> [u8; 4] { + self.spec + .compute_fork_digest(self.genesis_validators_root, epoch) + } + + /// Calculates the `Duration` to the next fork digest (this could be either a regular or BPO + /// hard fork) if it exists and returns it with its corresponding `Epoch`. + pub fn duration_to_next_digest(&self) -> Option<(Epoch, Duration)> { // If we are unable to read the slot clock we assume that it is prior to genesis and // therefore use the genesis slot. let slot = self.slot().unwrap_or(self.spec.genesis_slot); + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + + let next_digest_epoch = self.spec.next_digest_epoch(epoch)?; + let next_digest_slot = next_digest_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let (fork_name, epoch) = self.spec.next_fork_epoch::(slot)?; self.slot_clock - .duration_to_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())) - .map(|duration| (fork_name, duration)) + .duration_to_slot(next_digest_slot) + .map(|duration| (next_digest_epoch, duration)) + } + + /// Update data column custody info with the slot at which cgc was changed. + pub fn update_data_column_custody_info(&self, slot: Option) { + self.store + .put_data_column_custody_info(slot) + .unwrap_or_else(|e| error!(error = ?e, "Failed to update data column custody info")); + } + + /// Get the earliest epoch in which the node has met its custody requirements. + /// A `None` response indicates that we've met our custody requirements up to the + /// column data availability window + pub fn earliest_custodied_data_column_epoch(&self) -> Option { + self.store + .get_data_column_custody_info() + .inspect_err( + |e| error!(error=?e, "Failed to get data column custody info from the store"), + ) + .ok() + .flatten() + .and_then(|info| info.earliest_data_column_slot) + .map(|slot| { + let mut epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + // If the earliest custodied slot isn't the first slot in the epoch + // The node has only met its custody requirements for the next epoch. + if slot > epoch.start_slot(T::EthSpec::slots_per_epoch()) { + epoch += 1; + } + epoch + }) + } + + /// The data availability boundary for custodying columns. It will just be the + /// regular data availability boundary unless we are near the Fulu fork epoch. + pub fn column_data_availability_boundary(&self) -> Option { + match self.data_availability_boundary() { + Some(da_boundary_epoch) => { + if let Some(fulu_fork_epoch) = self.spec.fulu_fork_epoch { + if da_boundary_epoch < fulu_fork_epoch { + Some(fulu_fork_epoch) + } else { + Some(da_boundary_epoch) + } + } else { + None // Fulu hasn't been enabled + } + } + None => None, // Deneb hasn't been enabled + } + } + + /// Safely update data column custody info by ensuring that: + /// - cgc values at the updated epoch and the earliest custodied column epoch are equal + /// - we are only decrementing the earliest custodied data column epoch by one epoch + /// - the new earliest data column slot is set to the first slot in `effective_epoch`. + pub fn safely_backfill_data_column_custody_info( + &self, + effective_epoch: Epoch, + ) -> Result<(), Error> { + let Some(earliest_data_column_epoch) = self.earliest_custodied_data_column_epoch() else { + return Ok(()); + }; + + if effective_epoch >= earliest_data_column_epoch { + return Ok(()); + } + + let cgc_at_effective_epoch = self + .data_availability_checker + .custody_context() + .custody_group_count_at_epoch(effective_epoch, &self.spec); + + let cgc_at_earliest_data_colum_epoch = self + .data_availability_checker + .custody_context() + .custody_group_count_at_epoch(earliest_data_column_epoch, &self.spec); + + let can_update_data_column_custody_info = cgc_at_effective_epoch + == cgc_at_earliest_data_colum_epoch + && effective_epoch == earliest_data_column_epoch - 1; + + if can_update_data_column_custody_info { + self.store.put_data_column_custody_info(Some( + effective_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ))?; + } else { + error!( + ?cgc_at_effective_epoch, + ?cgc_at_earliest_data_colum_epoch, + ?effective_epoch, + ?earliest_data_column_epoch, + "Couldn't update data column custody info" + ); + return Err(Error::FailedColumnCustodyInfoUpdate); + } + + Ok(()) + } + + /// Compare columns custodied for `epoch` versus columns custodied for the head of the chain + /// and return any column indices that are missing. + pub fn get_missing_columns_for_epoch(&self, epoch: Epoch) -> HashSet { + let custody_context = self.data_availability_checker.custody_context(); + + let columns_required = custody_context + .custody_columns_for_epoch(None, &self.spec) + .iter() + .cloned() + .collect::>(); + + let current_columns_at_epoch = custody_context + .custody_columns_for_epoch(Some(epoch), &self.spec) + .iter() + .cloned() + .collect::>(); + + columns_required + .difference(¤t_columns_at_epoch) + .cloned() + .collect::>() + } + + /// The da boundary for custodying columns. It will just be the DA boundary unless we are near the Fulu fork epoch. + pub fn get_column_da_boundary(&self) -> Option { + match self.data_availability_boundary() { + Some(da_boundary_epoch) => { + if let Some(fulu_fork_epoch) = self.spec.fulu_fork_epoch { + if da_boundary_epoch < fulu_fork_epoch { + Some(fulu_fork_epoch) + } else { + Some(da_boundary_epoch) + } + } else { + None + } + } + None => None, // If no DA boundary set, dont try to custody backfill + } } /// This method serves to get a sense of the current chain health. It is used in block proposal @@ -7090,10 +7370,9 @@ impl BeaconChain { .canonical_head .fork_choice_read_lock() .get_block_execution_status(parent_root) + && execution_status.is_strictly_optimistic() { - if execution_status.is_strictly_optimistic() { - return Ok(ChainHealth::Optimistic); - } + return Ok(ChainHealth::Optimistic); } if self.config.builder_fallback_disable_checks { @@ -7297,15 +7576,6 @@ impl BeaconChain { && self.spec.is_peer_das_enabled_for_epoch(block_epoch) } - /// Returns true if we should issue a sampling request for this block - /// TODO(das): check if the block is still within the da_window - pub fn should_sample_slot(&self, slot: Slot) -> bool { - self.config.enable_sampling - && self - .spec - .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) - } - /// Gets the `LightClientBootstrap` object for a requested block root. /// /// Returns `None` when the state or block is not found in the database. @@ -7369,17 +7639,12 @@ impl BeaconChain { .inclusion_list_seen(signed_il) } - pub fn metrics(&self) -> BeaconChainMetrics { - BeaconChainMetrics { - reqresp_pre_import_cache_len: self.reqresp_pre_import_cache.read().len(), - } - } - pub(crate) fn get_blobs_or_columns_store_op( &self, block_root: Hash256, + block_slot: Slot, block_data: AvailableBlockData, - ) -> Result>, String> { + ) -> Result>, String> { match block_data { AvailableBlockData::NoData => Ok(None), AvailableBlockData::Blobs(blobs) => { @@ -7390,7 +7655,15 @@ impl BeaconChain { ); Ok(Some(StoreOp::PutBlobs(block_root, blobs))) } - AvailableBlockData::DataColumns(data_columns) => { + AvailableBlockData::DataColumns(mut data_columns) => { + let columns_to_custody = self.custody_columns_for_epoch(Some( + block_slot.epoch(T::EthSpec::slots_per_epoch()), + )); + // Supernodes need to persist all sampled custody columns + if columns_to_custody.len() != self.spec.number_of_custody_groups as usize { + data_columns + .retain(|data_column| columns_to_custody.contains(&data_column.index)); + } debug!( %block_root, count = data_columns.len(), @@ -7425,6 +7698,25 @@ impl BeaconChain { roots.reverse(); roots } + + /// Returns a list of column indices that should be sampled for a given epoch. + /// Used for data availability sampling in PeerDAS. + pub fn sampling_columns_for_epoch(&self, epoch: Epoch) -> &[ColumnIndex] { + self.data_availability_checker + .custody_context() + .sampling_columns_for_epoch(epoch, &self.spec) + } + + /// Returns a list of column indices that the node is expected to custody for a given epoch. + /// i.e. the node must have validated and persisted the column samples and should be able to + /// serve them to peers. + /// + /// If epoch is `None`, this function computes the custody columns at head. + pub fn custody_columns_for_epoch(&self, epoch_opt: Option) -> &[ColumnIndex] { + self.data_availability_checker + .custody_context() + .custody_columns_for_epoch(epoch_opt, &self.spec) + } } impl Drop for BeaconChain { @@ -7432,7 +7724,7 @@ impl Drop for BeaconChain { let drop = || -> Result<(), Error> { self.persist_fork_choice()?; self.persist_op_pool()?; - self.persist_eth1_cache() + self.persist_custody_context() }; if let Err(e) = drop() { diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 2134d78138..7705fced43 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -4,8 +4,9 @@ //! Additionally, the `BalancesCache` struct is defined; a cache designed to avoid database //! reads when fork choice requires the validator balances of the justified state. -use crate::{metrics, BeaconSnapshot}; -use derivative::Derivative; +use crate::{BeaconSnapshot, metrics}; +use educe::Educe; +use fixed_bytes::FixedBytesExtended; use fork_choice::ForkChoiceStore; use proto_array::JustifiedBalances; use safe_arith::ArithError; @@ -18,7 +19,7 @@ use superstruct::superstruct; use tracing::info; use types::{ AbstractExecPayload, BeaconBlockRef, BeaconState, BeaconStateError, Checkpoint, Epoch, EthSpec, - FixedBytesExtended, Hash256, Slot, + Hash256, Slot, }; #[derive(Debug)] @@ -28,6 +29,7 @@ pub enum Error { FailedToReadState(StoreError), MissingState(Hash256), BeaconStateError(BeaconStateError), + UnalignedCheckpoint { block_slot: Slot, state_slot: Slot }, Arith(ArithError), } @@ -127,17 +129,19 @@ impl BalancesCache { /// Implements `fork_choice::ForkChoiceStore` in order to provide a persistent backing to the /// `fork_choice::ForkChoice` struct. -#[derive(Debug, Derivative)] -#[derivative(PartialEq(bound = "E: EthSpec, Hot: ItemStore, Cold: ItemStore"))] +#[derive(Debug, Educe)] +#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))] pub struct BeaconForkChoiceStore, Cold: ItemStore> { - #[derivative(PartialEq = "ignore")] + #[educe(PartialEq(ignore))] store: Arc>, balances_cache: BalancesCache, time: Slot, finalized_checkpoint: Checkpoint, justified_checkpoint: Checkpoint, justified_balances: JustifiedBalances, + justified_state_root: Hash256, unrealized_justified_checkpoint: Checkpoint, + unrealized_justified_state_root: Hash256, unrealized_finalized_checkpoint: Checkpoint, proposer_boost_root: Hash256, equivocating_indices: BTreeSet, @@ -165,21 +169,37 @@ where /// It is assumed that `anchor` is already persisted in `store`. pub fn get_forkchoice_store( store: Arc>, - anchor: &BeaconSnapshot, + anchor: BeaconSnapshot, ) -> Result { - let anchor_state = &anchor.beacon_state; + let unadvanced_state_root = anchor.beacon_state_root(); + let mut anchor_state = anchor.beacon_state; let mut anchor_block_header = anchor_state.latest_block_header().clone(); - if anchor_block_header.state_root == Hash256::zero() { - anchor_block_header.state_root = anchor.beacon_state_root(); + + // The anchor state MUST be on an epoch boundary (it should be advanced by the caller). + if !anchor_state + .slot() + .as_u64() + .is_multiple_of(E::slots_per_epoch()) + { + return Err(Error::UnalignedCheckpoint { + block_slot: anchor_block_header.slot, + state_slot: anchor_state.slot(), + }); } - let anchor_root = anchor_block_header.canonical_root(); + + // Compute the accurate block root for the checkpoint block. + if anchor_block_header.state_root.is_zero() { + anchor_block_header.state_root = unadvanced_state_root; + } + let anchor_block_root = anchor_block_header.canonical_root(); let anchor_epoch = anchor_state.current_epoch(); let justified_checkpoint = Checkpoint { epoch: anchor_epoch, - root: anchor_root, + root: anchor_block_root, }; let finalized_checkpoint = justified_checkpoint; - let justified_balances = JustifiedBalances::from_justified_state(anchor_state)?; + let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; + let justified_state_root = anchor_state.canonical_root()?; Ok(Self { store, @@ -187,8 +207,10 @@ where time: anchor_state.slot(), justified_checkpoint, justified_balances, + justified_state_root, finalized_checkpoint, unrealized_justified_checkpoint: justified_checkpoint, + unrealized_justified_state_root: justified_state_root, unrealized_finalized_checkpoint: finalized_checkpoint, proposer_boost_root: Hash256::zero(), equivocating_indices: BTreeSet::new(), @@ -202,12 +224,12 @@ where /// on-disk database. pub fn to_persisted(&self) -> PersistedForkChoiceStore { PersistedForkChoiceStore { - balances_cache: self.balances_cache.clone(), time: self.time, finalized_checkpoint: self.finalized_checkpoint, justified_checkpoint: self.justified_checkpoint, - justified_balances: self.justified_balances.effective_balances.clone(), + justified_state_root: self.justified_state_root, unrealized_justified_checkpoint: self.unrealized_justified_checkpoint, + unrealized_justified_state_root: self.unrealized_justified_state_root, unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint, proposer_boost_root: self.proposer_boost_root, equivocating_indices: self.equivocating_indices.clone(), @@ -215,20 +237,62 @@ where } /// Restore `Self` from a previously-generated `PersistedForkChoiceStore`. - pub fn from_persisted( - persisted: PersistedForkChoiceStore, + /// + /// DEPRECATED. Can be deleted once migrations no longer require it. + pub fn from_persisted_v17( + persisted: PersistedForkChoiceStoreV17, + justified_state_root: Hash256, + unrealized_justified_state_root: Hash256, store: Arc>, ) -> Result { let justified_balances = JustifiedBalances::from_effective_balances(persisted.justified_balances)?; + Ok(Self { store, - balances_cache: persisted.balances_cache, + balances_cache: <_>::default(), time: persisted.time, finalized_checkpoint: persisted.finalized_checkpoint, justified_checkpoint: persisted.justified_checkpoint, justified_balances, + justified_state_root, unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint, + unrealized_justified_state_root, + unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint, + proposer_boost_root: persisted.proposer_boost_root, + equivocating_indices: persisted.equivocating_indices, + // TODO(eip7805) should persist these values? + inclusion_list_equivocators: <_>::default(), + unsatisfied_inclusion_list_blocks: <_>::default(), + _phantom: PhantomData, + }) + } + + /// Restore `Self` from a previously-generated `PersistedForkChoiceStore`. + pub fn from_persisted( + persisted: PersistedForkChoiceStore, + store: Arc>, + ) -> Result { + let justified_checkpoint = persisted.justified_checkpoint; + let justified_state_root = persisted.justified_state_root; + + let update_cache = true; + let justified_state = store + .get_hot_state(&justified_state_root, update_cache) + .map_err(Error::FailedToReadState)? + .ok_or(Error::MissingState(justified_state_root))?; + + let justified_balances = JustifiedBalances::from_justified_state(&justified_state)?; + Ok(Self { + store, + balances_cache: <_>::default(), + time: persisted.time, + finalized_checkpoint: persisted.finalized_checkpoint, + justified_checkpoint, + justified_balances, + justified_state_root, + unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint, + unrealized_justified_state_root: persisted.unrealized_justified_state_root, unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint, proposer_boost_root: persisted.proposer_boost_root, equivocating_indices: persisted.equivocating_indices, @@ -268,6 +332,10 @@ where &self.justified_checkpoint } + fn justified_state_root(&self) -> Hash256 { + self.justified_state_root + } + fn justified_balances(&self) -> &JustifiedBalances { &self.justified_balances } @@ -280,6 +348,10 @@ where &self.unrealized_justified_checkpoint } + fn unrealized_justified_state_root(&self) -> Hash256 { + self.unrealized_justified_state_root + } + fn unrealized_finalized_checkpoint(&self) -> &Checkpoint { &self.unrealized_finalized_checkpoint } @@ -292,8 +364,13 @@ where self.finalized_checkpoint = checkpoint } - fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> { + fn set_justified_checkpoint( + &mut self, + checkpoint: Checkpoint, + justified_state_root: Hash256, + ) -> Result<(), Error> { self.justified_checkpoint = checkpoint; + self.justified_state_root = justified_state_root; if let Some(balances) = self.balances_cache.get( self.justified_checkpoint.root, @@ -304,27 +381,14 @@ where self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; } else { metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); - let justified_block = self - .store - .get_blinded_block(&self.justified_checkpoint.root) - .map_err(Error::FailedToReadBlock)? - .ok_or(Error::MissingBlock(self.justified_checkpoint.root))? - .deconstruct() - .0; - let max_slot = self - .justified_checkpoint - .epoch - .start_slot(E::slots_per_epoch()); - let (_, state) = self + // Justified state is reasonably useful to cache, it might be finalized soon. + let update_cache = true; + let state = self .store - .get_advanced_hot_state( - self.justified_checkpoint.root, - max_slot, - justified_block.state_root(), - ) + .get_hot_state(&self.justified_state_root, update_cache) .map_err(Error::FailedToReadState)? - .ok_or_else(|| Error::MissingState(justified_block.state_root()))?; + .ok_or(Error::MissingState(self.justified_state_root))?; self.justified_balances = JustifiedBalances::from_justified_state(&state)?; } @@ -332,8 +396,9 @@ where Ok(()) } - fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint) { + fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) { self.unrealized_justified_checkpoint = checkpoint; + self.unrealized_justified_state_root = state_root; } fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint) { @@ -371,18 +436,48 @@ where } } -pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV17; +pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV28; /// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database. -#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)] +#[superstruct( + variants(V17, V28), + variant_attributes(derive(Encode, Decode)), + no_enum +)] pub struct PersistedForkChoiceStore { + /// The balances cache was removed from disk storage in schema V28. + #[superstruct(only(V17))] pub balances_cache: BalancesCacheV8, pub time: Slot, pub finalized_checkpoint: Checkpoint, pub justified_checkpoint: Checkpoint, + /// The justified balances were removed from disk storage in schema V28. + #[superstruct(only(V17))] pub justified_balances: Vec, + /// The justified state root is stored so that it can be used to load the justified balances. + #[superstruct(only(V28))] + pub justified_state_root: Hash256, pub unrealized_justified_checkpoint: Checkpoint, + #[superstruct(only(V28))] + pub unrealized_justified_state_root: Hash256, pub unrealized_finalized_checkpoint: Checkpoint, pub proposer_boost_root: Hash256, pub equivocating_indices: BTreeSet, } + +// Convert V28 to V17 by adding balances and removing justified state roots. +impl From<(PersistedForkChoiceStoreV28, JustifiedBalances)> for PersistedForkChoiceStoreV17 { + fn from((v28, balances): (PersistedForkChoiceStoreV28, JustifiedBalances)) -> Self { + Self { + balances_cache: Default::default(), + time: v28.time, + finalized_checkpoint: v28.finalized_checkpoint, + justified_checkpoint: v28.justified_checkpoint, + justified_balances: balances.effective_balances, + unrealized_justified_checkpoint: v28.unrealized_justified_checkpoint, + unrealized_finalized_checkpoint: v28.unrealized_finalized_checkpoint, + proposer_boost_root: v28.proposer_boost_root, + equivocating_indices: v28.equivocating_indices, + } + } +} diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 56b13b0b77..a923d657a8 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -12,15 +12,15 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use fork_choice::ExecutionStatus; use lru::LruCache; use once_cell::sync::OnceCell; +use safe_arith::SafeArith; use smallvec::SmallVec; use state_processing::state_advance::partial_state_advance; -use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; +use tracing::instrument; +use typenum::Unsigned; use types::non_zero_usize::new_non_zero_usize; -use types::{ - BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot, Unsigned, -}; +use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot}; /// The number of sets of proposer indices that should be cached. const CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); @@ -51,6 +51,34 @@ pub struct EpochBlockProposers { pub(crate) proposers: SmallVec<[usize; TYPICAL_SLOTS_PER_EPOCH]>, } +impl EpochBlockProposers { + pub fn new(epoch: Epoch, fork: Fork, proposers: Vec) -> Self { + Self { + epoch, + fork, + proposers: proposers.into(), + } + } + + pub fn get_slot(&self, slot: Slot) -> Result { + let epoch = slot.epoch(E::slots_per_epoch()); + if epoch == self.epoch { + self.proposers + .get(slot.as_usize() % E::SlotsPerEpoch::to_usize()) + .map(|&index| Proposer { + index, + fork: self.fork, + }) + .ok_or(BeaconChainError::ProposerCacheOutOfBounds { slot, epoch }) + } else { + Err(BeaconChainError::ProposerCacheWrongEpoch { + request_epoch: epoch, + cache_epoch: self.epoch, + }) + } + } +} + /// A cache to store the proposers for some epoch. /// /// See the module-level documentation for more information. @@ -76,23 +104,8 @@ impl BeaconProposerCache { ) -> Option { let epoch = slot.epoch(E::slots_per_epoch()); let key = (epoch, shuffling_decision_block); - let cache_opt = self.cache.get(&key).and_then(|cell| cell.get()); - if let Some(cache) = cache_opt { - // This `if` statement is likely unnecessary, but it feels like good practice. - if epoch == cache.epoch { - cache - .proposers - .get(slot.as_usize() % E::SlotsPerEpoch::to_usize()) - .map(|&index| Proposer { - index, - fork: cache.fork, - }) - } else { - None - } - } else { - None - } + let cache = self.cache.get(&key)?.get()?; + cache.get_slot::(slot).ok() } /// As per `Self::get_slot`, but returns all proposers in all slots for the given `epoch`. @@ -142,11 +155,7 @@ impl BeaconProposerCache { ) -> Result<(), BeaconStateError> { let key = (epoch, shuffling_decision_block); if !self.cache.contains(&key) { - let epoch_proposers = EpochBlockProposers { - epoch, - fork, - proposers: proposers.into(), - }; + let epoch_proposers = EpochBlockProposers::new(epoch, fork, proposers); self.cache .put(key, Arc::new(OnceCell::with_value(epoch_proposers))); } @@ -156,10 +165,17 @@ impl BeaconProposerCache { } /// Compute the proposer duties using the head state without cache. +/// +/// Return: +/// - Proposer indices. +/// - True dependent root. +/// - Legacy dependent root (last block of epoch `N - 1`). +/// - Head execution status. +/// - Fork at `request_epoch`. pub fn compute_proposer_duties_from_head( request_epoch: Epoch, chain: &BeaconChain, -) -> Result<(Vec, Hash256, ExecutionStatus, Fork), BeaconChainError> { +) -> Result<(Vec, Hash256, Hash256, ExecutionStatus, Fork), BeaconChainError> { // Atomically collect information about the head whilst holding the canonical head `Arc` as // short as possible. let (mut state, head_state_root, head_block_root) = { @@ -178,21 +194,41 @@ pub fn compute_proposer_duties_from_head( .ok_or(BeaconChainError::HeadMissingFromForkChoice(head_block_root))?; // Advance the state into the requested epoch. - ensure_state_is_in_epoch(&mut state, head_state_root, request_epoch, &chain.spec)?; + ensure_state_can_determine_proposers_for_epoch( + &mut state, + head_state_root, + request_epoch, + &chain.spec, + )?; let indices = state - .get_beacon_proposer_indices(&chain.spec) + .get_beacon_proposer_indices(request_epoch, &chain.spec) .map_err(BeaconChainError::from)?; let dependent_root = state - // The only block which decides its own shuffling is the genesis block. - .proposer_shuffling_decision_root(chain.genesis_block_root) + .proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec) .map_err(BeaconChainError::from)?; - Ok((indices, dependent_root, execution_status, state.fork())) + // This is only required because the V1 proposer duties endpoint spec wasn't updated for Fulu. We + // can delete this once the V1 endpoint is deprecated at the Glamsterdam fork. + let legacy_dependent_root = state + .legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root) + .map_err(BeaconChainError::from)?; + + // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have advanced + // the state completely into the new epoch. + let fork = chain.spec.fork_at_epoch(request_epoch); + + Ok(( + indices, + dependent_root, + legacy_dependent_root, + execution_status, + fork, + )) } -/// If required, advance `state` to `target_epoch`. +/// If required, advance `state` to the epoch required to determine proposer indices in `target_epoch`. /// /// ## Details /// @@ -200,22 +236,32 @@ pub fn compute_proposer_duties_from_head( /// - No-op if `state.current_epoch() == target_epoch`. /// - It must be the case that `state.canonical_root() == state_root`, but this function will not /// check that. -pub fn ensure_state_is_in_epoch( +#[instrument(skip_all, fields(?state_root, %target_epoch, state_slot = %state.slot()), level = "debug")] +pub fn ensure_state_can_determine_proposers_for_epoch( state: &mut BeaconState, state_root: Hash256, target_epoch: Epoch, spec: &ChainSpec, ) -> Result<(), BeaconChainError> { - match state.current_epoch().cmp(&target_epoch) { - // Protects against an inconsistent slot clock. - Ordering::Greater => Err(BeaconStateError::SlotOutOfBounds.into()), - // The state needs to be advanced. - Ordering::Less => { - let target_slot = target_epoch.start_slot(E::slots_per_epoch()); - partial_state_advance(state, Some(state_root), target_slot, spec) - .map_err(BeaconChainError::from) - } - // The state is suitable, nothing to do. - Ordering::Equal => Ok(()), + // The decision slot is the end of an epoch, so we add 1 to reach the first slot of the epoch + // at which the shuffling is determined. + let minimum_slot = spec + .proposer_shuffling_decision_slot::(target_epoch) + .safe_add(1)?; + let minimum_epoch = minimum_slot.epoch(E::slots_per_epoch()); + + // Before and after Fulu, the oldest epoch reachable from a state at epoch N is epoch N itself, + // i.e. we can never "look back". + let maximum_epoch = target_epoch; + + if state.current_epoch() > maximum_epoch { + Err(BeaconStateError::SlotOutOfBounds.into()) + } else if state.current_epoch() >= minimum_epoch { + Ok(()) + } else { + // State's current epoch is less than the minimum epoch. + // Advance the state up to the minimum epoch. + partial_state_advance(state, Some(state_root), minimum_slot, spec) + .map_err(BeaconChainError::from) } } diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index 6fe710f41a..874673b52e 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -1,20 +1,19 @@ -use derivative::Derivative; +use educe::Educe; use slot_clock::SlotClock; use std::marker::PhantomData; use std::sync::Arc; use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; use crate::block_verification::{ - cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info, - BlockSlashInfo, + BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info, }; use crate::kzg_utils::{validate_blob, validate_blobs}; -use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe}; -use crate::{metrics, BeaconChainError}; +use crate::observed_data_sidecars::{ObservationStrategy, Observe}; +use crate::{BeaconChainError, metrics}; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use ssz_derive::{Decode, Encode}; use std::time::Duration; -use tracing::debug; +use tracing::{debug, instrument}; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; use types::{ @@ -96,7 +95,7 @@ pub enum GossipBlobError { /// ## Peer scoring /// /// We cannot process the blob without validating its parent, the peer isn't necessarily faulty. - BlobParentUnknown { parent_root: Hash256 }, + ParentUnknown { parent_root: Hash256 }, /// Invalid kzg commitment inclusion proof /// ## Peer scoring @@ -166,6 +165,16 @@ pub struct GossipVerifiedBlob, } +impl Clone for GossipVerifiedBlob { + fn clone(&self) -> Self { + Self { + block_root: self.block_root, + blob: self.blob.clone(), + _phantom: PhantomData, + } + } +} + impl GossipVerifiedBlob { pub fn new( blob: Arc>, @@ -236,8 +245,8 @@ impl GossipVerifiedBlob { /// Wrapper over a `BlobSidecar` for which we have completed kzg verification. /// i.e. `verify_blob_kzg_proof(blob, commitment, proof) == true`. -#[derive(Debug, Derivative, Clone, Encode, Decode)] -#[derivative(PartialEq, Eq)] +#[derive(Debug, Educe, Clone, Encode, Decode)] +#[educe(PartialEq, Eq)] #[ssz(struct_behaviour = "transparent")] pub struct KzgVerifiedBlob { blob: Arc>, @@ -294,6 +303,14 @@ impl KzgVerifiedBlob { seen_timestamp: Duration::from_secs(0), } } + /// Mark a blob as KZG verified. Caller must ONLY use this on blob sidecars constructed + /// from EL blobs. + pub fn from_execution_verified(blob: Arc>, seen_timestamp: Duration) -> Self { + Self { + blob, + seen_timestamp, + } + } } /// Complete kzg verification for a `BlobSidecar`. @@ -335,21 +352,9 @@ impl KzgVerifiedBlobList { } /// Create a `KzgVerifiedBlobList` from `blobs` that are already KZG verified. - /// - /// This should be used with caution, as used incorrectly it could result in KZG verification - /// being skipped and invalid blobs being deemed valid. - pub fn from_verified>>>( - blobs: I, - seen_timestamp: Duration, - ) -> Self { + pub fn from_verified>>(blobs: I) -> Self { Self { - verified_blobs: blobs - .into_iter() - .map(|blob| KzgVerifiedBlob { - blob, - seen_timestamp, - }) - .collect(), + verified_blobs: blobs.into_iter().collect(), } } } @@ -368,6 +373,7 @@ impl IntoIterator for KzgVerifiedBlobList { /// /// Note: This function should be preferred over calling `verify_kzg_for_blob` /// in a loop since this function kzg verifies a list of blobs more efficiently. +#[instrument(skip_all, level = "debug")] pub fn verify_kzg_for_blob_list<'a, E: EthSpec, I>( blob_iter: I, kzg: &'a Kzg, @@ -467,7 +473,7 @@ pub fn validate_blob_sidecar_for_gossip(proposer_shuffling_root, blob_slot); - - let (proposer_index, fork) = if let Some(proposer) = proposer_opt { - (proposer.index, proposer.fork) - } else { - debug!( - %block_root, - %blob_index, - "Proposer shuffling cache miss for blob verification" - ); - let (parent_state_root, mut parent_state) = chain - .store - .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .ok_or_else(|| { - BeaconChainError::DBInconsistent(format!( - "Missing state for parent block {block_parent_root:?}", - )) - })?; - - let state = cheap_state_advance_to_obtain_committees::<_, GossipBlobError>( - &mut parent_state, - Some(parent_state_root), - blob_slot, - &chain.spec, - )?; - - let proposers = state.get_beacon_proposer_indices(&chain.spec)?; - let proposer_index = *proposers - .get(blob_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize) - .ok_or_else(|| BeaconChainError::NoProposerForSlot(blob_slot))?; - - // Prime the proposer shuffling cache with the newly-learned value. - chain.beacon_proposer_cache.lock().insert( - blob_epoch, - proposer_shuffling_root, - proposers, - state.fork(), - )?; - (proposer_index, state.fork()) - }; + let proposer = chain.with_proposer_cache( + proposer_shuffling_root, + blob_epoch, + |proposers| proposers.get_slot::(blob_slot), + || { + debug!( + %block_root, + index = %blob_index, + "Proposer shuffling cache miss for blob verification" + ); + chain + .store + .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) + .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? + .ok_or_else(|| { + GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent( + format!("Missing state for parent block {block_parent_root:?}",), + ))) + }) + }, + )?; + let proposer_index = proposer.index; + let fork = proposer.fork; // Signature verify the signed block header. let signature_is_valid = { @@ -595,21 +574,7 @@ pub fn validate_blob_sidecar_for_gossip GossipVerifiedBlob { - pub fn observe( - self, - chain: &BeaconChain, - ) -> Result, GossipBlobError> { - observe_gossip_blob(&self.blob.blob, chain)?; - Ok(GossipVerifiedBlob { - block_root: self.block_root, - blob: self.blob, - _phantom: PhantomData, - }) - } -} - -fn observe_gossip_blob( +pub fn observe_gossip_blob( blob_sidecar: &BlobSidecar, chain: &BeaconChain, ) -> Result<(), GossipBlobError> { diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs index 0809ce34ef..f3924bb473 100644 --- a/beacon_node/beacon_chain/src/block_reward.rs +++ b/beacon_node/beacon_chain/src/block_reward.rs @@ -1,7 +1,7 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::lighthouse::{AttestationRewards, BlockReward, BlockRewardMeta}; use operation_pool::{ - AttMaxCover, MaxCover, RewardCache, SplitAttestation, PROPOSER_REWARD_DENOMINATOR, + AttMaxCover, MaxCover, PROPOSER_REWARD_DENOMINATOR, RewardCache, SplitAttestation, }; use state_processing::{ common::get_attesting_indices_from_state, diff --git a/beacon_node/beacon_chain/src/block_times_cache.rs b/beacon_node/beacon_chain/src/block_times_cache.rs index bd1adb7e40..e8d4c75dce 100644 --- a/beacon_node/beacon_chain/src/block_times_cache.rs +++ b/beacon_node/beacon_chain/src/block_times_cache.rs @@ -294,7 +294,7 @@ impl BlockTimesCache { #[cfg(test)] mod test { use super::*; - use types::FixedBytesExtended; + use fixed_bytes::FixedBytesExtended; #[test] fn observed_time_uses_minimum() { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 26bf872392..bca8d2bc57 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -53,20 +53,21 @@ use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, RpcBlock}; use crate::data_availability_checker::{AvailabilityCheckError, MaybeAvailableBlock}; use crate::data_column_verification::GossipDataColumnError; -use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::execution_payload::{ - validate_execution_payload_for_gossip, validate_merge_block, AllowOptimisticImport, - NotifyExecutionLayer, PayloadNotifier, + AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, + validate_execution_payload_for_gossip, validate_merge_block, }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, beacon_chain::{BeaconForkChoice, ForkChoiceError}, - metrics, BeaconChain, BeaconChainError, BeaconChainTypes, + metrics, }; -use derivative::Derivative; +use bls::{PublicKey, PublicKeyBytes}; +use educe::Educe; use eth2::types::{BlockGossip, EventKind}; use execution_layer::PayloadStatus; pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus}; @@ -79,26 +80,26 @@ use ssz::Encode; use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::{errors::IntoWithIndex, is_merge_transition_block}; use state_processing::{ + AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, + VerifyBlockRoot, block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError}, per_block_processing, per_slot_processing, state_advance::partial_state_advance, - AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, - VerifyBlockRoot, }; use std::borrow::Cow; use std::fmt::Debug; use std::fs; use std::io::Write; use std::sync::Arc; -use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; +use store::{Error as DBError, KeyValueStore}; use strum::AsRefStr; use task_executor::JoinHandle; -use tracing::{debug, error}; +use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; use types::{ - data_column_sidecar::DataColumnSidecarError, BeaconBlockRef, BeaconState, BeaconStateError, - BlobsList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, ExecutionBlockHash, FullPayload, - Hash256, InconsistentFork, KzgProofs, PublicKey, PublicKeyBytes, RelativeEpoch, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, + Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, KzgProofs, + RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + data_column_sidecar::DataColumnSidecarError, }; pub const POS_PANDA_BANNER: &str = r#" @@ -324,6 +325,16 @@ pub enum BlockError { /// We were unable to process this block due to an internal error. It's unclear if the block is /// valid. InternalError(String), + /// The number of kzg commitments in the block exceed the max allowed blobs per block for + /// the block's epoch. + /// + /// ## Peer scoring + /// + /// This block is invalid and the peer should be penalised. + InvalidBlobCount { + max_blobs_at_epoch: usize, + block: usize, + }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -609,6 +620,7 @@ pub(crate) fn process_block_slash_info( mut chain_segment: Vec<(Hash256, RpcBlock)>, chain: &BeaconChain, @@ -678,14 +690,13 @@ pub fn signature_verify_chain_segment( /// A wrapper around a `SignedBeaconBlock` that indicates it has been approved for re-gossiping on /// the p2p network. -#[derive(Derivative)] -#[derivative(Debug(bound = "T: BeaconChainTypes"))] +#[derive(Educe)] +#[educe(Debug(bound(T: BeaconChainTypes)))] pub struct GossipVerifiedBlock { pub block: Arc>, pub block_root: Hash256, parent: Option>, consensus_context: ConsensusContext, - custody_columns_count: usize, } /// A wrapper around a `SignedBeaconBlock` that indicates that all signatures (except the deposit @@ -721,7 +732,6 @@ pub trait IntoGossipVerifiedBlock: Sized { fn into_gossip_verified_block( self, chain: &BeaconChain, - custody_columns_count: usize, ) -> Result, BlockError>; fn inner_block(&self) -> Arc>; } @@ -730,7 +740,6 @@ impl IntoGossipVerifiedBlock for GossipVerifiedBlock fn into_gossip_verified_block( self, _chain: &BeaconChain, - _custody_columns_count: usize, ) -> Result, BlockError> { Ok(self) } @@ -743,9 +752,8 @@ impl IntoGossipVerifiedBlock for Arc, - custody_columns_count: usize, ) -> Result, BlockError> { - GossipVerifiedBlock::new(self, chain, custody_columns_count) + GossipVerifiedBlock::new(self, chain) } fn inner_block(&self) -> Arc> { @@ -785,6 +793,7 @@ pub fn build_blob_data_column_sidecars( /// /// Used to allow functions to accept blocks at various stages of verification. pub trait IntoExecutionPendingBlock: Sized { + #[instrument(skip_all, level = "debug")] fn into_execution_pending_block( self, block_root: Hash256, @@ -818,10 +827,10 @@ impl GossipVerifiedBlock { /// on the p2p network. /// /// Returns an error if the block is invalid, or if the block was unable to be verified. + #[instrument(name = "verify_gossip_block", skip_all, fields(block_root = tracing::field::Empty))] pub fn new( block: Arc>, chain: &BeaconChain, - custody_columns_count: usize, ) -> Result { // If the block is valid for gossip we don't supply it to the slasher here because // we assume it will be transformed into a fully verified block. We *do* need to supply @@ -831,14 +840,17 @@ impl GossipVerifiedBlock { // The `SignedBeaconBlock` and `SignedBeaconBlockHeader` have the same canonical root, // but it's way quicker to calculate root of the header since the hash of the tree rooted // at `BeaconBlockBody` is already computed in the header. - Self::new_without_slasher_checks(block, &header, chain, custody_columns_count).map_err( - |e| { + Self::new_without_slasher_checks(block, &header, chain) + .map_err(|e| { process_block_slash_info::<_, BlockError>( chain, BlockSlashInfo::from_early_error_block(header, e), ) - }, - ) + }) + .inspect(|block| { + let current_span = Span::current(); + current_span.record("block_root", block.block_root.to_string()); + }) } /// As for new, but doesn't pass the block to the slasher. @@ -846,7 +858,6 @@ impl GossipVerifiedBlock { block: Arc>, block_header: &SignedBeaconBlockHeader, chain: &BeaconChain, - custody_columns_count: usize, ) -> Result { // Ensure the block is the correct structure for the fork at `block.slot()`. block @@ -865,6 +876,21 @@ impl GossipVerifiedBlock { }); } + // Do not gossip blocks that claim to contain more blobs than the max allowed + // at the given block epoch. + if let Ok(commitments) = block.message().body().blob_kzg_commitments() { + let max_blobs_at_epoch = chain + .spec + .max_blobs_per_block(block.slot().epoch(T::EthSpec::slots_per_epoch())) + as usize; + if commitments.len() > max_blobs_at_epoch { + return Err(BlockError::InvalidBlobCount { + max_blobs_at_epoch, + block: commitments.len(), + }); + } + } + let block_root = get_block_header_root(block_header); // Do not gossip a block from a finalized slot. @@ -923,60 +949,33 @@ impl GossipVerifiedBlock { } let proposer_shuffling_decision_block = - if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == block_epoch { - parent_block - .next_epoch_shuffling_id - .shuffling_decision_block - } else { - parent_block.root - }; + parent_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec); - // We assign to a variable instead of using `if let Some` directly to ensure we drop the - // write lock before trying to acquire it again in the `else` clause. - let proposer_opt = chain - .beacon_proposer_cache - .lock() - .get_slot::(proposer_shuffling_decision_block, block.slot()); - let (expected_proposer, fork, parent, block) = if let Some(proposer) = proposer_opt { - // The proposer index was cached and we can return it without needing to load the - // parent. - (proposer.index, proposer.fork, None, block) - } else { - // The proposer index was *not* cached and we must load the parent in order to determine - // the proposer index. - let (mut parent, block) = load_parent(block, chain)?; - - debug!( - parent_root = ?parent.beacon_block_root, - parent_slot = %parent.beacon_block.slot(), - ?block_root, - block_slot = %block.slot(), - "Proposer shuffling cache miss" - ); - - // The state produced is only valid for determining proposer/attester shuffling indices. - let state = cheap_state_advance_to_obtain_committees::<_, BlockError>( - &mut parent.pre_state, - parent.beacon_state_root, - block.slot(), - &chain.spec, - )?; - - let proposers = state.get_beacon_proposer_indices(&chain.spec)?; - let proposer_index = *proposers - .get(block.slot().as_usize() % T::EthSpec::slots_per_epoch() as usize) - .ok_or_else(|| BeaconChainError::NoProposerForSlot(block.slot()))?; - - // Prime the proposer shuffling cache with the newly-learned value. - chain.beacon_proposer_cache.lock().insert( - block_epoch, - proposer_shuffling_decision_block, - proposers, - state.fork(), - )?; - - (proposer_index, state.fork(), Some(parent), block) - }; + let block_slot = block.slot(); + let mut opt_parent = None; + let proposer = chain.with_proposer_cache::<_, BlockError>( + proposer_shuffling_decision_block, + block_epoch, + |proposers| proposers.get_slot::(block_slot), + || { + // The proposer index was *not* cached and we must load the parent in order to + // determine the proposer index. + let (mut parent, _) = load_parent(block.clone(), chain)?; + let parent_state_root = if let Some(state_root) = parent.beacon_state_root { + state_root + } else { + // This is potentially a little inefficient, although we are likely to need + // the state's hash eventually (if the block is valid), and we are also likely + // to already have the hash cached (if fetched from the state cache). + parent.pre_state.canonical_root()? + }; + let parent_state = parent.pre_state.clone(); + opt_parent = Some(parent); + Ok((parent_state_root, parent_state)) + }, + )?; + let expected_proposer = proposer.index; + let fork = proposer.fork; let signature_is_valid = { let pubkey_cache = get_validator_pubkey_cache(chain)?; @@ -1018,7 +1017,7 @@ impl GossipVerifiedBlock { return Err(BlockError::Slashable); } SeenBlock::Duplicate => { - return Err(BlockError::DuplicateImportStatusUnknown(block_root)) + return Err(BlockError::DuplicateImportStatusUnknown(block_root)); } SeenBlock::UniqueNonSlashable => {} }; @@ -1034,13 +1033,13 @@ impl GossipVerifiedBlock { validate_execution_payload_for_gossip(&parent_block, block.message(), chain)?; // Beacon API block_gossip events - if let Some(event_handler) = chain.event_handler.as_ref() { - if event_handler.has_block_gossip_subscribers() { - event_handler.register(EventKind::BlockGossip(Box::new(BlockGossip { - slot: block.slot(), - block: block_root, - }))); - } + if let Some(event_handler) = chain.event_handler.as_ref() + && event_handler.has_block_gossip_subscribers() + { + event_handler.register(EventKind::BlockGossip(Box::new(BlockGossip { + slot: block.slot(), + block: block_root, + }))); } // Having checked the proposer index and the block root we can cache them. @@ -1051,9 +1050,8 @@ impl GossipVerifiedBlock { Ok(Self { block, block_root, - parent, + parent: opt_parent, consensus_context, - custody_columns_count, }) } @@ -1064,6 +1062,11 @@ impl GossipVerifiedBlock { impl IntoExecutionPendingBlock for GossipVerifiedBlock { /// Completes verification of the wrapped `block`. + #[instrument( + name = "gossip_block_into_execution_pending_block_slashable", + level = "debug" + skip_all, + )] fn into_execution_pending_block_slashable( self, block_root: Hash256, @@ -1162,13 +1165,14 @@ impl SignatureVerifiedBlock { block_root: Hash256, chain: &BeaconChain, ) -> Result> { - let header = block.signed_block_header(); + let arc_block = block.block_cloned(); Self::new(block, block_root, chain) - .map_err(|e| BlockSlashInfo::from_early_error_block(header, e)) + .map_err(|e| BlockSlashInfo::from_early_error_block(arc_block.signed_block_header(), e)) } /// Finishes signature verification on the provided `GossipVerifedBlock`. Does not re-verify /// the proposer signature. + #[instrument(skip_all, level = "debug")] pub fn from_gossip_verified_block( from: GossipVerifiedBlock, chain: &BeaconChain, @@ -1196,21 +1200,20 @@ impl SignatureVerifiedBlock { signature_verifier .include_all_signatures_except_proposal(block.as_ref(), &mut consensus_context)?; - if signature_verifier.verify().is_ok() { - Ok(Self { + let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify()); + match result { + Ok(_) => Ok(Self { block: MaybeAvailableBlock::AvailabilityPending { block_root: from.block_root, block, - custody_columns_count: from.custody_columns_count, }, block_root: from.block_root, parent: Some(parent), consensus_context, - }) - } else { - Err(BlockError::InvalidSignature( + }), + Err(_) => Err(BlockError::InvalidSignature( InvalidSignature::BlockBodySignatures, - )) + )), } } @@ -1219,9 +1222,13 @@ impl SignatureVerifiedBlock { from: GossipVerifiedBlock, chain: &BeaconChain, ) -> Result> { - let header = from.block.signed_block_header(); - Self::from_gossip_verified_block(from, chain) - .map_err(|e| BlockSlashInfo::from_early_error_block(header, e)) + let block = from.block.clone(); + Self::from_gossip_verified_block(from, chain).map_err(|e| { + // Lazily create the header from the block in case of error. Computing the header + // involves some hashing and takes ~13ms which we DO NOT want to do on the hot path of + // block processing (prior to sending newPayload pre-Gloas). + BlockSlashInfo::from_early_error_block(block.signed_block_header(), e) + }) } pub fn block_root(&self) -> Hash256 { @@ -1235,18 +1242,23 @@ impl SignatureVerifiedBlock { impl IntoExecutionPendingBlock for SignatureVerifiedBlock { /// Completes verification of the wrapped `block`. + #[instrument( + name = "sig_verified_block_into_execution_pending_block_slashable", + level = "debug" + skip_all, + )] fn into_execution_pending_block_slashable( self, block_root: Hash256, chain: &Arc>, notify_execution_layer: NotifyExecutionLayer, ) -> Result, BlockSlashInfo> { - let header = self.block.signed_block_header(); + let arc_block = self.block.block_cloned(); let (parent, block) = if let Some(parent) = self.parent { (parent, self.block) } else { load_parent(self.block, chain) - .map_err(|e| BlockSlashInfo::SignatureValid(header.clone(), e))? + .map_err(|e| BlockSlashInfo::SignatureValid(arc_block.signed_block_header(), e))? }; ExecutionPendingBlock::from_signature_verified_components( @@ -1257,7 +1269,7 @@ impl IntoExecutionPendingBlock for SignatureVerifiedBloc chain, notify_execution_layer, ) - .map_err(|e| BlockSlashInfo::SignatureValid(header, e)) + .map_err(|e| BlockSlashInfo::SignatureValid(arc_block.signed_block_header(), e)) } fn block(&self) -> &SignedBeaconBlock { @@ -1272,6 +1284,11 @@ impl IntoExecutionPendingBlock for SignatureVerifiedBloc impl IntoExecutionPendingBlock for RpcBlock { /// Verifies the `SignedBeaconBlock` by first transforming it into a `SignatureVerifiedBlock` /// and then using that implementation of `IntoExecutionPendingBlock` to complete verification. + #[instrument( + name = "rpc_block_into_execution_pending_block_slashable", + level = "debug" + skip_all, + )] fn into_execution_pending_block_slashable( self, block_root: Hash256, @@ -1311,6 +1328,7 @@ impl ExecutionPendingBlock { /// verification must be done upstream (e.g., via a `SignatureVerifiedBlock` /// /// Returns an error if the block is invalid, or if the block was unable to be verified. + #[instrument(skip_all, level = "debug")] pub fn from_signature_verified_components( block: MaybeAvailableBlock, block_root: Hash256, @@ -1376,6 +1394,7 @@ impl ExecutionPendingBlock { )?; let is_valid_merge_transition_block = is_merge_transition_block(&parent.pre_state, block.message().body()); + let payload_verification_future = async move { let chain = payload_notifier.chain.clone(); let block = payload_notifier.block.clone(); @@ -1417,7 +1436,8 @@ impl ExecutionPendingBlock { let payload_verification_handle = chain .task_executor .spawn_handle( - payload_verification_future, + payload_verification_future + .instrument(debug_span!("execution_payload_verification")), "execution_payload_verification", ) .ok_or(BeaconChainError::RuntimeShutdown)?; @@ -1451,11 +1471,6 @@ impl ExecutionPendingBlock { .into()); } - let parent_eth1_finalization_data = Eth1FinalizationData { - eth1_data: state.eth1_data().clone(), - eth1_deposit_index: state.eth1_deposit_index(), - }; - // Transition the parent state to the block slot. // // It is important to note that we're using a "pre-state" here, one that has potentially @@ -1476,28 +1491,19 @@ impl ExecutionPendingBlock { // processing, but we get early access to it. let state_root = state.update_tree_hash_cache()?; - // Store the state immediately. - let txn_lock = chain.store.hot_db.begin_rw_transaction(); + // Store the state immediately. States are ONLY deleted on finalization pruning, so + // we won't have race conditions where we should have written a state and didn't. let state_already_exists = chain.store.load_hot_state_summary(&state_root)?.is_some(); - let state_batch = if state_already_exists { + if state_already_exists { // If the state exists, we do not need to re-write it. - vec![] } else { - vec![if state.slot() % T::EthSpec::slots_per_epoch() == 0 { - StoreOp::PutState(state_root, &state) - } else { - StoreOp::PutStateSummary( - state_root, - HotStateSummary::new(&state_root, &state)?, - ) - }] + // Recycle store codepath to create a state summary and store the state / diff + let mut ops = vec![]; + chain.store.store_hot_state(&state_root, &state, &mut ops)?; + chain.store.hot_db.do_atomically(ops)?; }; - chain - .store - .do_atomically_with_block_and_blobs_cache(state_batch)?; - drop(txn_lock); state_root }; @@ -1558,18 +1564,18 @@ impl ExecutionPendingBlock { * If we have block reward listeners, compute the block reward and push it to the * event handler. */ - if let Some(ref event_handler) = chain.event_handler { - if event_handler.has_block_reward_subscribers() { - let mut reward_cache = Default::default(); - let block_reward = chain.compute_block_reward( - block.message(), - block_root, - &state, - &mut reward_cache, - true, - )?; - event_handler.register(EventKind::BlockReward(block_reward)); - } + if let Some(ref event_handler) = chain.event_handler + && event_handler.has_block_reward_subscribers() + { + let mut reward_cache = Default::default(); + let block_reward = chain.compute_block_reward( + block.message(), + block_root, + &state, + &mut reward_cache, + true, + )?; + event_handler.register(EventKind::BlockReward(block_reward)); } /* @@ -1664,7 +1670,6 @@ impl ExecutionPendingBlock { block_root, state, parent_block: parent.beacon_block, - parent_eth1_finalization_data, consensus_context, }, payload_verification_handle, @@ -1854,6 +1859,7 @@ fn verify_parent_block_is_known( /// Returns `Err(BlockError::ParentUnknown)` if the parent is not found, or if an error occurs /// whilst attempting the operation. #[allow(clippy::type_complexity)] +#[instrument(skip_all, level = "debug", fields(parent_root = %block.parent_root()))] fn load_parent>( block: B, chain: &BeaconChain, @@ -1878,6 +1884,7 @@ fn load_parent>( }); } + let _db_read_span = debug_span!("block_processing_db_read").entered(); let db_read_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_READ); let result = { @@ -2031,6 +2038,7 @@ impl BlockBlobError for GossipDataColumnError { /// and `Cow::Borrowed(state)` will be returned. Otherwise, the state will be cloned, cheaply /// advanced and then returned as a `Cow::Owned`. The end result is that the given `state` is never /// mutated to be invalid (in fact, it is never changed beyond a simple committee cache build). +#[instrument(skip_all, fields(?state_root_opt, %block_slot), level = "debug")] pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobError>( state: &'a mut BeaconState, state_root_opt: Option, @@ -2065,9 +2073,10 @@ pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobEr } /// Obtains a read-locked `ValidatorPubkeyCache` from the `chain`. +#[instrument(skip(chain), level = "debug")] pub fn get_validator_pubkey_cache( chain: &BeaconChain, -) -> Result>, BeaconChainError> { +) -> Result>, BeaconChainError> { Ok(chain.validator_pubkey_cache.read()) } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index dab54dc823..5978e97c4d 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -1,16 +1,16 @@ use crate::data_availability_checker::AvailabilityCheckError; pub use crate::data_availability_checker::{AvailableBlock, MaybeAvailableBlock}; use crate::data_column_verification::{CustodyDataColumn, CustodyDataColumnList}; -use crate::eth1_finalization_cache::Eth1FinalizationData; -use crate::{get_block_root, PayloadVerificationOutcome}; -use derivative::Derivative; +use crate::{PayloadVerificationOutcome, get_block_root}; +use educe::Educe; +use ssz_types::VariableList; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, - Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A block that has been received over RPC. It has 2 internal variants: @@ -26,12 +26,11 @@ use types::{ /// Note: We make a distinction over blocks received over gossip because /// in a post-deneb world, the blobs corresponding to a given block that are received /// over rpc do not contain the proposer signature for dos resistance. -#[derive(Clone, Derivative)] -#[derivative(Hash(bound = "E: EthSpec"))] +#[derive(Clone, Educe)] +#[educe(Hash(bound(E: EthSpec)))] pub struct RpcBlock { block_root: Hash256, block: RpcBlockInner, - custody_columns_count: usize, } impl Debug for RpcBlock { @@ -45,10 +44,6 @@ impl RpcBlock { self.block_root } - pub fn custody_columns_count(&self) -> usize { - self.custody_columns_count - } - pub fn as_block(&self) -> &SignedBeaconBlock { match &self.block { RpcBlockInner::Block(block) => block, @@ -85,8 +80,8 @@ impl RpcBlock { /// Note: This variant is intentionally private because we want to safely construct the /// internal variants after applying consistency checks to ensure that the block and blobs /// are consistent with respect to each other. -#[derive(Debug, Clone, Derivative)] -#[derivative(Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Educe)] +#[educe(Hash(bound(E: EthSpec)))] enum RpcBlockInner { /// Single block lookup response. This should potentially hit the data availability cache. Block(Arc>), @@ -103,14 +98,12 @@ impl RpcBlock { pub fn new_without_blobs( block_root: Option, block: Arc>, - custody_columns_count: usize, ) -> Self { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); Self { block_root, block: RpcBlockInner::Block(block), - custody_columns_count, } } @@ -152,8 +145,6 @@ impl RpcBlock { Ok(Self { block_root, block: inner, - // Block is before PeerDAS - custody_columns_count: 0, }) } @@ -161,8 +152,6 @@ impl RpcBlock { block_root: Option, block: Arc>, custody_columns: Vec>, - custody_columns_count: usize, - spec: &ChainSpec, ) -> Result { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); @@ -172,17 +161,13 @@ impl RpcBlock { } // Treat empty data column lists as if they are missing. let inner = if !custody_columns.is_empty() { - RpcBlockInner::BlockAndCustodyColumns( - block, - RuntimeVariableList::new(custody_columns, spec.number_of_columns as usize)?, - ) + RpcBlockInner::BlockAndCustodyColumns(block, VariableList::new(custody_columns)?) } else { RpcBlockInner::Block(block) }; Ok(Self { block_root, block: inner, - custody_columns_count, }) } @@ -250,12 +235,10 @@ impl ExecutedBlock { MaybeAvailableBlock::AvailabilityPending { block_root: _, block: pending_block, - custody_columns_count, } => Self::AvailabilityPending(AvailabilityPendingExecutedBlock::new( pending_block, import_data, payload_verification_outcome, - custody_columns_count, )), } } @@ -321,7 +304,6 @@ pub struct AvailabilityPendingExecutedBlock { pub block: Arc>, pub import_data: BlockImportData, pub payload_verification_outcome: PayloadVerificationOutcome, - pub custody_columns_count: usize, } impl AvailabilityPendingExecutedBlock { @@ -329,13 +311,11 @@ impl AvailabilityPendingExecutedBlock { block: Arc>, import_data: BlockImportData, payload_verification_outcome: PayloadVerificationOutcome, - custody_columns_count: usize, ) -> Self { Self { block, import_data, payload_verification_outcome, - custody_columns_count, } } @@ -357,7 +337,6 @@ pub struct BlockImportData { pub block_root: Hash256, pub state: BeaconState, pub parent_block: SignedBeaconBlock>, - pub parent_eth1_finalization_data: Eth1FinalizationData, pub consensus_context: ConsensusContext, } @@ -371,10 +350,6 @@ impl BlockImportData { block_root, state, parent_block, - parent_eth1_finalization_data: Eth1FinalizationData { - eth1_data: <_>::default(), - eth1_deposit_index: 0, - }, consensus_context: ConsensusContext::new(Slot::new(0)), } } @@ -387,7 +362,7 @@ pub trait AsBlock { fn parent_root(&self) -> Hash256; fn state_root(&self) -> Hash256; fn signed_block_header(&self) -> SignedBeaconBlockHeader; - fn message(&self) -> BeaconBlockRef; + fn message(&self) -> BeaconBlockRef<'_, E>; fn as_block(&self) -> &SignedBeaconBlock; fn block_cloned(&self) -> Arc>; fn canonical_root(&self) -> Hash256; @@ -414,7 +389,7 @@ impl AsBlock for Arc> { SignedBeaconBlock::signed_block_header(self) } - fn message(&self) -> BeaconBlockRef { + fn message(&self) -> BeaconBlockRef<'_, E> { SignedBeaconBlock::message(self) } @@ -447,7 +422,7 @@ impl AsBlock for MaybeAvailableBlock { fn signed_block_header(&self) -> SignedBeaconBlockHeader { self.as_block().signed_block_header() } - fn message(&self) -> BeaconBlockRef { + fn message(&self) -> BeaconBlockRef<'_, E> { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { @@ -488,7 +463,7 @@ impl AsBlock for AvailableBlock { self.block().signed_block_header() } - fn message(&self) -> BeaconBlockRef { + fn message(&self) -> BeaconBlockRef<'_, E> { self.block().message() } @@ -521,7 +496,7 @@ impl AsBlock for RpcBlock { fn signed_block_header(&self) -> SignedBeaconBlockHeader { self.as_block().signed_block_header() } - fn message(&self) -> BeaconBlockRef { + fn message(&self) -> BeaconBlockRef<'_, E> { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 6f232400ed..7a55499a1b 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1,10 +1,11 @@ +use crate::ChainConfig; +use crate::CustodyContext; use crate::beacon_chain::{ - CanonicalHead, LightClientProducerEvent, BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, OP_POOL_DB_KEY, + BEACON_CHAIN_DB_KEY, CanonicalHead, LightClientProducerEvent, OP_POOL_DB_KEY, }; use crate::beacon_proposer_cache::BeaconProposerCache; +use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; -use crate::eth1_chain::{CachingEth1Backend, SszEth1}; -use crate::eth1_finalization_cache::Eth1FinalizationCache; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; @@ -13,16 +14,16 @@ use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::persisted_beacon_chain::PersistedBeaconChain; +use crate::persisted_custody::load_custody_context; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; -use crate::ChainConfig; use crate::{ - BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, Eth1Chain, - Eth1ChainBackend, ServerSentEventHandler, + BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, ServerSentEventHandler, }; -use eth1::Config as Eth1Config; +use bls::Signature; use execution_layer::ExecutionLayer; +use fixed_bytes::FixedBytesExtended; use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use kzg::Kzg; @@ -34,37 +35,36 @@ use rand::RngCore; use rayon::prelude::*; use slasher::Slasher; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::{per_slot_processing, AllCaches}; +use state_processing::{AllCaches, per_slot_processing}; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use tracing::{debug, error, info}; +use types::data_column_custody_group::CustodyIndex; use types::{ - BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, Checkpoint, DataColumnSidecarList, Epoch, - EthSpec, FixedBytesExtended, Hash256, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, + Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing /// functionality and only exists to satisfy the type system. -pub struct Witness( - PhantomData<(TSlotClock, TEth1Backend, E, THotStore, TColdStore)>, +pub struct Witness( + PhantomData<(TSlotClock, E, THotStore, TColdStore)>, ); -impl BeaconChainTypes - for Witness +impl BeaconChainTypes + for Witness where THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, { type HotStore = THotStore; type ColdStore = TColdStore; type SlotClock = TSlotClock; - type Eth1Chain = TEth1Backend; type EthSpec = E; } @@ -88,7 +88,6 @@ pub struct BeaconChainBuilder { ForkChoice, T::EthSpec>, >, op_pool: Option>, - eth1_chain: Option>, execution_layer: Option>, event_handler: Option>, slot_clock: Option, @@ -105,17 +104,17 @@ pub struct BeaconChainBuilder { kzg: Arc, task_executor: Option, validator_monitor_config: Option, - import_all_data_columns: bool, + node_custody_type: NodeCustodyType, + ordered_custody_column_indices: Option>, rng: Option>, } -impl - BeaconChainBuilder> +impl + BeaconChainBuilder> where THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, { /// Returns a new builder. @@ -131,7 +130,6 @@ where genesis_state_root: None, fork_choice: None, op_pool: None, - eth1_chain: None, execution_layer: None, event_handler: None, slot_clock: None, @@ -146,7 +144,8 @@ where kzg, task_executor: None, validator_monitor_config: None, - import_all_data_columns: false, + node_custody_type: NodeCustodyType::Fullnode, + ordered_custody_column_indices: None, rng: None, } } @@ -224,18 +223,6 @@ where self } - /// Attempt to load an existing eth1 cache from the builder's `Store`. - pub fn get_persisted_eth1_backend(&self) -> Result, String> { - let store = self - .store - .clone() - .ok_or("get_persisted_eth1_backend requires a store.")?; - - store - .get_item::(Ð1_CACHE_DB_KEY) - .map_err(|e| format!("DB error whilst reading eth1 cache: {:?}", e)) - } - /// Returns true if `self.store` contains a persisted beacon chain. pub fn store_contains_beacon_chain(&self) -> Result { let store = self @@ -268,16 +255,15 @@ where .to_string() })?; - let fork_choice = - BeaconChain::>::load_fork_choice( - store.clone(), - ResetPayloadStatuses::always_reset_conditionally( - self.chain_config.always_reset_payload_statuses, - ), - &self.spec, - ) - .map_err(|e| format!("Unable to load fork choice from disk: {:?}", e))? - .ok_or("Fork choice not found in store")?; + let fork_choice = BeaconChain::>::load_fork_choice( + store.clone(), + ResetPayloadStatuses::always_reset_conditionally( + self.chain_config.always_reset_payload_statuses, + ), + &self.spec, + ) + .map_err(|e| format!("Unable to load fork choice from disk: {:?}", e))? + .ok_or("Fork choice not found in store")?; let genesis_block = store .get_blinded_block(&chain.genesis_block_root) @@ -380,21 +366,29 @@ where } /// Starts a new chain from a genesis state. - pub fn genesis_state(mut self, beacon_state: BeaconState) -> Result { + pub fn genesis_state(mut self, mut beacon_state: BeaconState) -> Result { let store = self.store.clone().ok_or("genesis_state requires a store")?; + // Initialize anchor info before attempting to write the genesis state. + // Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent + // historic states from being retained (unless `--reconstruct-historic-states` is set). + let retain_historic_states = self.chain_config.reconstruct_historic_states; + let genesis_beacon_block = genesis_block(&mut beacon_state, &self.spec)?; + self.pending_io_batch.push( + store + .init_anchor_info( + genesis_beacon_block.parent_root(), + genesis_beacon_block.slot(), + Slot::new(0), + retain_historic_states, + ) + .map_err(|e| format!("Failed to initialize genesis anchor: {:?}", e))?, + ); + let (genesis, updated_builder) = self.set_genesis_state(beacon_state)?; self = updated_builder; // Stage the database's metadata fields for atomic storage when `build` is called. - // Since v4.4.0 we will set the anchor with a dummy state upper limit in order to prevent - // historic states from being retained (unless `--reconstruct-historic-states` is set). - let retain_historic_states = self.chain_config.reconstruct_historic_states; - self.pending_io_batch.push( - store - .init_anchor_info(genesis.beacon_block.message(), retain_historic_states) - .map_err(|e| format!("Failed to initialize genesis anchor: {:?}", e))?, - ); self.pending_io_batch.push( store .init_blob_info(genesis.beacon_block.slot()) @@ -406,7 +400,7 @@ where .map_err(|e| format!("Failed to initialize genesis data column info: {:?}", e))?, ); - let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis) + let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, genesis.clone()) .map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?; let current_slot = None; @@ -486,30 +480,66 @@ where // Verify that blobs (if provided) match the block. if let Some(blobs) = &weak_subj_blobs { - let commitments = weak_subj_block - .message() - .body() - .blob_kzg_commitments() - .map_err(|e| format!("Blobs provided but block does not reference them: {e:?}"))?; - if blobs.len() != commitments.len() { - return Err(format!( - "Wrong number of blobs, expected: {}, got: {}", - commitments.len(), - blobs.len() - )); - } - if commitments - .iter() - .zip(blobs.iter()) - .any(|(commitment, blob)| *commitment != blob.kzg_commitment) - { - return Err("Checkpoint blob does not match block commitment".into()); + let fulu_enabled = weak_subj_block.fork_name_unchecked().fulu_enabled(); + if fulu_enabled && blobs.is_empty() { + // Blobs expected for this block, but the checkpoint server is not able to serve them. + // This is expected from Fulu, as only supernodes are able to serve blobs. + // We can consider using backfill to retrieve the data columns from the p2p network, + // but we can ignore this fow now until we have validator custody backfill + // implemented as we'll likely be able to reuse the logic. + // https://github.com/sigp/lighthouse/issues/6837 + } else { + let commitments = weak_subj_block + .message() + .body() + .blob_kzg_commitments() + .map_err(|e| { + format!("Blobs provided but block does not reference them: {e:?}") + })?; + if blobs.len() != commitments.len() { + return Err(format!( + "Wrong number of blobs, expected: {}, got: {}", + commitments.len(), + blobs.len() + )); + } + if commitments + .iter() + .zip(blobs.iter()) + .any(|(commitment, blob)| *commitment != blob.kzg_commitment) + { + return Err("Checkpoint blob does not match block commitment".into()); + } } } - // Set the store's split point *before* storing genesis so that genesis is stored - // immediately in the freezer DB. + debug!( + slot = %weak_subj_slot, + state_root = ?weak_subj_state_root, + block_root = ?weak_subj_block_root, + "Storing split from weak subjectivity state" + ); + + // Set the store's split point *before* storing genesis so that if the genesis state + // is prior to the split slot, it will immediately be stored in the freezer DB. store.set_split(weak_subj_slot, weak_subj_state_root, weak_subj_block_root); + + // It is also possible for the checkpoint state to be equal to the genesis state, in which + // case it will be stored in the hot DB. In this case, we need to ensure the store's anchor + // is initialised prior to storing the state, as the anchor is required for working out + // hdiff storage strategies. + let retain_historic_states = self.chain_config.reconstruct_historic_states; + self.pending_io_batch.push( + store + .init_anchor_info( + weak_subj_block.parent_root(), + weak_subj_block.slot(), + weak_subj_slot, + retain_historic_states, + ) + .map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?, + ); + let (_, updated_builder) = self.set_genesis_state(genesis_state)?; self = updated_builder; @@ -527,6 +557,12 @@ where .cold_db .do_atomically(block_root_batch) .map_err(|e| format!("Error writing frozen block roots: {e:?}"))?; + debug!( + from = %weak_subj_block.slot(), + to_excl = %weak_subj_state.slot(), + block_root = ?weak_subj_block_root, + "Stored frozen block roots at skipped slots" + ); // Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten // about on a crash restart. @@ -537,6 +573,8 @@ where weak_subj_state.clone(), ) .map_err(|e| format!("Failed to set checkpoint state as finalized state: {:?}", e))?; + // Note: post hot hdiff must update the anchor info before attempting to put_state otherwise + // the write will fail if the weak_subj_slot is not aligned with the snapshot moduli. store .put_state(&weak_subj_state_root, &weak_subj_state) .map_err(|e| format!("Failed to store weak subjectivity state: {e:?}"))?; @@ -566,13 +604,7 @@ where // Stage the database's metadata fields for atomic storage when `build` is called. // This prevents the database from restarting in an inconsistent state if the anchor // info or split point is written before the `PersistedBeaconChain`. - let retain_historic_states = self.chain_config.reconstruct_historic_states; self.pending_io_batch.push(store.store_split_in_batch()); - self.pending_io_batch.push( - store - .init_anchor_info(weak_subj_block.message(), retain_historic_states) - .map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?, - ); self.pending_io_batch.push( store .init_blob_info(weak_subj_block.slot()) @@ -584,20 +616,13 @@ where .map_err(|e| format!("Failed to initialize data column info: {:?}", e))?, ); - // Store pruning checkpoint to prevent attempting to prune before the anchor state. - self.pending_io_batch - .push(store.pruning_checkpoint_store_op(Checkpoint { - root: weak_subj_block_root, - epoch: weak_subj_state.slot().epoch(E::slots_per_epoch()), - })); - let snapshot = BeaconSnapshot { beacon_block_root: weak_subj_block_root, beacon_block: Arc::new(weak_subj_block), beacon_state: weak_subj_state, }; - let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot) + let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, snapshot.clone()) .map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?; let fork_choice = ForkChoice::from_anchor( @@ -615,21 +640,25 @@ where Ok(self.empty_op_pool()) } - /// Sets the `BeaconChain` eth1 backend. - pub fn eth1_backend(mut self, backend: Option) -> Self { - self.eth1_chain = backend.map(Eth1Chain::new); - self - } - /// Sets the `BeaconChain` execution layer. pub fn execution_layer(mut self, execution_layer: Option>) -> Self { self.execution_layer = execution_layer; self } - /// Sets whether to require and import all data columns when importing block. - pub fn import_all_data_columns(mut self, import_all_data_columns: bool) -> Self { - self.import_all_data_columns = import_all_data_columns; + /// Sets the node custody type for data column import. + pub fn node_custody_type(mut self, node_custody_type: NodeCustodyType) -> Self { + self.node_custody_type = node_custody_type; + self + } + + /// Sets the ordered custody column indices for this node. + /// This is used to determine the data columns the node is required to custody. + pub fn ordered_custody_column_indices( + mut self, + ordered_custody_column_indices: Vec, + ) -> Self { + self.ordered_custody_column_indices = Some(ordered_custody_column_indices); self } @@ -711,8 +740,7 @@ where #[allow(clippy::type_complexity)] // I think there's nothing to be gained here from a type alias. pub fn build( mut self, - ) -> Result>, String> - { + ) -> Result>, String> { let slot_clock = self .slot_clock .ok_or("Cannot build without a slot_clock.")?; @@ -727,6 +755,9 @@ where .genesis_state_root .ok_or("Cannot build without a genesis state root")?; let validator_monitor_config = self.validator_monitor_config.unwrap_or_default(); + let ordered_custody_column_indices = self + .ordered_custody_column_indices + .ok_or("Cannot build without ordered custody column indices")?; let rng = self.rng.ok_or("Cannot build without an RNG")?; let beacon_proposer_cache: Arc> = <_>::default(); @@ -868,15 +899,16 @@ where // This *must* be stored before constructing the `BeaconChain`, so that its `Drop` instance // doesn't write a `PersistedBeaconChain` without the rest of the batch. self.pending_io_batch.push(BeaconChain::< - Witness, + Witness, >::persist_head_in_batch_standalone( genesis_block_root )); self.pending_io_batch.push(BeaconChain::< - Witness, + Witness, >::persist_fork_choice_in_batch_standalone( - &fork_choice - )); + &fork_choice, + store.get_config(), + ).map_err(|e| format!("Fork choice compression error: {e:?}"))?); store .hot_db .do_atomically(self.pending_io_batch) @@ -886,6 +918,7 @@ where let genesis_time = head_snapshot.beacon_state.genesis_time(); let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot)); let shuffling_cache_size = self.chain_config.shuffling_cache_size; + let complete_blob_backfill = self.chain_config.complete_blob_backfill; // Calculate the weak subjectivity point in which to backfill blocks to. let genesis_backfill_slot = if self.chain_config.genesis_backfill { @@ -914,6 +947,34 @@ where } }; + // Load the persisted custody context from the db and initialize + // the context for this run + let (custody_context, cgc_changed_opt) = if let Some(custody) = + load_custody_context::(store.clone()) + { + let head_epoch = canonical_head + .cached_head() + .head_slot() + .epoch(E::slots_per_epoch()); + CustodyContext::new_from_persisted_custody_context( + custody, + self.node_custody_type, + head_epoch, + ordered_custody_column_indices, + &self.spec, + ) + } else { + ( + CustodyContext::new( + self.node_custody_type, + ordered_custody_column_indices, + &self.spec, + ), + None, + ) + }; + debug!(?custody_context, "Loaded persisted custody context"); + let beacon_chain = BeaconChain { spec: self.spec.clone(), config: self.chain_config, @@ -951,7 +1012,6 @@ where observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), observed_bls_to_execution_changes: <_>::default(), - eth1_chain: self.eth1_chain, execution_layer: self.execution_layer.clone(), genesis_validators_root, genesis_time, @@ -965,15 +1025,13 @@ where shuffling_cache_size, head_shuffling_ids, )), - eth1_finalization_cache: RwLock::new(Eth1FinalizationCache::default()), beacon_proposer_cache, + inclusion_list_cache: <_>::default(), block_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), validator_pubkey_cache: RwLock::new(validator_pubkey_cache), attester_cache: <_>::default(), early_attester_cache: <_>::default(), - inclusion_list_cache: <_>::default(), - reqresp_pre_import_cache: <_>::default(), light_client_server_cache: LightClientServerCache::new(), light_client_server_tx: self.light_client_server_tx, shutdown_sender: self @@ -988,8 +1046,15 @@ where validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, data_availability_checker: Arc::new( - DataAvailabilityChecker::new(slot_clock, self.kzg.clone(), store, self.spec) - .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, + DataAvailabilityChecker::new( + complete_blob_backfill, + slot_clock, + self.kzg.clone(), + store, + Arc::new(custody_context), + self.spec, + ) + .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, ), kzg: self.kzg.clone(), rng: Arc::new(Mutex::new(rng)), @@ -1008,23 +1073,33 @@ where .map_err(|e| format!("Failed to prime attester cache: {:?}", e))?; // Only perform the check if it was configured. - if let Some(wss_checkpoint) = beacon_chain.config.weak_subjectivity_checkpoint { - if let Err(e) = beacon_chain.verify_weak_subjectivity_checkpoint( + if let Some(wss_checkpoint) = beacon_chain.config.weak_subjectivity_checkpoint + && let Err(e) = beacon_chain.verify_weak_subjectivity_checkpoint( wss_checkpoint, head.beacon_block_root, &head.beacon_state, - ) { - crit!( - head_block_root = %head.beacon_block_root, - head_slot = %head.beacon_block.slot(), - finalized_epoch = %head.beacon_state.finalized_checkpoint().epoch, - wss_checkpoint_epoch = %wss_checkpoint.epoch, - error = ?e, - "Weak subjectivity checkpoint verification failed on startup!" - ); - crit!("You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); - return Err(format!("Weak subjectivity verification failed: {:?}", e)); - } + ) + { + crit!( + head_block_root = %head.beacon_block_root, + head_slot = %head.beacon_block.slot(), + finalized_epoch = %head.beacon_state.finalized_checkpoint().epoch, + wss_checkpoint_epoch = %wss_checkpoint.epoch, + error = ?e, + "Weak subjectivity checkpoint verification failed on startup!" + ); + crit!( + "You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network." + ); + return Err(format!("Weak subjectivity verification failed: {:?}", e)); + } + + if let Some(cgc_changed) = cgc_changed_opt { + // Update data column custody info if there's a CGC change from CLI flags. + // This will trigger column backfill. + let cgc_change_effective_slot = + cgc_changed.effective_epoch.start_slot(E::slots_per_epoch()); + beacon_chain.update_data_column_custody_info(Some(cgc_change_effective_slot)); } info!( @@ -1065,35 +1140,11 @@ where } } -impl - BeaconChainBuilder, E, THotStore, TColdStore>> +impl + BeaconChainBuilder> where THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, - TSlotClock: SlotClock + 'static, - E: EthSpec + 'static, -{ - /// Do not use any eth1 backend. The client will not be able to produce beacon blocks. - pub fn no_eth1_backend(self) -> Self { - self.eth1_backend(None) - } - - /// Sets the `BeaconChain` eth1 back-end to produce predictably junk data when producing blocks. - pub fn dummy_eth1_backend(mut self) -> Result { - let backend = CachingEth1Backend::new(Eth1Config::default(), self.spec.clone())?; - - self.eth1_chain = Some(Eth1Chain::new_dummy(backend)); - - Ok(self) - } -} - -impl - BeaconChainBuilder> -where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, { /// Sets the `BeaconChain` slot clock to `TestingSlotClock`. @@ -1193,13 +1244,15 @@ fn build_data_columns_from_blobs( #[cfg(test)] mod test { use super::*; - use crate::test_utils::{get_kzg, EphemeralHarnessType}; + use crate::test_utils::{ + EphemeralHarnessType, generate_data_column_indices_rand_order, get_kzg, + }; use ethereum_hashing::hash; use genesis::{ - generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH, + DEFAULT_ETH1_BLOCK_HASH, generate_deterministic_keypairs, interop_genesis_state, }; - use rand::rngs::StdRng; use rand::SeedableRng; + use rand::rngs::StdRng; use ssz::Encode; use std::time::Duration; use store::config::StoreConfig; @@ -1241,12 +1294,13 @@ mod test { .task_executor(runtime.task_executor.clone()) .genesis_state(genesis_state) .expect("should build state using recent genesis") - .dummy_eth1_backend() - .expect("should build the dummy eth1 backend") .testing_slot_clock(Duration::from_secs(1)) .expect("should configure testing slot clock") .shutdown_sender(shutdown_tx) .rng(Box::new(StdRng::seed_from_u64(42))) + .ordered_custody_column_indices( + generate_data_column_indices_rand_order::(), + ) .build() .expect("should build"); diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 7c3d5b57d0..8b513c9c53 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -34,12 +34,12 @@ use crate::persisted_fork_choice::PersistedForkChoice; use crate::shuffling_cache::BlockShufflingIds; use crate::{ - beacon_chain::{BeaconForkChoice, BeaconStore, OverrideForkchoiceUpdate, FORK_CHOICE_DB_KEY}, + BeaconChain, BeaconChainError as Error, BeaconChainTypes, BeaconSnapshot, + beacon_chain::{BeaconForkChoice, BeaconStore, FORK_CHOICE_DB_KEY, OverrideForkchoiceUpdate}, block_times_cache::BlockTimesCache, events::ServerSentEventHandler, metrics, - validator_monitor::{get_slot_delay_ms, timestamp_now}, - BeaconChain, BeaconChainError as Error, BeaconChainTypes, BeaconSnapshot, + validator_monitor::get_slot_delay_ms, }; use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead}; use fork_choice::{ @@ -47,15 +47,19 @@ use fork_choice::{ ResetPayloadStatuses, }; use itertools::process_results; +use lighthouse_tracing::SPAN_RECOMPUTE_HEAD; use logging::crit; -use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard}; use slot_clock::SlotClock; use state_processing::AllCaches; use std::sync::Arc; use std::time::Duration; -use store::{iter::StateRootsIterator, KeyValueStore, KeyValueStoreOp, StoreItem}; +use store::{ + Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreConfig, iter::StateRootsIterator, +}; use task_executor::{JoinHandle, ShutdownReason}; -use tracing::{debug, error, info, warn}; +use tracing::info_span; +use tracing::{debug, error, info, instrument, warn}; use types::*; /// Simple wrapper around `RwLock` that uses private visibility to prevent any other modules from @@ -73,11 +77,15 @@ impl CanonicalHeadRwLock { Self::from(RwLock::new(item)) } - fn read(&self) -> RwLockReadGuard { + fn read(&self) -> RwLockReadGuard<'_, T> { self.0.read() } - fn write(&self) -> RwLockWriteGuard { + fn upgradable_read(&self) -> RwLockUpgradableReadGuard<'_, T> { + self.0.upgradable_read() + } + + fn write(&self) -> RwLockWriteGuard<'_, T> { self.0.write() } } @@ -374,7 +382,7 @@ impl CanonicalHead { /// /// This function is **not safe** to be public. See the module-level documentation for more /// information about protecting from deadlocks. - fn cached_head_read_lock(&self) -> RwLockReadGuard> { + fn cached_head_read_lock(&self) -> RwLockReadGuard<'_, CachedHead> { self.cached_head.read() } @@ -382,18 +390,28 @@ impl CanonicalHead { /// /// This function is **not safe** to be public. See the module-level documentation for more /// information about protecting from deadlocks. - fn cached_head_write_lock(&self) -> RwLockWriteGuard> { + #[instrument(skip_all)] + fn cached_head_write_lock(&self) -> RwLockWriteGuard<'_, CachedHead> { self.cached_head.write() } /// Access a read-lock for fork choice. - pub fn fork_choice_read_lock(&self) -> RwLockReadGuard> { + pub fn fork_choice_read_lock(&self) -> RwLockReadGuard<'_, BeaconForkChoice> { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_READ_LOCK_AQUIRE_TIMES); self.fork_choice.read() } + /// Access an upgradable read-lock for fork choice. + pub fn fork_choice_upgradable_read_lock( + &self, + ) -> RwLockUpgradableReadGuard<'_, BeaconForkChoice> { + let _timer = metrics::start_timer(&metrics::FORK_CHOICE_UPGRADABLE_READ_LOCK_AQUIRE_TIMES); + self.fork_choice.upgradable_read() + } + /// Access a write-lock for fork choice. - pub fn fork_choice_write_lock(&self) -> RwLockWriteGuard> { + #[instrument(skip_all)] + pub fn fork_choice_write_lock(&self) -> RwLockWriteGuard<'_, BeaconForkChoice> { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES); self.fork_choice.write() } @@ -476,6 +494,7 @@ impl BeaconChain { /// Execute the fork choice algorithm and enthrone the result as the canonical head. /// /// This method replaces the old `BeaconChain::fork_choice` method. + #[instrument(skip_all, level = "debug")] pub async fn recompute_head_at_current_slot(self: &Arc) { match self.slot() { Ok(current_slot) => self.recompute_head_at_slot(current_slot).await, @@ -499,13 +518,21 @@ impl BeaconChain { /// situation can be rectified. We avoid returning an error here so that calling functions /// can't abort block import because an error is returned here. pub async fn recompute_head_at_slot(self: &Arc, current_slot: Slot) { + let span = info_span!( + SPAN_RECOMPUTE_HEAD, + slot = %current_slot + ); + metrics::inc_counter(&metrics::FORK_CHOICE_REQUESTS); let _timer = metrics::start_timer(&metrics::FORK_CHOICE_TIMES); let chain = self.clone(); match self .spawn_blocking_handle( - move || chain.recompute_head_at_slot_internal(current_slot), + move || { + let _guard = span.enter(); + chain.recompute_head_at_slot_internal(current_slot) + }, "recompute_head_internal", ) .await @@ -723,15 +750,14 @@ impl BeaconChain { let old_snapshot = &old_cached_head.snapshot; // If the head changed, perform some updates. - if new_snapshot.beacon_block_root != old_snapshot.beacon_block_root { - if let Err(e) = + if new_snapshot.beacon_block_root != old_snapshot.beacon_block_root + && let Err(e) = self.after_new_head(&old_cached_head, &new_cached_head, new_head_proto_block) - { - crit!( - error = ?e, - "Error updating canonical head" - ); - } + { + crit!( + error = ?e, + "Error updating canonical head" + ); } // Drop the old cache head nice and early to try and free the memory as soon as possible. @@ -741,15 +767,14 @@ impl BeaconChain { // // The `after_finalization` function will take a write-lock on `fork_choice`, therefore it // is a dead-lock risk to hold any other lock on fork choice at this point. - if new_view.finalized_checkpoint != old_view.finalized_checkpoint { - if let Err(e) = + if new_view.finalized_checkpoint != old_view.finalized_checkpoint + && let Err(e) = self.after_finalization(&new_cached_head, new_view, finalized_proto_block) - { - crit!( - error = ?e, - "Error updating finalization" - ); - } + { + crit!( + error = ?e, + "Error updating finalization" + ); } // The execution layer updates might attempt to take a write-lock on fork choice, so it's @@ -765,6 +790,7 @@ impl BeaconChain { } /// Perform updates to caches and other components after the canonical head has been changed. + #[instrument(skip_all)] fn after_new_head( self: &Arc, old_cached_head: &CachedHead, @@ -808,7 +834,7 @@ impl BeaconChain { let head_slot = new_snapshot.beacon_state.slot(); let dependent_root = new_snapshot .beacon_state - .proposer_shuffling_decision_root(self.genesis_block_root); + .attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Next); let prev_dependent_root = new_snapshot .beacon_state .attester_shuffling_decision_root(self.genesis_block_root, RelativeEpoch::Current); @@ -877,23 +903,22 @@ impl BeaconChain { } // Register a server-sent-event for a reorg (if necessary). - if let Some(depth) = reorg_distance { - if let Some(event_handler) = self + if let Some(depth) = reorg_distance + && let Some(event_handler) = self .event_handler .as_ref() .filter(|handler| handler.has_reorg_subscribers()) - { - event_handler.register(EventKind::ChainReorg(SseChainReorg { - slot: head_slot, - depth: depth.as_u64(), - old_head_block: old_snapshot.beacon_block_root, - old_head_state: old_snapshot.beacon_state_root(), - new_head_block: new_snapshot.beacon_block_root, - new_head_state: new_snapshot.beacon_state_root(), - epoch: head_slot.epoch(T::EthSpec::slots_per_epoch()), - execution_optimistic: new_head_is_optimistic, - })); - } + { + event_handler.register(EventKind::ChainReorg(SseChainReorg { + slot: head_slot, + depth: depth.as_u64(), + old_head_block: old_snapshot.beacon_block_root, + old_head_state: old_snapshot.beacon_state_root(), + new_head_block: new_snapshot.beacon_block_root, + new_head_state: new_snapshot.beacon_state_root(), + epoch: head_slot.epoch(T::EthSpec::slots_per_epoch()), + execution_optimistic: new_head_is_optimistic, + })); } Ok(()) @@ -904,6 +929,7 @@ impl BeaconChain { /// /// This function will take a write-lock on `canonical_head.fork_choice`, therefore it would be /// unwise to hold any lock on fork choice while calling this function. + #[instrument(skip_all)] fn after_finalization( self: &Arc, new_cached_head: &CachedHead, @@ -916,13 +942,6 @@ impl BeaconChain { .execution_status .is_optimistic_or_invalid(); - self.op_pool.prune_all( - &new_snapshot.beacon_block, - &new_snapshot.beacon_state, - self.epoch()?, - &self.spec, - ); - self.observed_block_producers.write().prune( new_view .finalized_checkpoint @@ -947,23 +966,23 @@ impl BeaconChain { self.attester_cache .prune_below(new_view.finalized_checkpoint.epoch); - if let Some(event_handler) = self.event_handler.as_ref() { - if event_handler.has_finalized_subscribers() { - event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { - epoch: new_view.finalized_checkpoint.epoch, - block: new_view.finalized_checkpoint.root, - // Provide the state root of the latest finalized block, rather than the - // specific state root at the first slot of the finalized epoch (which - // might be a skip slot). - state: finalized_proto_block.state_root, - execution_optimistic: finalized_block_is_optimistic, - })); - } + if let Some(event_handler) = self.event_handler.as_ref() + && event_handler.has_finalized_subscribers() + { + event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { + epoch: new_view.finalized_checkpoint.epoch, + block: new_view.finalized_checkpoint.root, + // Provide the state root of the latest finalized block, rather than the + // specific state root at the first slot of the finalized epoch (which + // might be a skip slot). + state: finalized_proto_block.state_root, + execution_optimistic: finalized_block_is_optimistic, + })); } - // The store migration task requires the *state at the slot of the finalized epoch*, - // rather than the state of the latest finalized block. These two values will only - // differ when the first slot of the finalized epoch is a skip slot. + // The store migration task and op pool pruning require the *state at the first slot of the + // finalized epoch*, rather than the state of the latest finalized block. These two values + // will only differ when the first slot of the finalized epoch is a skip slot. // // Use the `StateRootsIterator` directly rather than `BeaconChain::state_root_at_slot` // to ensure we use the same state that we just set as the head. @@ -985,6 +1004,23 @@ impl BeaconChain { )? .ok_or(Error::MissingFinalizedStateRoot(new_finalized_slot))?; + let update_cache = true; + let new_finalized_state = self + .store + .get_hot_state(&new_finalized_state_root, update_cache)? + .ok_or(Error::MissingBeaconState(new_finalized_state_root))?; + + self.op_pool.prune_all( + &new_snapshot.beacon_block, + &new_snapshot.beacon_state, + &new_finalized_state, + self.epoch()?, + &self.spec, + ); + + // We just pass the state root to the finalization thread. It should be able to reload the + // state from the state_cache near instantly anyway. We could experiment with sending the + // state over a channel in future, but it's probably no quicker. self.store_migrator.process_finalization( new_finalized_state_root.into(), new_view.finalized_checkpoint, @@ -1005,25 +1041,30 @@ impl BeaconChain { /// Persist fork choice to disk, writing immediately. pub fn persist_fork_choice(&self) -> Result<(), Error> { let _fork_choice_timer = metrics::start_timer(&metrics::PERSIST_FORK_CHOICE); - let batch = vec![self.persist_fork_choice_in_batch()]; + let batch = vec![self.persist_fork_choice_in_batch()?]; self.store.hot_db.do_atomically(batch)?; Ok(()) } /// Return a database operation for writing fork choice to disk. - pub fn persist_fork_choice_in_batch(&self) -> KeyValueStoreOp { - Self::persist_fork_choice_in_batch_standalone(&self.canonical_head.fork_choice_read_lock()) + pub fn persist_fork_choice_in_batch(&self) -> Result { + Self::persist_fork_choice_in_batch_standalone( + &self.canonical_head.fork_choice_read_lock(), + self.store.get_config(), + ) + .map_err(Into::into) } /// Return a database operation for writing fork choice to disk. pub fn persist_fork_choice_in_batch_standalone( fork_choice: &BeaconForkChoice, - ) -> KeyValueStoreOp { + store_config: &StoreConfig, + ) -> Result { let persisted_fork_choice = PersistedForkChoice { fork_choice: fork_choice.to_persisted(), fork_choice_store: fork_choice.fc_store().to_persisted(), }; - persisted_fork_choice.as_kv_store_op(FORK_CHOICE_DB_KEY) + persisted_fork_choice.as_kv_store_op(FORK_CHOICE_DB_KEY, store_config) } } @@ -1034,6 +1075,7 @@ impl BeaconChain { /// /// This function is called whilst holding a write-lock on the `canonical_head`. To ensure dead-lock /// safety, **do not take any other locks inside this function**. +#[instrument(skip_all)] fn check_finalized_payload_validity( chain: &BeaconChain, finalized_proto_block: &ProtoBlock, @@ -1117,6 +1159,7 @@ fn perform_debug_logging( } } +#[instrument(skip_all)] fn spawn_execution_layer_updates( chain: Arc>, forkchoice_update_params: ForkchoiceUpdateParameters, @@ -1292,7 +1335,10 @@ fn observe_head_block_delays( slot_clock: &S, event_handler: Option<&ServerSentEventHandler>, ) { - let block_time_set_as_head = timestamp_now(); + let Some(block_time_set_as_head) = slot_clock.now_duration() else { + // Practically unreachable: the slot clock's time should not be before the UNIX epoch. + return; + }; let head_block_root = head_block.root; let head_block_slot = head_block.slot; let head_block_is_optimistic = head_block.execution_status.is_optimistic_or_invalid(); @@ -1313,10 +1359,6 @@ fn observe_head_block_delays( // If a block comes in from over 4 slots ago, it is most likely a block from sync. let block_from_sync = block_delay_total > slot_clock.slot_duration() * 4; - // Determine whether the block has been set as head too late for proper attestation - // production. - let late_head = block_delay_total >= slot_clock.unagg_attestation_production_delay(); - // Do not store metrics if the block was > 4 slots old, this helps prevent noise during // sync. if !block_from_sync { @@ -1415,6 +1457,14 @@ fn observe_head_block_delays( .as_millis() as i64, ); + // Consider the block late if the time it became attestable is after the attestation + // deadline. If the block was not made attestable, use the set-as-head time. + let attestable_delay = block_delays.attestable.unwrap_or(block_delay_total); + + // Determine whether the block has been set as head too late for proper attestation + // production. + let late_head = attestable_delay >= slot_clock.unagg_attestation_production_delay(); + // If the block was enshrined as head too late for attestations to be created for it, // log a debug warning and increment a metric. let format_delay = |delay: &Option| { @@ -1437,6 +1487,24 @@ fn observe_head_block_delays( set_as_head_time_ms = format_delay(&block_delays.set_as_head), "Delayed head block" ); + if let Some(event_handler) = event_handler + && event_handler.has_late_head_subscribers() + { + let peer_info = block_times_cache.get_peer_info(head_block_root); + event_handler.register(EventKind::LateHead(SseLateHead { + slot: head_block_slot, + block: head_block_root, + peer_id: peer_info.id, + peer_client: peer_info.client, + proposer_index: head_block_proposer_index, + proposer_graffiti: head_block_graffiti, + block_delay: block_delay_total, + observed_delay: block_delays.observed, + imported_delay: block_delays.imported, + set_as_head_delay: block_delays.set_as_head, + execution_optimistic: head_block_is_optimistic, + })); + } } else { debug!( block_root = ?head_block_root, @@ -1455,29 +1523,4 @@ fn observe_head_block_delays( ); } } - - if let Some(event_handler) = event_handler { - if !block_from_sync && late_head && event_handler.has_late_head_subscribers() { - let peer_info = block_times_cache.get_peer_info(head_block_root); - let block_delays = block_times_cache.get_block_delays( - head_block_root, - slot_clock - .start_of(head_block_slot) - .unwrap_or_else(|| Duration::from_secs(0)), - ); - event_handler.register(EventKind::LateHead(SseLateHead { - slot: head_block_slot, - block: head_block_root, - peer_id: peer_info.id, - peer_client: peer_info.client, - proposer_index: head_block_proposer_index, - proposer_graffiti: head_block_graffiti, - block_delay: block_delay_total, - observed_delay: block_delays.observed, - imported_delay: block_delays.imported, - set_as_head_delay: block_delays.set_as_head, - execution_optimistic: head_block_is_optimistic, - })); - } - } } diff --git a/beacon_node/beacon_chain/src/capella_readiness.rs b/beacon_node/beacon_chain/src/capella_readiness.rs deleted file mode 100644 index 88af7db0aa..0000000000 --- a/beacon_node/beacon_chain/src/capella_readiness.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Provides tools for checking if a node is ready for the Capella upgrade. - -use crate::{BeaconChain, BeaconChainTypes}; -use execution_layer::http::{ - ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_GET_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V2, -}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::time::Duration; -use types::*; - -/// The time before the Capella fork when we will start issuing warnings about preparation. -use super::bellatrix_readiness::SECONDS_IN_A_WEEK; -pub const CAPELLA_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; -pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type")] -pub enum CapellaReadiness { - /// The execution engine is capella-enabled (as far as we can tell) - Ready, - /// We are connected to an execution engine which doesn't support the V2 engine api methods - V2MethodsNotSupported { error: String }, - /// The transition configuration with the EL failed, there might be a problem with - /// connectivity, authentication or a difference in configuration. - ExchangeCapabilitiesFailed { error: String }, - /// The user has not configured an execution endpoint - NoExecutionEndpoint, -} - -impl fmt::Display for CapellaReadiness { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CapellaReadiness::Ready => { - write!(f, "This node appears ready for Capella.") - } - CapellaReadiness::ExchangeCapabilitiesFailed { error } => write!( - f, - "Could not exchange capabilities with the \ - execution endpoint: {}", - error - ), - CapellaReadiness::NoExecutionEndpoint => write!( - f, - "The --execution-endpoint flag is not specified, this is a \ - requirement post-merge" - ), - CapellaReadiness::V2MethodsNotSupported { error } => write!( - f, - "Execution endpoint does not support Capella methods: {}", - error - ), - } - } -} - -impl BeaconChain { - /// Returns `true` if capella epoch is set and Capella fork has occurred or will - /// occur within `CAPELLA_READINESS_PREPARATION_SECONDS` - pub fn is_time_to_prepare_for_capella(&self, current_slot: Slot) -> bool { - if let Some(capella_epoch) = self.spec.capella_fork_epoch { - let capella_slot = capella_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let capella_readiness_preparation_slots = - CAPELLA_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; - // Return `true` if Capella has happened or is within the preparation time. - current_slot + capella_readiness_preparation_slots > capella_slot - } else { - // The Capella fork epoch has not been defined yet, no need to prepare. - false - } - } - - /// Attempts to connect to the EL and confirm that it is ready for capella. - pub async fn check_capella_readiness(&self) -> CapellaReadiness { - if let Some(el) = self.execution_layer.as_ref() { - match el - .get_engine_capabilities(Some(Duration::from_secs( - ENGINE_CAPABILITIES_REFRESH_INTERVAL, - ))) - .await - { - Err(e) => { - // The EL was either unreachable or responded with an error - CapellaReadiness::ExchangeCapabilitiesFailed { - error: format!("{:?}", e), - } - } - Ok(capabilities) => { - let mut missing_methods = String::from("Required Methods Unsupported:"); - let mut all_good = true; - if !capabilities.get_payload_v2 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_GET_PAYLOAD_V2); - all_good = false; - } - if !capabilities.forkchoice_updated_v2 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_FORKCHOICE_UPDATED_V2); - all_good = false; - } - if !capabilities.new_payload_v2 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_NEW_PAYLOAD_V2); - all_good = false; - } - - if all_good { - CapellaReadiness::Ready - } else { - CapellaReadiness::V2MethodsNotSupported { - error: missing_methods, - } - } - } - } - } else { - CapellaReadiness::NoExecutionEndpoint - } - } -} diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 808c96d965..1f5abc4891 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,3 +1,4 @@ +use crate::custody_context::NodeCustodyType; pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -86,6 +87,8 @@ pub struct ChainConfig { /// If using a weak-subjectivity sync, whether we should download blocks all the way back to /// genesis. pub genesis_backfill: bool, + /// EXPERIMENTAL: backfill blobs and data columns beyond the data availability window. + pub complete_blob_backfill: bool, /// Whether to send payload attributes every slot, regardless of connected proposers. /// /// This is useful for block builders and testing. @@ -96,8 +99,6 @@ pub struct ChainConfig { pub enable_light_client_server: bool, /// The number of data columns to withhold / exclude from publishing when proposing a block. pub malicious_withhold_count: usize, - /// Enable peer sampling on blocks. - pub enable_sampling: bool, /// Number of batches that the node splits blobs or data columns into during publication. /// This doesn't apply if the node is the block proposer. For PeerDAS only. pub blob_publication_batches: usize, @@ -116,6 +117,10 @@ pub struct ChainConfig { /// On Holesky there is a block which is added to this set by default but which can be removed /// by using `--invalid-block-roots ""`. pub invalid_block_roots: HashSet, + /// Disable the getBlobs optimisation to fetch blobs from the EL mempool. + pub disable_get_blobs: bool, + /// The node's custody type, determining how many data columns to custody and sample. + pub node_custody_type: NodeCustodyType, } impl Default for ChainConfig { @@ -144,17 +149,19 @@ impl Default for ChainConfig { optimistic_finalized_sync: true, shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE, genesis_backfill: false, + complete_blob_backfill: false, always_prepare_payload: false, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, enable_light_client_server: true, malicious_withhold_count: 0, - enable_sampling: false, blob_publication_batches: 4, blob_publication_batch_interval: Duration::from_millis(300), sync_tolerance_epochs: DEFAULT_SYNC_TOLERANCE_EPOCHS, block_publishing_delay: None, data_column_publishing_delay: None, invalid_block_roots: HashSet::new(), + disable_get_blobs: false, + node_custody_type: NodeCustodyType::Fullnode, } } } diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs new file mode 100644 index 0000000000..c512ce616a --- /dev/null +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -0,0 +1,1543 @@ +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use std::marker::PhantomData; +use std::{ + collections::{BTreeMap, HashMap}, + sync::atomic::{AtomicU64, Ordering}, +}; +use tracing::{debug, warn}; +use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Slot}; + +/// A delay before making the CGC change effective to the data availability checker. +pub const CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS: u64 = 30; + +/// Number of slots after which a validator's registration is removed if it has not re-registered. +const VALIDATOR_REGISTRATION_EXPIRY_SLOTS: Slot = Slot::new(256); + +type ValidatorsAndBalances = Vec<(usize, u64)>; +type SlotAndEffectiveBalance = (Slot, u64); + +/// This currently just registers increases in validator count. +/// Does not handle decreasing validator counts +#[derive(Default, Debug)] +struct ValidatorRegistrations { + /// Set of all validators that is registered to this node along with its effective balance + /// + /// Key is validator index and value is effective_balance. + validators: HashMap, + /// Maintains the validator custody requirement at a given epoch. + /// + /// Note: Only stores the epoch value when there's a change in custody requirement. + /// So if epoch 10 and 11 has the same custody requirement, only 10 is stored. + /// This map is only pruned during custody backfill. If epoch 11 has custody requirements + /// that are then backfilled to epoch 10, the value at epoch 11 will be removed and epoch 10 + /// will be added to the map instead. This should keep map size constrained to a maximum + /// value of 128. + /// + /// If the node's is started with a cgc override (i.e. supernode/semi-supernode flag), the cgc + /// value is inserted into this map on initialisation with epoch set to 0. For a semi-supernode, + /// this means the custody requirement can still be increased if validator custody exceeds + /// 64 columns. + epoch_validator_custody_requirements: BTreeMap, +} + +impl ValidatorRegistrations { + /// Initialise the validator registration with some default custody requirements. + /// + /// If a `cgc_override` value is specified, the cgc value is inserted into the registration map + /// and is equivalent to registering validator(s) with the same custody requirement. + /// + /// The node will backfill all the way back to either data_availability_boundary or fulu epoch, + /// and because this is a fresh node, setting the epoch to 0 is fine, as backfill will be done via + /// backfill sync instead of column backfill. + fn new(cgc_override: Option) -> Self { + let mut registrations = ValidatorRegistrations { + validators: Default::default(), + epoch_validator_custody_requirements: Default::default(), + }; + if let Some(custody_count) = cgc_override { + registrations + .epoch_validator_custody_requirements + .insert(Epoch::new(0), custody_count); + } + registrations + } + + /// Returns the validator custody requirement at the latest epoch. + fn latest_validator_custody_requirement(&self) -> Option { + self.epoch_validator_custody_requirements + .last_key_value() + .map(|(_, v)| *v) + } + + /// Lookup the active custody requirement at the given epoch. + fn custody_requirement_at_epoch(&self, epoch: Epoch) -> Option { + self.epoch_validator_custody_requirements + .range(..=epoch) + .last() + .map(|(_, custody_count)| *custody_count) + } + + /// Register a new validator index and updates the list of validators if required. + /// Returns `Some((effective_epoch, new_cgc))` if the registration results in a CGC update. + pub(crate) fn register_validators( + &mut self, + validators_and_balance: ValidatorsAndBalances, + current_slot: Slot, + spec: &ChainSpec, + ) -> Option<(Epoch, u64)> { + for (validator_index, effective_balance) in validators_and_balance { + self.validators + .insert(validator_index, (current_slot, effective_balance)); + } + + // Drop validators that haven't re-registered with the node for `VALIDATOR_REGISTRATION_EXPIRY_SLOTS`. + self.validators + .retain(|_, (slot, _)| *slot >= current_slot - VALIDATOR_REGISTRATION_EXPIRY_SLOTS); + + // Each `BALANCE_PER_ADDITIONAL_CUSTODY_GROUP` effectively contributes one unit of "weight". + let validator_custody_units = self.validators.values().map(|(_, eb)| eb).sum::() + / spec.balance_per_additional_custody_group; + let validator_custody_requirement = + get_validators_custody_requirement(validator_custody_units, spec); + + debug!( + validator_custody_units, + validator_custody_requirement, "Registered validators" + ); + + // If registering the new validator increased the total validator "units", then + // add a new entry for the current epoch + if Some(validator_custody_requirement) > self.latest_validator_custody_requirement() { + // Apply the change from the next epoch after adding some delay buffer to ensure + // the node has enough time to subscribe to subnets etc, and to avoid having + // inconsistent column counts within an epoch. + let effective_delay_slots = + CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS / spec.seconds_per_slot; + let effective_epoch = + (current_slot + effective_delay_slots).epoch(E::slots_per_epoch()) + 1; + self.epoch_validator_custody_requirements + .insert(effective_epoch, validator_custody_requirement); + Some((effective_epoch, validator_custody_requirement)) + } else { + None + } + } + + /// Updates the `epoch -> cgc` map after custody backfill has been completed for + /// the specified epoch. + /// + /// This is done by pruning all values on/after `effective_epoch` and updating the map to store + /// the latest validator custody requirements for the `effective_epoch`. + pub fn backfill_validator_custody_requirements( + &mut self, + effective_epoch: Epoch, + expected_cgc: u64, + ) { + if let Some(latest_validator_custody) = self.latest_validator_custody_requirement() { + // If the expected cgc isn't equal to the latest validator custody a very recent cgc change may have occurred. + // We should not update the mapping. + if expected_cgc != latest_validator_custody { + return; + } + // Delete records if + // 1. The epoch is greater than or equal than `effective_epoch` + // 2. the cgc requirements match the latest validator custody requirements + self.epoch_validator_custody_requirements + .retain(|&epoch, custody_requirement| { + !(epoch >= effective_epoch && *custody_requirement == latest_validator_custody) + }); + + self.epoch_validator_custody_requirements + .insert(effective_epoch, latest_validator_custody); + } + } + + /// Updates the `epoch -> cgc` map by pruning records before `effective_epoch` + /// while setting the `cgc` at `effective_epoch` to the latest validator custody requirement. + /// + /// This is used to restart custody backfill sync at `effective_epoch` + pub fn reset_validator_custody_requirements(&mut self, effective_epoch: Epoch) { + if let Some(latest_validator_custody_requirements) = + self.latest_validator_custody_requirement() + { + self.epoch_validator_custody_requirements + .retain(|&epoch, _| epoch >= effective_epoch); + + self.epoch_validator_custody_requirements + .insert(effective_epoch, latest_validator_custody_requirements); + }; + } +} + +/// Given the `validator_custody_units`, return the custody requirement based on +/// the spec parameters. +/// +/// Note: a `validator_custody_units` here represents the number of 32 eth effective_balance +/// equivalent to `BALANCE_PER_ADDITIONAL_CUSTODY_GROUP`. +/// +/// For e.g. a validator with eb 32 eth is 1 unit. +/// a validator with eb 65 eth is 65 // 32 = 2 units. +/// +/// See https://github.com/ethereum/consensus-specs/blob/dev/specs/fulu/validator.md#validator-custody +fn get_validators_custody_requirement(validator_custody_units: u64, spec: &ChainSpec) -> u64 { + std::cmp::min( + std::cmp::max(validator_custody_units, spec.validator_custody_requirement), + spec.number_of_custody_groups, + ) +} + +/// Indicates the different "modes" that a node can run based on the cli +/// parameters that are relevant for computing the custody count. +/// +/// The custody count is derived from 2 values: +/// 1. The number of validators attached to the node and the spec parameters +/// that attach custody weight to attached validators. +/// 2. The cli parameters that the current node is running with. +/// +/// We always persist the validator custody units to the db across restarts +/// such that we know the validator custody units at any given epoch in the past. +/// However, knowing the cli parameter at any given epoch is a pain to maintain +/// and unnecessary. +/// +/// Therefore, the custody count at any point in time is calculated as the max of +/// the validator custody at that time and the current cli params. +/// +/// Choosing the max ensures that we always have the minimum required columns, and +/// we can adjust the `status.earliest_available_slot` value to indicate to our peers +/// the columns that we can guarantee to serve. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Deserialize, Serialize)] +pub enum NodeCustodyType { + /// The node is running with cli parameters to indicate that it + /// wants to subscribe to all columns. + Supernode, + /// The node is running with cli parameters to indicate that it + /// wants to subscribe to the minimum number of columns to enable + /// reconstruction (50%) of the full blob data on demand. + SemiSupernode, + /// The node isn't running with any explicit cli parameters + /// or is running with cli parameters to indicate that it wants + /// to only subscribe to the minimal custody requirements. + #[default] + Fullnode, +} + +impl NodeCustodyType { + pub fn get_custody_count_override(&self, spec: &ChainSpec) -> Option { + match self { + Self::Fullnode => None, + Self::SemiSupernode => Some(spec.number_of_custody_groups / 2), + Self::Supernode => Some(spec.number_of_custody_groups), + } + } +} + +/// Contains all the information the node requires to calculate the +/// number of columns to be custodied when checking for DA. +#[derive(Debug)] +pub struct CustodyContext { + /// The Number of custody groups required based on the number of validators + /// that is attached to this node. + /// + /// This is the number that we use to compute the custody group count that + /// we require for data availability check, and we use to advertise to our peers in the metadata + /// and enr values. + validator_custody_count: AtomicU64, + /// Maintains all the validators that this node is connected to currently + validator_registrations: RwLock, + /// Stores an immutable, ordered list of all data column indices as determined by the node's NodeID + /// on startup. This used to determine the node's custody columns. + ordered_custody_column_indices: Vec, + _phantom_data: PhantomData, +} + +impl CustodyContext { + /// Create a new custody default custody context object when no persisted object + /// exists. + /// + /// The `node_custody_type` value is based on current cli parameters. + pub fn new( + node_custody_type: NodeCustodyType, + ordered_custody_column_indices: Vec, + spec: &ChainSpec, + ) -> Self { + let cgc_override = node_custody_type.get_custody_count_override(spec); + // If there's no override, we initialise `validator_custody_count` to 0. This has been the + // existing behaviour and we maintain this for now to avoid a semantic schema change until + // a later release. + Self { + validator_custody_count: AtomicU64::new(cgc_override.unwrap_or(0)), + validator_registrations: RwLock::new(ValidatorRegistrations::new(cgc_override)), + ordered_custody_column_indices, + _phantom_data: PhantomData, + } + } + + /// Restore the custody context from disk. + /// + /// # Behavior + /// * If [`NodeCustodyType::get_custody_count_override`] < validator_custody_at_head, it means + /// validators have increased the CGC beyond the derived CGC from cli flags. We ignore the CLI input. + /// * If [`NodeCustodyType::get_custody_count_override`] > validator_custody_at_head, it means the user has + /// changed the node's custody type via either the --supernode or --semi-supernode flags which + /// has resulted in a CGC increase. **The new CGC will be made effective from the next epoch**. + /// + /// # Returns + /// A tuple containing: + /// * `Self` - The restored custody context with updated CGC at head + /// * `Option` - `Some` if the CLI flag caused a CGC increase (triggering backfill), + /// `None` if no CGC change occurred or reduction was prevented + pub fn new_from_persisted_custody_context( + ssz_context: CustodyContextSsz, + node_custody_type: NodeCustodyType, + head_epoch: Epoch, + ordered_custody_column_indices: Vec, + spec: &ChainSpec, + ) -> (Self, Option) { + let CustodyContextSsz { + mut validator_custody_at_head, + mut epoch_validator_custody_requirements, + persisted_is_supernode: _, + } = ssz_context; + + let mut custody_count_changed = None; + + if let Some(cgc_from_cli) = node_custody_type.get_custody_count_override(spec) { + debug!( + ?node_custody_type, + persisted_custody_count = validator_custody_at_head, + "Initialising from persisted custody context" + ); + + if cgc_from_cli > validator_custody_at_head { + // Make the CGC from CLI effective from the next epoch + let effective_epoch = head_epoch + 1; + let old_custody_group_count = validator_custody_at_head; + validator_custody_at_head = cgc_from_cli; + + let sampling_count = spec + .sampling_size_custody_groups(cgc_from_cli) + .expect("should compute node sampling size from valid chain spec"); + + epoch_validator_custody_requirements.push((effective_epoch, cgc_from_cli)); + + custody_count_changed = Some(CustodyCountChanged { + new_custody_group_count: validator_custody_at_head, + old_custody_group_count, + sampling_count, + effective_epoch, + }); + + debug!( + info = "new CGC will be effective from the next epoch", + ?node_custody_type, + old_cgc = old_custody_group_count, + new_cgc = validator_custody_at_head, + effective_epoch = %effective_epoch, + "Node custody type change caused a custody count increase", + ); + } else if cgc_from_cli < validator_custody_at_head { + // We don't currently support reducing CGC for simplicity. + // A common scenario is that user may restart with a CLI flag, but the validators + // are only attached later, and we end up having CGC inconsistency. + warn!( + info = "node will continue to run with the current custody count", + current_custody_count = validator_custody_at_head, + node_custody_type = ?node_custody_type, + "Reducing CGC is currently not supported without a resync and will have no effect", + ); + } + } + + let custody_context = CustodyContext { + validator_custody_count: AtomicU64::new(validator_custody_at_head), + validator_registrations: RwLock::new(ValidatorRegistrations { + validators: Default::default(), + epoch_validator_custody_requirements: epoch_validator_custody_requirements + .into_iter() + .collect(), + }), + ordered_custody_column_indices, + _phantom_data: PhantomData, + }; + + (custody_context, custody_count_changed) + } + + /// Register a new validator index and updates the list of validators if required. + /// + /// Also modifies the internal structures if the validator custody has changed to + /// update the `custody_column_count`. + /// + /// Returns `Some` along with the updated custody group count if it has changed, otherwise returns `None`. + pub fn register_validators( + &self, + validators_and_balance: ValidatorsAndBalances, + current_slot: Slot, + spec: &ChainSpec, + ) -> Option { + let Some((effective_epoch, new_validator_custody)) = self + .validator_registrations + .write() + .register_validators::(validators_and_balance, current_slot, spec) + else { + return None; + }; + + let current_cgc = self.validator_custody_count.load(Ordering::Relaxed); + + if new_validator_custody != current_cgc { + debug!( + old_count = current_cgc, + new_count = new_validator_custody, + "Validator count at head updated" + ); + self.validator_custody_count + .store(new_validator_custody, Ordering::Relaxed); + + let updated_cgc = self.custody_group_count_at_head(spec); + // Send the message to network only if there are more columns subnets to subscribe to + if updated_cgc > current_cgc { + debug!( + old_cgc = current_cgc, + updated_cgc, "Custody group count updated" + ); + return Some(CustodyCountChanged { + new_custody_group_count: updated_cgc, + old_custody_group_count: current_cgc, + sampling_count: self.num_of_custody_groups_to_sample(effective_epoch, spec), + effective_epoch, + }); + } + } + + None + } + + /// This function is used to determine the custody group count at head ONLY. + /// Do NOT use this directly for data availability check, use `self.sampling_size` instead as + /// CGC can change over epochs. + pub fn custody_group_count_at_head(&self, spec: &ChainSpec) -> u64 { + let validator_custody_count_at_head = self.validator_custody_count.load(Ordering::Relaxed); + + // If there are no validators, return the minimum custody_requirement + if validator_custody_count_at_head > 0 { + validator_custody_count_at_head + } else { + spec.custody_requirement + } + } + + /// This function is used to determine the custody group count at a given epoch. + /// + /// This differs from the number of custody groups sampled per slot, as the spec requires a + /// minimum sampling size which may exceed the custody group count (CGC). + /// + /// See also: [`Self::num_of_custody_groups_to_sample`]. + pub fn custody_group_count_at_epoch(&self, epoch: Epoch, spec: &ChainSpec) -> u64 { + self.validator_registrations + .read() + .custody_requirement_at_epoch(epoch) + .unwrap_or(spec.custody_requirement) + } + + /// Returns the count of custody groups this node must _sample_ for a block at `epoch` to import. + pub fn num_of_custody_groups_to_sample(&self, epoch: Epoch, spec: &ChainSpec) -> u64 { + let custody_group_count = self.custody_group_count_at_epoch(epoch, spec); + spec.sampling_size_custody_groups(custody_group_count) + .expect("should compute node sampling size from valid chain spec") + } + + /// Returns the count of columns this node must _sample_ for a block at `epoch` to import. + pub fn num_of_data_columns_to_sample(&self, epoch: Epoch, spec: &ChainSpec) -> usize { + let custody_group_count = self.custody_group_count_at_epoch(epoch, spec); + spec.sampling_size_columns::(custody_group_count) + .expect("should compute node sampling size from valid chain spec") + } + + /// Returns whether the node should attempt reconstruction at a given epoch. + pub fn should_attempt_reconstruction(&self, epoch: Epoch, spec: &ChainSpec) -> bool { + let min_columns_for_reconstruction = E::number_of_columns() / 2; + // performing reconstruction is not necessary if sampling column count is exactly 50%, + // because the node doesn't need the remaining columns. + self.num_of_data_columns_to_sample(epoch, spec) > min_columns_for_reconstruction + } + + /// Returns the ordered list of column indices that should be sampled for data availability checking at the given epoch. + /// + /// # Parameters + /// * `epoch` - Epoch to determine sampling columns for + /// * `spec` - Chain specification containing sampling parameters + /// + /// # Returns + /// A slice of ordered column indices that should be sampled for this epoch based on the node's custody configuration + pub fn sampling_columns_for_epoch(&self, epoch: Epoch, spec: &ChainSpec) -> &[ColumnIndex] { + let num_of_columns_to_sample = self.num_of_data_columns_to_sample(epoch, spec); + &self.ordered_custody_column_indices[..num_of_columns_to_sample] + } + + /// Returns the ordered list of column indices that the node is assigned to custody + /// (and advertised to peers) at the given epoch. If epoch is `None`, this function + /// computes the custody columns at head. + /// + /// This method differs from [`self::sampling_columns_for_epoch`] which returns all sampling columns. + /// The columns returned by this method are either identical to or a subset of the sampling columns, + /// representing only those columns that this node is responsible for maintaining custody of. + /// + /// # Parameters + /// * `epoch_opt` - Optional epoch to determine custody columns for. + /// + /// # Returns + /// A slice of ordered custody column indices for this epoch based on the node's custody configuration + pub fn custody_columns_for_epoch( + &self, + epoch_opt: Option, + spec: &ChainSpec, + ) -> &[ColumnIndex] { + let custody_group_count = if let Some(epoch) = epoch_opt { + self.custody_group_count_at_epoch(epoch, spec) as usize + } else { + self.custody_group_count_at_head(spec) as usize + }; + + // This is an unnecessary conversion for spec compliance, basically just multiplying by 1. + let columns_per_custody_group = spec.data_columns_per_group::() as usize; + let custody_column_count = columns_per_custody_group * custody_group_count; + + &self.ordered_custody_column_indices[..custody_column_count] + } + + /// The node has completed backfill for this epoch. Update the internal records so the function + /// [`Self::custody_columns_for_epoch()`] returns up-to-date results. + pub fn update_and_backfill_custody_count_at_epoch( + &self, + effective_epoch: Epoch, + expected_cgc: u64, + ) { + self.validator_registrations + .write() + .backfill_validator_custody_requirements(effective_epoch, expected_cgc); + } + + /// The node is attempting to restart custody backfill. Update the internal records so that + /// custody backfill can start backfilling at `effective_epoch`. + pub fn reset_validator_custody_requirements(&self, effective_epoch: Epoch) { + self.validator_registrations + .write() + .reset_validator_custody_requirements(effective_epoch); + } +} + +/// Indicates that the custody group count (CGC) has increased. +/// +/// CGC increases can occur due to: +/// 1. Validator registrations increasing effective balance beyond current CGC +/// 2. CLI flag changes (e.g., switching to --supernode or --semi-supernode) +/// +/// This struct is used to trigger column backfill and network subnet subscription updates. +pub struct CustodyCountChanged { + pub new_custody_group_count: u64, + pub old_custody_group_count: u64, + pub sampling_count: u64, + pub effective_epoch: Epoch, +} + +/// The custody information that gets persisted across runs. +#[derive(Debug, Encode, Decode, Clone)] +pub struct CustodyContextSsz { + pub validator_custody_at_head: u64, + /// DEPRECATED. This field is no longer in used and will be removed in a future release. + pub persisted_is_supernode: bool, + pub epoch_validator_custody_requirements: Vec<(Epoch, u64)>, +} + +impl From<&CustodyContext> for CustodyContextSsz { + fn from(context: &CustodyContext) -> Self { + CustodyContextSsz { + validator_custody_at_head: context.validator_custody_count.load(Ordering::Relaxed), + // This field is deprecated and has no effect + persisted_is_supernode: false, + epoch_validator_custody_requirements: context + .validator_registrations + .read() + .epoch_validator_custody_requirements + .iter() + .map(|(epoch, count)| (*epoch, *count)) + .collect(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::generate_data_column_indices_rand_order; + use types::MainnetEthSpec; + + type E = MainnetEthSpec; + + fn setup_custody_context( + spec: &ChainSpec, + head_epoch: Epoch, + epoch_and_cgc_tuples: Vec<(Epoch, u64)>, + ) -> CustodyContext { + let cgc_at_head = epoch_and_cgc_tuples.last().unwrap().1; + let ssz_context = CustodyContextSsz { + validator_custody_at_head: cgc_at_head, + persisted_is_supernode: false, + epoch_validator_custody_requirements: epoch_and_cgc_tuples, + }; + + let (custody_context, _) = CustodyContext::::new_from_persisted_custody_context( + ssz_context, + NodeCustodyType::Fullnode, + head_epoch, + generate_data_column_indices_rand_order::(), + spec, + ); + + custody_context + } + + fn complete_backfill_for_epochs( + custody_context: &CustodyContext, + start_epoch: Epoch, + end_epoch: Epoch, + expected_cgc: u64, + ) { + assert!(start_epoch >= end_epoch); + // Call from end_epoch down to start_epoch (inclusive), simulating backfill + for epoch in (end_epoch.as_u64()..=start_epoch.as_u64()).rev() { + custody_context + .update_and_backfill_custody_count_at_epoch(Epoch::new(epoch), expected_cgc); + } + } + + /// Helper function to test CGC increases when switching node custody types. + /// Verifies that CustodyCountChanged is returned with correct values and + /// that custody_group_count_at_epoch returns appropriate values for current and next epoch. + fn assert_custody_type_switch_increases_cgc( + persisted_cgc: u64, + target_node_custody_type: NodeCustodyType, + expected_new_cgc: u64, + head_epoch: Epoch, + spec: &ChainSpec, + ) { + let ssz_context = CustodyContextSsz { + validator_custody_at_head: persisted_cgc, + persisted_is_supernode: false, + epoch_validator_custody_requirements: vec![(Epoch::new(0), persisted_cgc)], + }; + + let (custody_context, custody_count_changed) = + CustodyContext::::new_from_persisted_custody_context( + ssz_context, + target_node_custody_type, + head_epoch, + generate_data_column_indices_rand_order::(), + spec, + ); + + // Verify CGC increased + assert_eq!( + custody_context.custody_group_count_at_head(spec), + expected_new_cgc, + "cgc should increase from {} to {}", + persisted_cgc, + expected_new_cgc + ); + + // Verify CustodyCountChanged is returned with correct values + let cgc_changed = custody_count_changed.expect("CustodyCountChanged should be returned"); + assert_eq!( + cgc_changed.new_custody_group_count, expected_new_cgc, + "new_custody_group_count should be {}", + expected_new_cgc + ); + assert_eq!( + cgc_changed.old_custody_group_count, persisted_cgc, + "old_custody_group_count should be {}", + persisted_cgc + ); + assert_eq!( + cgc_changed.effective_epoch, + head_epoch + 1, + "effective epoch should be head_epoch + 1" + ); + assert_eq!( + cgc_changed.sampling_count, + spec.sampling_size_custody_groups(expected_new_cgc) + .expect("should compute sampling size"), + "sampling_count should match expected value" + ); + + // Verify custody_group_count_at_epoch returns correct values + assert_eq!( + custody_context.custody_group_count_at_epoch(head_epoch, spec), + persisted_cgc, + "current epoch should still use old cgc ({})", + persisted_cgc + ); + assert_eq!( + custody_context.custody_group_count_at_epoch(head_epoch + 1, spec), + expected_new_cgc, + "next epoch should use new cgc ({})", + expected_new_cgc + ); + } + + /// Helper function to test CGC reduction prevention when switching node custody types. + /// Verifies that CGC stays at the persisted value and CustodyCountChanged is not returned. + fn assert_custody_type_switch_unchanged_cgc( + persisted_cgc: u64, + target_node_custody_type: NodeCustodyType, + head_epoch: Epoch, + spec: &ChainSpec, + ) { + let ssz_context = CustodyContextSsz { + validator_custody_at_head: persisted_cgc, + persisted_is_supernode: false, + epoch_validator_custody_requirements: vec![(Epoch::new(0), persisted_cgc)], + }; + + let (custody_context, custody_count_changed) = + CustodyContext::::new_from_persisted_custody_context( + ssz_context, + target_node_custody_type, + head_epoch, + generate_data_column_indices_rand_order::(), + spec, + ); + + // Verify CGC stays at persisted value (no reduction) + assert_eq!( + custody_context.custody_group_count_at_head(spec), + persisted_cgc, + "cgc should remain at {} (reduction not supported)", + persisted_cgc + ); + + // Verify no CustodyCountChanged is returned (no change occurred) + assert!( + custody_count_changed.is_none(), + "CustodyCountChanged should not be returned when CGC doesn't change" + ); + } + + #[test] + fn no_validators_supernode_default() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Supernode, + generate_data_column_indices_rand_order::(), + &spec, + ); + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + spec.number_of_custody_groups + ); + assert_eq!( + custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec), + spec.number_of_custody_groups + ); + } + + #[test] + fn no_validators_semi_supernode_default() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::SemiSupernode, + generate_data_column_indices_rand_order::(), + &spec, + ); + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + spec.number_of_custody_groups / 2 + ); + assert_eq!( + custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec), + spec.number_of_custody_groups / 2 + ); + } + + #[test] + fn no_validators_fullnode_default() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + spec.custody_requirement, + "head custody count should be minimum spec custody requirement" + ); + assert_eq!( + custody_context.num_of_custody_groups_to_sample(Epoch::new(0), &spec), + spec.samples_per_slot + ); + } + + #[test] + fn register_single_validator_should_update_cgc() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let bal_per_additional_group = spec.balance_per_additional_custody_group; + let min_val_custody_requirement = spec.validator_custody_requirement; + // One single node increases its balance over 3 epochs. + let validators_and_expected_cgc_change = vec![ + ( + vec![(0, bal_per_additional_group)], + Some(min_val_custody_requirement), + ), + // No CGC change at 8 custody units, as it's the minimum requirement + (vec![(0, 8 * bal_per_additional_group)], None), + (vec![(0, 10 * bal_per_additional_group)], Some(10)), + ]; + + register_validators_and_assert_cgc::( + &custody_context, + validators_and_expected_cgc_change, + &spec, + ); + } + + #[test] + fn register_multiple_validators_should_update_cgc() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let bal_per_additional_group = spec.balance_per_additional_custody_group; + let min_val_custody_requirement = spec.validator_custody_requirement; + // Add 3 validators over 3 epochs. + let validators_and_expected_cgc = vec![ + ( + vec![(0, bal_per_additional_group)], + Some(min_val_custody_requirement), + ), + ( + vec![ + (0, bal_per_additional_group), + (1, 7 * bal_per_additional_group), + ], + // No CGC change at 8 custody units, as it's the minimum requirement + None, + ), + ( + vec![ + (0, bal_per_additional_group), + (1, 7 * bal_per_additional_group), + (2, 2 * bal_per_additional_group), + ], + Some(10), + ), + ]; + + register_validators_and_assert_cgc::( + &custody_context, + validators_and_expected_cgc, + &spec, + ); + } + + #[test] + fn register_validators_should_not_update_cgc_for_supernode() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Supernode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let bal_per_additional_group = spec.balance_per_additional_custody_group; + + // Add 3 validators over 3 epochs. + let validators_and_expected_cgc = vec![ + (vec![(0, bal_per_additional_group)], None), + ( + vec![ + (0, bal_per_additional_group), + (1, 7 * bal_per_additional_group), + ], + None, + ), + ( + vec![ + (0, bal_per_additional_group), + (1, 7 * bal_per_additional_group), + (2, 2 * bal_per_additional_group), + ], + None, + ), + ]; + + register_validators_and_assert_cgc::( + &custody_context, + validators_and_expected_cgc, + &spec, + ); + let current_epoch = Epoch::new(2); + assert_eq!( + custody_context.num_of_custody_groups_to_sample(current_epoch, &spec), + spec.number_of_custody_groups + ); + } + + #[test] + fn cgc_change_should_be_effective_to_sampling_after_delay() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let current_slot = Slot::new(10); + let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let default_sampling_size = + custody_context.num_of_custody_groups_to_sample(current_epoch, &spec); + let validator_custody_units = 10; + + let _cgc_changed = custody_context.register_validators( + vec![( + 0, + validator_custody_units * spec.balance_per_additional_custody_group, + )], + current_slot, + &spec, + ); + + // CGC update is not applied for `current_epoch`. + assert_eq!( + custody_context.num_of_custody_groups_to_sample(current_epoch, &spec), + default_sampling_size + ); + // CGC update is applied for the next epoch. + assert_eq!( + custody_context.num_of_custody_groups_to_sample(current_epoch + 1, &spec), + validator_custody_units + ); + } + + #[test] + fn validator_dropped_after_no_registrations_within_expiry_should_not_reduce_cgc() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let current_slot = Slot::new(10); + let val_custody_units_1 = 10; + let val_custody_units_2 = 5; + + // GIVEN val_1 and val_2 registered at `current_slot` + let _ = custody_context.register_validators( + vec![ + ( + 1, + val_custody_units_1 * spec.balance_per_additional_custody_group, + ), + ( + 2, + val_custody_units_2 * spec.balance_per_additional_custody_group, + ), + ], + current_slot, + &spec, + ); + + // WHEN val_1 re-registered, but val_2 did not re-register after `VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1` slots + let cgc_changed_opt = custody_context.register_validators( + vec![( + 1, + val_custody_units_1 * spec.balance_per_additional_custody_group, + )], + current_slot + VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1, + &spec, + ); + + // THEN the reduction from dropping val_2 balance should NOT result in a CGC reduction + assert!(cgc_changed_opt.is_none(), "CGC should remain unchanged"); + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + val_custody_units_1 + val_custody_units_2 + ) + } + + #[test] + fn validator_dropped_after_no_registrations_within_expiry() { + let spec = E::default_spec(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + ); + let current_slot = Slot::new(10); + let val_custody_units_1 = 10; + let val_custody_units_2 = 5; + let val_custody_units_3 = 6; + + // GIVEN val_1 and val_2 registered at `current_slot` + let _ = custody_context.register_validators( + vec![ + ( + 1, + val_custody_units_1 * spec.balance_per_additional_custody_group, + ), + ( + 2, + val_custody_units_2 * spec.balance_per_additional_custody_group, + ), + ], + current_slot, + &spec, + ); + + // WHEN val_1 and val_3 registered, but val_3 did not re-register after `VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1` slots + let cgc_changed = custody_context.register_validators( + vec![ + ( + 1, + val_custody_units_1 * spec.balance_per_additional_custody_group, + ), + ( + 3, + val_custody_units_3 * spec.balance_per_additional_custody_group, + ), + ], + current_slot + VALIDATOR_REGISTRATION_EXPIRY_SLOTS + 1, + &spec, + ); + + // THEN CGC should increase, BUT val_2 balance should NOT be included in CGC + assert_eq!( + cgc_changed + .expect("CGC should change") + .new_custody_group_count, + val_custody_units_1 + val_custody_units_3 + ); + } + + /// Update the validator every epoch and assert cgc against expected values. + fn register_validators_and_assert_cgc( + custody_context: &CustodyContext, + validators_and_expected_cgc_changed: Vec<(ValidatorsAndBalances, Option)>, + spec: &ChainSpec, + ) { + for (idx, (validators_and_balance, expected_cgc_change)) in + validators_and_expected_cgc_changed.into_iter().enumerate() + { + let epoch = Epoch::new(idx as u64); + let updated_custody_count_opt = custody_context + .register_validators( + validators_and_balance, + epoch.start_slot(E::slots_per_epoch()), + spec, + ) + .map(|c| c.new_custody_group_count); + + assert_eq!(updated_custody_count_opt, expected_cgc_change); + } + } + + #[test] + fn custody_columns_for_epoch_no_validators_fullnode() { + let spec = E::default_spec(); + let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + ordered_custody_column_indices, + &spec, + ); + + assert_eq!( + custody_context.custody_columns_for_epoch(None, &spec).len(), + spec.custody_requirement as usize + ); + } + + #[test] + fn custody_columns_for_epoch_no_validators_supernode() { + let spec = E::default_spec(); + let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Supernode, + ordered_custody_column_indices, + &spec, + ); + + assert_eq!( + custody_context.custody_columns_for_epoch(None, &spec).len(), + spec.number_of_custody_groups as usize + ); + } + + #[test] + fn custody_columns_for_epoch_with_validators_should_match_cgc() { + let spec = E::default_spec(); + let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + ordered_custody_column_indices, + &spec, + ); + let val_custody_units = 10; + + let _ = custody_context.register_validators( + vec![( + 0, + val_custody_units * spec.balance_per_additional_custody_group, + )], + Slot::new(10), + &spec, + ); + + assert_eq!( + custody_context.custody_columns_for_epoch(None, &spec).len(), + val_custody_units as usize + ); + } + + #[test] + fn custody_columns_for_epoch_specific_epoch_uses_epoch_cgc() { + let spec = E::default_spec(); + let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); + let custody_context = CustodyContext::::new( + NodeCustodyType::Fullnode, + ordered_custody_column_indices, + &spec, + ); + let test_epoch = Epoch::new(5); + + let expected_cgc = custody_context.custody_group_count_at_epoch(test_epoch, &spec); + assert_eq!( + custody_context + .custody_columns_for_epoch(Some(test_epoch), &spec) + .len(), + expected_cgc as usize + ); + } + + #[test] + fn restore_from_persisted_fullnode_no_validators() { + let spec = E::default_spec(); + let ssz_context = CustodyContextSsz { + validator_custody_at_head: 0, // no validators + persisted_is_supernode: false, + epoch_validator_custody_requirements: vec![], + }; + + let (custody_context, _) = CustodyContext::::new_from_persisted_custody_context( + ssz_context, + NodeCustodyType::Fullnode, + Epoch::new(0), + generate_data_column_indices_rand_order::(), + &spec, + ); + + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + spec.custody_requirement, + "restored custody group count should match fullnode default" + ); + } + + /// Tests CLI flag change: Fullnode (CGC=0) → Supernode (CGC=128) + /// CGC should increase and trigger backfill via CustodyCountChanged. + #[test] + fn restore_fullnode_then_switch_to_supernode_increases_cgc() { + let spec = E::default_spec(); + let head_epoch = Epoch::new(10); + let supernode_cgc = spec.number_of_custody_groups; + + assert_custody_type_switch_increases_cgc( + 0, + NodeCustodyType::Supernode, + supernode_cgc, + head_epoch, + &spec, + ); + } + + /// Tests validator-driven CGC increase: Semi-supernode (CGC=64) → CGC=70 + /// Semi-supernode can exceed 64 when validator effective balance increases CGC. + #[test] + fn restore_semi_supernode_with_validators_can_exceed_64() { + let spec = E::default_spec(); + let semi_supernode_cgc = spec.number_of_custody_groups / 2; // 64 + let custody_context = CustodyContext::::new( + NodeCustodyType::SemiSupernode, + generate_data_column_indices_rand_order::(), + &spec, + ); + + // Verify initial CGC is 64 (semi-supernode) + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + semi_supernode_cgc, + "initial cgc should be 64" + ); + + // Register validators with 70 custody units (exceeding semi-supernode default) + let validator_custody_units = 70; + let current_slot = Slot::new(10); + let cgc_changed = custody_context.register_validators( + vec![( + 0, + validator_custody_units * spec.balance_per_additional_custody_group, + )], + current_slot, + &spec, + ); + + // Verify CGC increased from 64 to 70 + assert!( + cgc_changed.is_some(), + "CustodyCountChanged should be returned" + ); + let cgc_changed = cgc_changed.unwrap(); + assert_eq!( + cgc_changed.new_custody_group_count, validator_custody_units, + "cgc should increase to 70" + ); + assert_eq!( + cgc_changed.old_custody_group_count, semi_supernode_cgc, + "old cgc should be 64" + ); + + // Verify the custody context reflects the new CGC + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + validator_custody_units, + "custody_group_count_at_head should be 70" + ); + } + + /// Tests CLI flag change prevention: Supernode (CGC=128) → Fullnode (CGC stays 128) + /// CGC reduction is not supported - persisted value is retained. + #[test] + fn restore_supernode_then_switch_to_fullnode_uses_persisted() { + let spec = E::default_spec(); + let supernode_cgc = spec.number_of_custody_groups; + + assert_custody_type_switch_unchanged_cgc( + supernode_cgc, + NodeCustodyType::Fullnode, + Epoch::new(0), + &spec, + ); + } + + /// Tests CLI flag change prevention: Supernode (CGC=128) → Semi-supernode (CGC stays 128) + /// CGC reduction is not supported - persisted value is retained. + #[test] + fn restore_supernode_then_switch_to_semi_supernode_keeps_supernode_cgc() { + let spec = E::default_spec(); + let supernode_cgc = spec.number_of_custody_groups; + let head_epoch = Epoch::new(10); + + assert_custody_type_switch_unchanged_cgc( + supernode_cgc, + NodeCustodyType::SemiSupernode, + head_epoch, + &spec, + ); + } + + /// Tests CLI flag change: Fullnode with validators (CGC=32) → Semi-supernode (CGC=64) + /// CGC should increase and trigger backfill via CustodyCountChanged. + #[test] + fn restore_fullnode_with_validators_then_switch_to_semi_supernode() { + let spec = E::default_spec(); + let persisted_cgc = 32u64; + let semi_supernode_cgc = spec.number_of_custody_groups / 2; + let head_epoch = Epoch::new(10); + + assert_custody_type_switch_increases_cgc( + persisted_cgc, + NodeCustodyType::SemiSupernode, + semi_supernode_cgc, + head_epoch, + &spec, + ); + } + + /// Tests CLI flag change: Semi-supernode (CGC=64) → Supernode (CGC=128) + /// CGC should increase and trigger backfill via CustodyCountChanged. + #[test] + fn restore_semi_supernode_then_switch_to_supernode() { + let spec = E::default_spec(); + let semi_supernode_cgc = spec.number_of_custody_groups / 2; + let supernode_cgc = spec.number_of_custody_groups; + let head_epoch = Epoch::new(10); + + assert_custody_type_switch_increases_cgc( + semi_supernode_cgc, + NodeCustodyType::Supernode, + supernode_cgc, + head_epoch, + &spec, + ); + } + + /// Tests CLI flag change: Fullnode with validators (CGC=32) → Supernode (CGC=128) + /// CGC should increase and trigger backfill via CustodyCountChanged. + #[test] + fn restore_with_cli_flag_increases_cgc_from_nonzero() { + let spec = E::default_spec(); + let persisted_cgc = 32u64; + let supernode_cgc = spec.number_of_custody_groups; + let head_epoch = Epoch::new(10); + + assert_custody_type_switch_increases_cgc( + persisted_cgc, + NodeCustodyType::Supernode, + supernode_cgc, + head_epoch, + &spec, + ); + } + + #[test] + fn restore_with_validator_custody_history_across_epochs() { + let spec = E::default_spec(); + let initial_cgc = 8u64; + let increased_cgc = 16u64; + let final_cgc = 32u64; + + let ssz_context = CustodyContextSsz { + validator_custody_at_head: final_cgc, + persisted_is_supernode: false, + epoch_validator_custody_requirements: vec![ + (Epoch::new(0), initial_cgc), + (Epoch::new(10), increased_cgc), + (Epoch::new(20), final_cgc), + ], + }; + + let (custody_context, _) = CustodyContext::::new_from_persisted_custody_context( + ssz_context, + NodeCustodyType::Fullnode, + Epoch::new(20), + generate_data_column_indices_rand_order::(), + &spec, + ); + + // Verify head uses latest value + assert_eq!( + custody_context.custody_group_count_at_head(&spec), + final_cgc + ); + + // Verify historical epoch lookups work correctly + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(5), &spec), + initial_cgc, + "epoch 5 should use initial cgc" + ); + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec), + increased_cgc, + "epoch 15 should use increased cgc" + ); + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(25), &spec), + final_cgc, + "epoch 25 should use final cgc" + ); + + // Verify sampling size calculation uses correct historical values + assert_eq!( + custody_context.num_of_custody_groups_to_sample(Epoch::new(5), &spec), + spec.samples_per_slot, + "sampling at epoch 5 should use spec minimum since cgc is at minimum" + ); + assert_eq!( + custody_context.num_of_custody_groups_to_sample(Epoch::new(25), &spec), + final_cgc, + "sampling at epoch 25 should match final cgc" + ); + } + + #[test] + fn backfill_single_cgc_increase_updates_past_epochs() { + let spec = E::default_spec(); + let final_cgc = 32u64; + let default_cgc = spec.custody_requirement; + + // Setup: Node restart after validators were registered, causing CGC increase to 32 at epoch 20 + let head_epoch = Epoch::new(20); + let epoch_and_cgc_tuples = vec![(head_epoch, final_cgc)]; + let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples); + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec), + default_cgc, + ); + + // Backfill from epoch 20 down to 15 (simulating backfill) + complete_backfill_for_epochs(&custody_context, head_epoch, Epoch::new(15), final_cgc); + + // After backfilling to epoch 15, it should use latest CGC (32) + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(15), &spec), + final_cgc, + ); + assert_eq!( + custody_context + .custody_columns_for_epoch(Some(Epoch::new(15)), &spec) + .len(), + final_cgc as usize, + ); + + // Prior epoch should still return the original CGC + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(14), &spec), + default_cgc, + ); + } + + #[test] + fn backfill_with_multiple_cgc_increases_prunes_map_correctly() { + let spec = E::default_spec(); + let initial_cgc = 8u64; + let mid_cgc = 16u64; + let final_cgc = 32u64; + + // Setup: Node restart after multiple validator registrations causing CGC increases + let head_epoch = Epoch::new(20); + let epoch_and_cgc_tuples = vec![ + (Epoch::new(0), initial_cgc), + (Epoch::new(10), mid_cgc), + (head_epoch, final_cgc), + ]; + let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples); + + // Backfill to epoch 15 (between the two CGC increases) + complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(15), final_cgc); + + // Verify epochs 15 - 20 return latest CGC (32) + for epoch in 15..=20 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + final_cgc, + ); + } + + // Verify epochs 10-14 still return mid_cgc (16) + for epoch in 10..14 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + mid_cgc, + ); + } + } + + #[test] + fn attempt_backfill_with_invalid_cgc() { + let spec = E::default_spec(); + let initial_cgc = 8u64; + let mid_cgc = 16u64; + let final_cgc = 32u64; + + // Setup: Node restart after multiple validator registrations causing CGC increases + let head_epoch = Epoch::new(20); + let epoch_and_cgc_tuples = vec![ + (Epoch::new(0), initial_cgc), + (Epoch::new(10), mid_cgc), + (head_epoch, final_cgc), + ]; + let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples); + + // Backfill to epoch 15 (between the two CGC increases) + complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(15), final_cgc); + + // Verify epochs 15 - 20 return latest CGC (32) + for epoch in 15..=20 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + final_cgc, + ); + } + + // Attempt backfill with an incorrect cgc value + complete_backfill_for_epochs( + &custody_context, + Epoch::new(20), + Epoch::new(15), + initial_cgc, + ); + + // Verify epochs 15 - 20 still return latest CGC (32) + for epoch in 15..=20 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + final_cgc, + ); + } + + // Verify epochs 10-14 still return mid_cgc (16) + for epoch in 10..14 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + mid_cgc, + ); + } + } + + #[test] + fn reset_validator_custody_requirements() { + let spec = E::default_spec(); + let minimum_cgc = 4u64; + let initial_cgc = 8u64; + let mid_cgc = 16u64; + let final_cgc = 32u64; + + // Setup: Node restart after multiple validator registrations causing CGC increases + let head_epoch = Epoch::new(20); + let epoch_and_cgc_tuples = vec![ + (Epoch::new(0), initial_cgc), + (Epoch::new(10), mid_cgc), + (head_epoch, final_cgc), + ]; + let custody_context = setup_custody_context(&spec, head_epoch, epoch_and_cgc_tuples); + + // Backfill from epoch 20 to 9 + complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(9), final_cgc); + + // Reset validator custody requirements to the latest cgc requirements at `head_epoch` up to the boundary epoch + custody_context.reset_validator_custody_requirements(head_epoch); + + // Verify epochs 0 - 19 return the minimum cgc requirement because of the validator custody requirement reset + for epoch in 0..=19 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + minimum_cgc, + ); + } + + // Verify epoch 20 returns a CGC of 32 + assert_eq!( + custody_context.custody_group_count_at_epoch(head_epoch, &spec), + final_cgc + ); + + // Rerun Backfill to epoch 20 + complete_backfill_for_epochs(&custody_context, Epoch::new(20), Epoch::new(0), final_cgc); + + // Verify epochs 0 - 20 return the final cgc requirements + for epoch in 0..=20 { + assert_eq!( + custody_context.custody_group_count_at_epoch(Epoch::new(epoch), &spec), + final_cgc, + ); + } + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 6f292f3551..3e859456b1 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1,11 +1,15 @@ -use crate::blob_verification::{verify_kzg_for_blob_list, GossipVerifiedBlob, KzgVerifiedBlobList}; +use crate::blob_verification::{ + GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list, +}; use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableExecutedBlock, RpcBlock, }; use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; -use crate::{metrics, BeaconChain, BeaconChainTypes, BeaconStore}; +use crate::{ + BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics, +}; use kzg::Kzg; use slot_clock::SlotClock; use std::fmt; @@ -14,36 +18,42 @@ use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; -use tracing::{debug, error, info_span, Instrument}; +use tracing::{debug, error, instrument}; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ - BlobSidecarList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, Hash256, - RuntimeVariableList, SignedBeaconBlock, + BlobSidecarList, BlockImportSource, ChainSpec, DataColumnSidecar, DataColumnSidecarList, Epoch, + EthSpec, Hash256, SignedBeaconBlock, Slot, }; mod error; mod overflow_lru_cache; mod state_lru_cache; +use crate::data_availability_checker::error::Error; use crate::data_column_verification::{ - verify_kzg_for_data_column_list_with_scoring, CustodyDataColumn, GossipVerifiedDataColumn, - KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, + CustodyDataColumn, GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, + KzgVerifiedDataColumn, verify_kzg_for_data_column_list, }; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; +use crate::observed_data_sidecars::ObservationStrategy; pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCheckErrorCategory}; use types::non_zero_usize::new_non_zero_usize; -/// The LRU Cache stores `PendingComponents` which can store up to -/// `MAX_BLOBS_PER_BLOCK = 6` blobs each. A `BlobSidecar` is 0.131256 MB. So -/// the maximum size of a `PendingComponents` is ~ 0.787536 MB. Setting this -/// to 1024 means the maximum size of the cache is ~ 0.8 GB. But the cache -/// will target a size of less than 75% of capacity. -pub const OVERFLOW_LRU_CAPACITY: NonZeroUsize = new_non_zero_usize(1024); -/// Until tree-states is implemented, we can't store very many states in memory :( -pub const STATE_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(2); -pub const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); +/// The LRU Cache stores `PendingComponents`, which store block and its associated blob data: +/// +/// * Deneb blobs are 128 kb each and are stored in the form of `BlobSidecar`. +/// * From Fulu (PeerDAS), blobs are erasure-coded and are 256 kb each, stored in the form of 128 `DataColumnSidecar`s. +/// +/// With `MAX_BLOBS_PER_BLOCK` = 48 (expected in the next year), the maximum size of data columns +/// in `PendingComponents` is ~12.29 MB. Setting this to 32 means the maximum size of the cache is +/// approximately 0.4 GB. +/// +/// `PendingComponents` are now never removed from the cache manually are only removed via LRU +/// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. +const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); +const STATE_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); /// Cache to hold fully valid data that can't be imported to fork-choice yet. After Dencun hard-fork /// blocks have a sidecar of data that is received separately from the network. We call the concept @@ -70,9 +80,11 @@ pub const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); /// proposer. Having a capacity > 1 is an optimization to prevent sync lookup from having re-fetch /// data during moments of unstable network conditions. pub struct DataAvailabilityChecker { + complete_blob_backfill: bool, availability_cache: Arc>, slot_clock: T::SlotClock, kzg: Arc, + custody_context: Arc>, spec: Arc, } @@ -107,28 +119,39 @@ impl Debug for Availability { impl DataAvailabilityChecker { pub fn new( + complete_blob_backfill: bool, slot_clock: T::SlotClock, kzg: Arc, store: BeaconStore, + custody_context: Arc>, spec: Arc, ) -> Result { - let inner = DataAvailabilityCheckerInner::new(OVERFLOW_LRU_CAPACITY, store, spec.clone())?; + let inner = DataAvailabilityCheckerInner::new( + OVERFLOW_LRU_CAPACITY_NON_ZERO, + store, + custody_context.clone(), + spec.clone(), + )?; Ok(Self { + complete_blob_backfill, availability_cache: Arc::new(inner), slot_clock, kzg, + custody_context, spec, }) } - /// Checks if the block root is currenlty in the availability cache awaiting import because + pub fn custody_context(&self) -> &Arc> { + &self.custody_context + } + + /// Checks if the block root is currently in the availability cache awaiting import because /// of missing components. - pub fn get_execution_valid_block( - &self, - block_root: &Hash256, - ) -> Option>> { - self.availability_cache - .get_execution_valid_block(block_root) + /// + /// Returns the cache block wrapped in a `BlockProcessStatus` enum if it exists. + pub fn get_cached_block(&self, block_root: &Hash256) -> Option> { + self.availability_cache.get_cached_block(block_root) } /// Return the set of cached blob indexes for `block_root`. Returns None if there is no block @@ -155,6 +178,21 @@ impl DataAvailabilityChecker { }) } + /// Check if the exact data column is in the availability cache. + pub fn is_data_column_cached( + &self, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.is_some_and(|components| { + let cached_column_opt = components.get_cached_data_column(data_column.index); + cached_column_opt.is_some_and(|cached| *cached == *data_column) + }) + }) + } + /// Get a blob from the availability cache. pub fn get_blob( &self, @@ -173,6 +211,7 @@ impl DataAvailabilityChecker { /// Put a list of blobs received via RPC into the availability cache. This performs KZG /// verification on the blobs in the list. + #[instrument(skip_all, level = "trace")] pub fn put_rpc_blobs( &self, block_root: Hash256, @@ -200,17 +239,29 @@ impl DataAvailabilityChecker { /// Put a list of custody columns received via RPC into the availability cache. This performs KZG /// verification on the blobs in the list. #[allow(clippy::type_complexity)] + #[instrument(skip_all, level = "trace")] pub fn put_rpc_custody_columns( &self, block_root: Hash256, + slot: Slot, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { // Attributes fault to the specific peer that sent an invalid column - let kzg_verified_columns = KzgVerifiedDataColumn::from_batch(custody_columns, &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; + let kzg_verified_columns = + KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + // Filter out columns that aren't required for custody for this slot + // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ + // not be yet effective for data availability check, as CGC changes are only effecive from + // a new epoch. + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); let verified_custody_columns = kzg_verified_columns .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) .collect::>(); @@ -218,65 +269,32 @@ impl DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Put a list of blobs received from the EL pool into the availability cache. - /// - /// This DOES NOT perform KZG verification because the KZG proofs should have been constructed - /// immediately prior to calling this function so they are assumed to be valid. - pub fn put_engine_blobs( - &self, - block_root: Hash256, - blobs: FixedBlobSidecarList, - ) -> Result, AvailabilityCheckError> { - let seen_timestamp = self - .slot_clock - .now_duration() - .ok_or(AvailabilityCheckError::SlotClockError)?; - self.availability_cache.put_kzg_verified_blobs( - block_root, - KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp), - ) - } - - /// Put a list of data columns computed from blobs received from the EL pool into the - /// availability cache. - /// - /// This DOES NOT perform KZG proof and inclusion proof verification because - /// - The KZG proofs should have been verified by the trusted EL. - /// - The KZG commitments inclusion proof should have been constructed immediately prior to - /// calling this function so they are assumed to be valid. - /// - /// This method is used if the EL already has the blobs and returns them via the `getBlobsV2` - /// engine method. - /// More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). - pub fn put_engine_data_columns( - &self, - block_root: Hash256, - data_columns: DataColumnSidecarList, - ) -> Result, AvailabilityCheckError> { - let kzg_verified_custody_columns = data_columns - .into_iter() - .map(|d| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::from_verified(d), - ) - }) - .collect::>(); - - self.availability_cache - .put_kzg_verified_data_columns(block_root, kzg_verified_custody_columns) - } - /// Check if we've cached other blobs for this block. If it completes a set and we also /// have a block cached, return the `Availability` variant triggering block import. /// Otherwise cache the blob sidecar. /// /// This should only accept gossip verified blobs, so we should not have to worry about dupes. - pub fn put_gossip_blob( + #[instrument(skip_all, level = "trace")] + pub fn put_gossip_verified_blobs< + I: IntoIterator>, + O: ObservationStrategy, + >( &self, - gossip_blob: GossipVerifiedBlob, + block_root: Hash256, + blobs: I, ) -> Result, AvailabilityCheckError> { self.availability_cache - .put_kzg_verified_blobs(gossip_blob.block_root(), vec![gossip_blob.into_inner()]) + .put_kzg_verified_blobs(block_root, blobs.into_iter().map(|b| b.into_inner())) + } + + #[instrument(skip_all, level = "trace")] + pub fn put_kzg_verified_blobs>>( + &self, + block_root: Hash256, + blobs: I, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_kzg_verified_blobs(block_root, blobs) } /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also @@ -284,14 +302,23 @@ impl DataAvailabilityChecker { /// Otherwise cache the data column sidecar. /// /// This should only accept gossip verified data columns, so we should not have to worry about dupes. - #[allow(clippy::type_complexity)] - pub fn put_gossip_data_columns( + #[instrument(skip_all, level = "trace")] + pub fn put_gossip_verified_data_columns< + O: ObservationStrategy, + I: IntoIterator>, + >( &self, block_root: Hash256, - gossip_data_columns: Vec>, + slot: Slot, + data_columns: I, ) -> Result, AvailabilityCheckError> { - let custody_columns = gossip_data_columns + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let custody_columns = data_columns .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) .collect::>(); @@ -299,19 +326,44 @@ impl DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, custody_columns) } + #[instrument(skip_all, level = "trace")] + pub fn put_kzg_verified_custody_data_columns< + I: IntoIterator>, + >( + &self, + block_root: Hash256, + custody_columns: I, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + /// Check if we have all the blobs for a block. Returns `Availability` which has information /// about whether all components have been received or more are required. - pub fn put_pending_executed_block( + pub fn put_executed_block( &self, executed_block: AvailabilityPendingExecutedBlock, ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_pending_executed_block(executed_block) + self.availability_cache.put_executed_block(executed_block) } - pub fn remove_pending_components(&self, block_root: Hash256) { + /// Inserts a pre-execution block into the cache. + /// This does NOT override an existing executed block. + pub fn put_pre_execution_block( + &self, + block_root: Hash256, + block: Arc>, + source: BlockImportSource, + ) -> Result<(), Error> { self.availability_cache - .remove_pending_components(block_root) + .put_pre_execution_block(block_root, block, source) + } + + /// Removes a pre-execution block from the cache. + /// This does NOT remove an existing executed block. + pub fn remove_block_on_execution_error(&self, block_root: &Hash256) { + self.availability_cache + .remove_pre_execution_block(block_root); } /// Verifies kzg commitments for an RpcBlock, returns a `MaybeAvailableBlock` that may @@ -323,7 +375,6 @@ impl DataAvailabilityChecker { &self, block: RpcBlock, ) -> Result, AvailabilityCheckError> { - let custody_columns_count = block.custody_columns_count(); let (block_root, block, blobs, data_columns) = block.deconstruct(); if self.blobs_required_for_block(&block) { return if let Some(blob_list) = blobs { @@ -337,16 +388,12 @@ impl DataAvailabilityChecker { spec: self.spec.clone(), })) } else { - Ok(MaybeAvailableBlock::AvailabilityPending { - block_root, - block, - custody_columns_count, - }) + Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) }; } if self.data_columns_required_for_block(&block) { return if let Some(data_column_list) = data_columns.as_ref() { - verify_kzg_for_data_column_list_with_scoring( + verify_kzg_for_data_column_list( data_column_list .iter() .map(|custody_column| custody_column.as_data_column()), @@ -366,11 +413,7 @@ impl DataAvailabilityChecker { spec: self.spec.clone(), })) } else { - Ok(MaybeAvailableBlock::AvailabilityPending { - block_root, - block, - custody_columns_count, - }) + Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) }; } @@ -389,6 +432,7 @@ impl DataAvailabilityChecker { /// /// WARNING: This function assumes all required blobs are already present, it does NOT /// check if there are any missing blobs. + #[instrument(skip_all)] pub fn verify_kzg_for_rpc_blocks( &self, blocks: Vec>, @@ -416,18 +460,15 @@ impl DataAvailabilityChecker { .flatten() .map(CustodyDataColumn::into_inner) .collect::>(); - let all_data_columns = - RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns as usize); // verify kzg for all data columns at once if !all_data_columns.is_empty() { // Attributes fault to the specific peer that sent an invalid column - verify_kzg_for_data_column_list_with_scoring(all_data_columns.iter(), &self.kzg) + verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidColumn)?; } for block in blocks { - let custody_columns_count = block.custody_columns_count(); let (block_root, block, blobs, data_columns) = block.deconstruct(); let maybe_available_block = if self.blobs_required_for_block(&block) { @@ -440,11 +481,7 @@ impl DataAvailabilityChecker { spec: self.spec.clone(), }) } else { - MaybeAvailableBlock::AvailabilityPending { - block_root, - block, - custody_columns_count, - } + MaybeAvailableBlock::AvailabilityPending { block_root, block } } } else if self.data_columns_required_for_block(&block) { if let Some(data_columns) = data_columns { @@ -458,11 +495,7 @@ impl DataAvailabilityChecker { spec: self.spec.clone(), }) } else { - MaybeAvailableBlock::AvailabilityPending { - block_root, - block, - custody_columns_count, - } + MaybeAvailableBlock::AvailabilityPending { block_root, block } } } else { MaybeAvailableBlock::Available(AvailableBlock { @@ -507,13 +540,14 @@ impl DataAvailabilityChecker { /// `None` if the `Deneb` fork is disabled. pub fn data_availability_boundary(&self) -> Option { let fork_epoch = self.spec.deneb_fork_epoch?; - let current_slot = self.slot_clock.now()?; - Some(std::cmp::max( - fork_epoch, - current_slot - .epoch(T::EthSpec::slots_per_epoch()) - .saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), - )) + + if self.complete_blob_backfill { + Some(fork_epoch) + } else { + let current_epoch = self.slot_clock.now()?.epoch(T::EthSpec::slots_per_epoch()); + self.spec + .min_epoch_data_availability_boundary(current_epoch) + } } /// Returns true if the given epoch lies within the da boundary and false otherwise. @@ -540,6 +574,7 @@ impl DataAvailabilityChecker { } } + #[instrument(skip_all, level = "debug")] pub fn reconstruct_data_columns( &self, block_root: &Hash256, @@ -576,44 +611,50 @@ impl DataAvailabilityChecker { // Check indices from cache again to make sure we don't publish components we've already received. let Some(existing_column_indices) = self.cached_data_column_indexes(block_root) else { - return Ok(DataColumnReconstructionResult::RecoveredColumnsNotImported( - "block already imported", + return Err(AvailabilityCheckError::Unexpected( + "block no longer exists in the data availability checker".to_string(), )); }; - let data_columns_to_publish = all_data_columns - .into_iter() - .filter(|d| !existing_column_indices.contains(&d.index())) - .collect::>(); - - let Some(slot) = data_columns_to_publish - .first() - .map(|d| d.as_data_column().slot()) - else { + let Some(slot) = all_data_columns.first().map(|d| d.as_data_column().slot()) else { return Ok(DataColumnReconstructionResult::RecoveredColumnsNotImported( "No new columns to import and publish", )); }; + let columns_to_sample = self + .custody_context() + .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch()), &self.spec); + + // We only need to import and publish columns that we need to sample + // and columns that we haven't already received + let data_columns_to_import_and_publish = all_data_columns + .into_iter() + .filter(|d| { + columns_to_sample.contains(&d.index()) + && !existing_column_indices.contains(&d.index()) + }) + .collect::>(); + metrics::stop_timer(timer); metrics::inc_counter_by( &metrics::DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS, - data_columns_to_publish.len() as u64, + data_columns_to_import_and_publish.len() as u64, ); debug!( - count = data_columns_to_publish.len(), + count = data_columns_to_import_and_publish.len(), ?block_root, %slot, "Reconstructed columns" ); self.availability_cache - .put_kzg_verified_data_columns(*block_root, data_columns_to_publish.clone()) + .put_kzg_verified_data_columns(*block_root, data_columns_to_import_and_publish.clone()) .map(|availability| { DataColumnReconstructionResult::Success(( availability, - data_columns_to_publish + data_columns_to_import_and_publish .into_iter() .map(|d| d.clone_arc()) .collect::>(), @@ -636,14 +677,7 @@ pub fn start_availability_cache_maintenance_service( if chain.spec.deneb_fork_epoch.is_some() { let overflow_cache = chain.data_availability_checker.availability_cache.clone(); executor.spawn( - async move { - availability_cache_maintenance_service(chain, overflow_cache) - .instrument(info_span!( - "DataAvailabilityChecker", - service = "data_availability_checker" - )) - .await - }, + async move { availability_cache_maintenance_service(chain, overflow_cache).await }, "availability_cache_service", ); } else { @@ -690,15 +724,17 @@ async fn availability_cache_maintenance_service( .fork_choice_read_lock() .finalized_checkpoint() .epoch; + + let Some(min_epochs_for_blobs) = chain + .spec + .min_epoch_data_availability_boundary(current_epoch) + else { + // Shutdown service if deneb fork epoch not set. Unreachable as the same check is performed above. + break; + }; + // any data belonging to an epoch before this should be pruned - let cutoff_epoch = std::cmp::max( - finalized_epoch + 1, - std::cmp::max( - current_epoch - .saturating_sub(chain.spec.min_epochs_for_blob_sidecars_requests), - deneb_fork_epoch, - ), - ); + let cutoff_epoch = std::cmp::max(finalized_epoch + 1, min_epochs_for_blobs); if let Err(e) = overflow_cache.do_maintenance(cutoff_epoch) { error!(error = ?e,"Failed to maintain availability cache"); @@ -812,7 +848,6 @@ pub enum MaybeAvailableBlock { AvailabilityPending { block_root: Hash256, block: Arc>, - custody_columns_count: usize, }, } @@ -824,3 +859,344 @@ impl MaybeAvailableBlock { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::CustodyContext; + use crate::custody_context::NodeCustodyType; + use crate::test_utils::{ + EphemeralHarnessType, NumBlobs, generate_data_column_indices_rand_order, + generate_rand_block_and_data_columns, get_kzg, + }; + use rand::SeedableRng; + use rand::prelude::StdRng; + use slot_clock::{SlotClock, TestingSlotClock}; + use std::collections::HashSet; + use std::sync::Arc; + use std::time::Duration; + use store::HotColdDB; + use types::data_column_sidecar::DataColumn; + use types::{ChainSpec, ColumnIndex, EthSpec, ForkName, MainnetEthSpec, Slot}; + + type E = MainnetEthSpec; + type T = EphemeralHarnessType; + + /// Test to verify any extra RPC columns received that are not part of the "effective" CGC for + /// the slot are excluded from import. + #[test] + fn should_exclude_rpc_columns_not_required_for_sampling() { + // SETUP + let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + + let da_checker = new_da_checker(spec.clone()); + let custody_context = &da_checker.custody_context; + + // GIVEN a single 32 ETH validator is attached slot 0 + let epoch = Epoch::new(0); + let validator_0 = 0; + custody_context.register_validators( + vec![(validator_0, 32_000_000_000)], + epoch.start_slot(E::slots_per_epoch()), + &spec, + ); + assert_eq!( + custody_context.num_of_data_columns_to_sample(epoch, &spec), + spec.validator_custody_requirement as usize, + "sampling size should be the minimal custody requirement == 8" + ); + + // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch + let validator_1 = 1; + let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); + custody_context.register_validators( + vec![(validator_1, 32_000_000_000 * 9)], + cgc_change_slot, + &spec, + ); + // AND custody columns (8) and any new extra columns (2) are received via RPC responses. + // NOTE: block lookup uses the **latest** CGC (10) instead of the effective CGC (8) as the slot is unknown. + let (_, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + let block_root = Hash256::random(); + let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); + let requested_columns = &custody_columns[..10]; + da_checker + .put_rpc_custody_columns( + block_root, + cgc_change_slot, + data_columns + .into_iter() + .filter(|d| requested_columns.contains(&d.index)) + .collect(), + ) + .expect("should put rpc custody columns"); + + // THEN the sampling size for the end slot of the same epoch remains unchanged + let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); + assert_eq!( + sampling_columns.len(), + spec.validator_custody_requirement as usize // 8 + ); + // AND any extra columns received via RPC responses are excluded from import. + let actual_cached: HashSet = da_checker + .cached_data_column_indexes(&block_root) + .expect("should have cached data columns") + .into_iter() + .collect(); + let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); + assert_eq!( + actual_cached, expected_sampling_columns, + "should cache only the effective sampling columns" + ); + assert!( + actual_cached.len() < requested_columns.len(), + "extra columns should be excluded" + ) + } + + /// Test to verify any extra gossip columns received that are not part of the "effective" CGC for + /// the slot are excluded from import. + #[test] + fn should_exclude_gossip_columns_not_required_for_sampling() { + // SETUP + let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + + let da_checker = new_da_checker(spec.clone()); + let custody_context = &da_checker.custody_context; + + // GIVEN a single 32 ETH validator is attached slot 0 + let epoch = Epoch::new(0); + let validator_0 = 0; + custody_context.register_validators( + vec![(validator_0, 32_000_000_000)], + epoch.start_slot(E::slots_per_epoch()), + &spec, + ); + assert_eq!( + custody_context.num_of_data_columns_to_sample(epoch, &spec), + spec.validator_custody_requirement as usize, + "sampling size should be the minimal custody requirement == 8" + ); + + // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch + let validator_1 = 1; + let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); + custody_context.register_validators( + vec![(validator_1, 32_000_000_000 * 9)], + cgc_change_slot, + &spec, + ); + // AND custody columns (8) and any new extra columns (2) are received via gossip. + // NOTE: CGC updates results in new topics subscriptions immediately, and extra columns may start to + // arrive via gossip. + let (_, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + let block_root = Hash256::random(); + let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); + let requested_columns = &custody_columns[..10]; + let gossip_columns = data_columns + .into_iter() + .filter(|d| requested_columns.contains(&d.index)) + .map(GossipVerifiedDataColumn::::__new_for_testing) + .collect::>(); + da_checker + .put_gossip_verified_data_columns(block_root, cgc_change_slot, gossip_columns) + .expect("should put gossip custody columns"); + + // THEN the sampling size for the end slot of the same epoch remains unchanged + let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); + assert_eq!( + sampling_columns.len(), + spec.validator_custody_requirement as usize // 8 + ); + // AND any extra columns received via gossip responses are excluded from import. + let actual_cached: HashSet = da_checker + .cached_data_column_indexes(&block_root) + .expect("should have cached data columns") + .into_iter() + .collect(); + let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); + assert_eq!( + actual_cached, expected_sampling_columns, + "should cache only the effective sampling columns" + ); + assert!( + actual_cached.len() < requested_columns.len(), + "extra columns should be excluded" + ) + } + + /// Regression test for KZG verification truncation bug (https://github.com/sigp/lighthouse/pull/7927) + #[test] + fn verify_kzg_for_rpc_blocks_should_not_truncate_data_columns() { + let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let da_checker = new_da_checker(spec.clone()); + + // GIVEN multiple RPC blocks with data columns totalling more than 128 + let blocks_with_columns = (0..2) + .map(|index| { + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let custody_columns = if index == 0 { + // 128 valid data columns in the first block + data_columns + .into_iter() + .map(CustodyDataColumn::from_asserted_custody) + .collect::>() + } else { + // invalid data columns in the second block + data_columns + .into_iter() + .map(|d| { + let invalid_sidecar = DataColumnSidecar { + column: DataColumn::::empty(), + ..d.as_ref().clone() + }; + CustodyDataColumn::from_asserted_custody(Arc::new(invalid_sidecar)) + }) + .collect::>() + }; + + RpcBlock::new_with_custody_columns(None, Arc::new(block), custody_columns) + .expect("should create RPC block with custody columns") + }) + .collect::>(); + + // WHEN verifying all blocks together (totalling 256 data columns) + let verification_result = da_checker.verify_kzg_for_rpc_blocks(blocks_with_columns); + + // THEN batch block verification should fail due to 128 invalid columns in the second block + verification_result.expect_err("should have failed to verify blocks"); + } + + #[test] + fn should_exclude_reconstructed_columns_not_required_for_sampling() { + // SETUP + let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + + let da_checker = new_da_checker(spec.clone()); + let custody_context = &da_checker.custody_context; + + // Set custody requirement to 65 columns (enough to trigger reconstruction) + let epoch = Epoch::new(1); + custody_context.register_validators( + vec![(0, 2_048_000_000_000), (1, 32_000_000_000)], // 64 + 1 + Slot::new(0), + &spec, + ); + let sampling_requirement = custody_context.num_of_data_columns_to_sample(epoch, &spec); + assert_eq!( + sampling_requirement, 65, + "sampling requirement should be 65" + ); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + let block_root = Hash256::random(); + // Add the block to the DA checker + da_checker + .availability_cache + .put_pre_execution_block(block_root, Arc::new(block), BlockImportSource::Gossip) + .expect("should put block"); + + // Add 64 columns to the da checker (enough to be able to reconstruct) + // Order by all_column_indices_ordered, then take first 64 + let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); + let custody_columns = custody_columns + .iter() + .filter_map(|&col_idx| data_columns.iter().find(|d| d.index == col_idx).cloned()) + .take(64) + .map(|d| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(d), + ) + }) + .collect::>(); + + da_checker + .availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + .expect("should put custody columns"); + + // Try reconstrucing + let reconstruction_result = da_checker + .reconstruct_data_columns(&block_root) + .expect("should reconstruct columns"); + + // Reconstruction should succeed + let (_availability, reconstructed_columns) = match reconstruction_result { + DataColumnReconstructionResult::Success(result) => result, + e => { + panic!("Expected successful reconstruction {:?}", e); + } + }; + + // Remaining 64 columns should be reconstructed + assert_eq!( + reconstructed_columns.len(), + sampling_requirement - spec.number_of_custody_groups as usize / 2, + "should reconstruct the remaining 1 columns" + ); + + // Only the columns required for custody (65) should be imported into the cache + let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); + let actual_cached: HashSet = da_checker + .cached_data_column_indexes(&block_root) + .expect("should have cached data columns") + .into_iter() + .collect(); + let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); + assert_eq!( + actual_cached, expected_sampling_columns, + "should cache only the required custody columns, not all reconstructed columns" + ); + } + + fn new_da_checker(spec: Arc) -> DataAvailabilityChecker { + let slot_clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(0), + Duration::from_secs(spec.seconds_per_slot), + ); + let kzg = get_kzg(&spec); + let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); + let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); + let custody_context = Arc::new(CustodyContext::new( + NodeCustodyType::Fullnode, + ordered_custody_column_indices, + &spec, + )); + let complete_blob_backfill = false; + DataAvailabilityChecker::new( + complete_blob_backfill, + slot_clock, + kzg, + store, + custody_context, + spec, + ) + .expect("should initialise data availability checker") + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index d091d6fefb..c9efb7a414 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -4,7 +4,7 @@ use types::{BeaconStateError, ColumnIndex, Hash256}; #[derive(Debug)] pub enum Error { InvalidBlobs(KzgError), - InvalidColumn(Vec<(ColumnIndex, KzgError)>), + InvalidColumn((Option, KzgError)), ReconstructColumnsError(KzgError), KzgCommitmentMismatch { blob_commitment: KzgCommitment, diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 3478c183f3..776fb50f61 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -1,5 +1,6 @@ -use super::state_lru_cache::{DietAvailabilityPendingExecutedBlock, StateLRUCache}; use super::AvailableBlockData; +use super::state_lru_cache::{DietAvailabilityPendingExecutedBlock, StateLRUCache}; +use crate::CustodyContext; use crate::beacon_chain::BeaconStore; use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::{ @@ -7,42 +8,91 @@ use crate::block_verification_types::{ }; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; use crate::data_column_verification::KzgVerifiedCustodyDataColumn; -use crate::BeaconChainTypes; +use crate::{BeaconChainTypes, BlockProcessStatus}; +use lighthouse_tracing::SPAN_PENDING_COMPONENTS; use lru::LruCache; -use parking_lot::RwLock; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use ssz_types::{RuntimeFixedVector, RuntimeVariableList}; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; -use tracing::debug; +use tracing::{Span, debug, debug_span}; +use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobIdentifier; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, - Hash256, RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, + BlobSidecar, BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, + DataColumnSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, }; +#[derive(Clone)] +pub enum CachedBlock { + PreExecution(Arc>, BlockImportSource), + Executed(Box>), +} + +impl CachedBlock { + pub fn get_commitments(&self) -> KzgCommitments { + let block = self.as_block(); + block + .message() + .body() + .blob_kzg_commitments() + .cloned() + .unwrap_or_default() + } + + fn as_block(&self) -> &SignedBeaconBlock { + match self { + CachedBlock::PreExecution(b, _) => b, + CachedBlock::Executed(b) => b.as_block(), + } + } + + pub fn num_blobs_expected(&self) -> usize { + self.as_block() + .message() + .body() + .blob_kzg_commitments() + .map_or(0, |commitments| commitments.len()) + } +} + /// This represents the components of a partially available block /// /// The blobs are all gossip and kzg verified. /// The block has completed all verifications except the availability check. +/// +/// There are currently three distinct hardfork eras that one should take note of: +/// - Pre-Deneb: No availability requirements (Block is immediately available) +/// - Post-Deneb, Pre-PeerDAS: Blobs are needed, but columns are not for the availability check +/// - Post-PeerDAS: Columns are needed, but blobs are not for the availability check +/// +/// Note: from this, one can immediately see that `verified_blobs` and `verified_data_columns` +/// are mutually exclusive. i.e. If we are verifying columns to determine a block's availability +/// we are ignoring the `verified_blobs` field. pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: RuntimeFixedVector>>, pub verified_data_columns: Vec>, - pub executed_block: Option>, + pub block: Option>, pub reconstruction_started: bool, + span: Span, } impl PendingComponents { - /// Returns an immutable reference to the cached block. - pub fn get_cached_block(&self) -> &Option> { - &self.executed_block - } - /// Returns an immutable reference to the fixed vector of cached blobs. pub fn get_cached_blobs(&self) -> &RuntimeFixedVector>> { &self.verified_blobs } + #[cfg(test)] + fn get_diet_block(&self) -> Option<&DietAvailabilityPendingExecutedBlock> { + self.block.as_ref().and_then(|block| match block { + CachedBlock::Executed(block) => Some(block.as_ref()), + _ => None, + }) + } + /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, @@ -54,11 +104,6 @@ impl PendingComponents { .map(|d| d.clone_arc()) } - /// Returns a mutable reference to the cached block. - pub fn get_cached_block_mut(&mut self) -> &mut Option> { - &mut self.executed_block - } - /// Returns a mutable reference to the fixed vector of cached blobs. pub fn get_cached_blobs_mut(&mut self) -> &mut RuntimeFixedVector>> { &mut self.verified_blobs @@ -84,9 +129,21 @@ impl PendingComponents { .collect() } - /// Inserts a block into the cache. - pub fn insert_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { - *self.get_cached_block_mut() = Some(block) + /// Inserts an executed block into the cache. + pub fn insert_executed_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { + self.block = Some(CachedBlock::Executed(Box::new(block))) + } + + /// Inserts a pre-execution block into the cache. + /// This does NOT override an existing executed block. + pub fn insert_pre_execution_block( + &mut self, + block: Arc>, + source: BlockImportSource, + ) { + if self.block.is_none() { + self.block = Some(CachedBlock::PreExecution(block, source)) + } } /// Inserts a blob at a specific index in the cache. @@ -116,12 +173,12 @@ impl PendingComponents { /// 1. The blob entry at the index is empty and no block exists, or /// 2. The block exists and its commitment matches the blob's commitment. pub fn merge_single_blob(&mut self, index: usize, blob: KzgVerifiedBlob) { - if let Some(cached_block) = self.get_cached_block() { + if let Some(cached_block) = &self.block { let block_commitment_opt = cached_block.get_commitments().get(index).copied(); - if let Some(block_commitment) = block_commitment_opt { - if block_commitment == *blob.get_commitment() { - self.insert_blob_at_index(index, blob) - } + if let Some(block_commitment) = block_commitment_opt + && block_commitment == *blob.get_commitment() + { + self.insert_blob_at_index(index, blob) } } else if !self.blob_exists(index) { self.insert_blob_at_index(index, blob) @@ -138,6 +195,7 @@ impl PendingComponents { self.verified_data_columns.push(data_column); } } + Ok(()) } @@ -145,7 +203,7 @@ impl PendingComponents { /// /// Blobs that don't match the new block's commitments are evicted. pub fn merge_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { - self.insert_block(block); + self.insert_executed_block(block); let reinsert = self.get_cached_blobs_mut().take(); self.merge_blobs(reinsert); } @@ -156,27 +214,27 @@ impl PendingComponents { /// WARNING: This function can potentially take a lot of time if the state needs to be /// reconstructed from disk. Ensure you are not holding any write locks while calling this. pub fn make_available( - &mut self, + &self, spec: &Arc, + num_expected_columns_opt: Option, recover: R, ) -> Result>, AvailabilityCheckError> where R: FnOnce( DietAvailabilityPendingExecutedBlock, + &Span, ) -> Result, AvailabilityCheckError>, { - let Some(block) = &self.executed_block else { + let Some(CachedBlock::Executed(block)) = &self.block else { // Block not available yet return Ok(None); }; let num_expected_blobs = block.num_blobs_expected(); - let blob_data = if num_expected_blobs == 0 { Some(AvailableBlockData::NoData) - } else if spec.is_peer_das_enabled_for_epoch(block.epoch()) { + } else if let Some(num_expected_columns) = num_expected_columns_opt { let num_received_columns = self.verified_data_columns.len(); - let num_expected_columns = block.custody_columns_count(); match num_received_columns.cmp(&num_expected_columns) { Ordering::Greater => { // Should never happen @@ -254,8 +312,7 @@ impl PendingComponents { block, import_data, payload_verification_outcome, - custody_columns_count: _, - } = recover(block.clone())?; + } = recover(*block.clone(), &self.span)?; let available_block = AvailableBlock { block_root: self.block_root, @@ -264,6 +321,10 @@ impl PendingComponents { blobs_available_timestamp, spec: spec.clone(), }; + + self.span.in_scope(|| { + debug!("Block and all data components are available"); + }); Ok(Some(AvailableExecutedBlock::new( available_block, import_data, @@ -273,57 +334,53 @@ impl PendingComponents { /// Returns an empty `PendingComponents` object with the given block root. pub fn empty(block_root: Hash256, max_len: usize) -> Self { + let span = debug_span!(parent: None, SPAN_PENDING_COMPONENTS, %block_root); + let _guard = span.clone().entered(); Self { block_root, verified_blobs: RuntimeFixedVector::new(vec![None; max_len]), verified_data_columns: vec![], - executed_block: None, + block: None, reconstruction_started: false, + span, } } - /// Returns the epoch of the block if it is cached, otherwise returns the epoch of the first blob. + /// Returns the epoch of: + /// - The block if it is cached + /// - The first available blob + /// - The first data column + /// Otherwise, returns None pub fn epoch(&self) -> Option { - self.executed_block - .as_ref() - .map(|pending_block| pending_block.as_block().epoch()) - .or_else(|| { - for maybe_blob in self.verified_blobs.iter() { - if maybe_blob.is_some() { - return maybe_blob.as_ref().map(|kzg_verified_blob| { - kzg_verified_blob - .as_blob() - .slot() - .epoch(E::slots_per_epoch()) - }); - } - } + // Get epoch from cached block + if let Some(block) = &self.block { + return Some(block.as_block().epoch()); + } - if let Some(kzg_verified_data_column) = self.verified_data_columns.first() { - let epoch = kzg_verified_data_column.as_data_column().epoch(); - return Some(epoch); - } + // Or, get epoch from first available blob + if let Some(blob) = self.verified_blobs.iter().flatten().next() { + return Some(blob.as_blob().slot().epoch(E::slots_per_epoch())); + } - None - }) + // Or, get epoch from first data column + if let Some(data_column) = self.verified_data_columns.first() { + return Some(data_column.as_data_column().epoch()); + } + + None } - pub fn status_str(&self, block_epoch: Epoch, spec: &ChainSpec) -> String { - let block_count = if self.executed_block.is_some() { 1 } else { 0 }; - if spec.is_peer_das_enabled_for_epoch(block_epoch) { - let custody_columns_count = if let Some(block) = self.get_cached_block() { - &block.custody_columns_count().to_string() - } else { - "?" - }; + pub fn status_str(&self, num_expected_columns_opt: Option) -> String { + let block_count = if self.block.is_some() { 1 } else { 0 }; + if let Some(num_expected_columns) = num_expected_columns_opt { format!( "block {} data_columns {}/{}", block_count, self.verified_data_columns.len(), - custody_columns_count, + num_expected_columns ) } else { - let num_expected_blobs = if let Some(block) = self.get_cached_block() { + let num_expected_blobs = if let Some(block) = &self.block { &block.num_blobs_expected().to_string() } else { "?" @@ -346,6 +403,7 @@ pub struct DataAvailabilityCheckerInner { /// This cache holds a limited number of states in memory and reconstructs them /// from disk when necessary. This is necessary until we merge tree-states state_cache: StateLRUCache, + custody_context: Arc>, spec: Arc, } @@ -362,28 +420,31 @@ impl DataAvailabilityCheckerInner { pub fn new( capacity: NonZeroUsize, beacon_store: BeaconStore, + custody_context: Arc>, spec: Arc, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), state_cache: StateLRUCache::new(beacon_store, spec.clone()), + custody_context, spec, }) } /// Returns true if the block root is known, without altering the LRU ordering - pub fn get_execution_valid_block( - &self, - block_root: &Hash256, - ) -> Option>> { + pub fn get_cached_block(&self, block_root: &Hash256) -> Option> { self.critical .read() .peek(block_root) .and_then(|pending_components| { - pending_components - .executed_block - .as_ref() - .map(|block| block.block_cloned()) + pending_components.block.as_ref().map(|block| match block { + CachedBlock::PreExecution(b, source) => { + BlockProcessStatus::NotValidated(b.clone(), *source) + } + CachedBlock::Executed(b) => { + BlockProcessStatus::ExecutionValidated(b.block_cloned()) + } + }) }) } @@ -453,38 +514,21 @@ impl DataAvailabilityCheckerInner { *blob_opt = Some(blob); } } + let pending_components = + self.update_or_insert_pending_components(block_root, epoch, |pending_components| { + pending_components.merge_blobs(fixed_blobs); + Ok(()) + })?; - let mut write_lock = self.critical.write(); + pending_components.span.in_scope(|| { + debug!( + component = "blobs", + status = pending_components.status_str(None), + "Component added to data availability checker" + ); + }); - // Grab existing entry or create a new entry. - let mut pending_components = write_lock - .pop_entry(&block_root) - .map(|(_, v)| v) - .unwrap_or_else(|| { - PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) - }); - - // Merge in the blobs. - pending_components.merge_blobs(fixed_blobs); - - debug!( - component = "blobs", - ?block_root, - status = pending_components.status_str(epoch, &self.spec), - "Component added to data availability checker" - ); - - if let Some(available_block) = pending_components.make_available(&self.spec, |block| { - self.state_cache.recover_pending_executed_block(block) - })? { - // We keep the pending components in the availability cache during block import (#5845). - write_lock.put(block_root, pending_components); - drop(write_lock); - Ok(Availability::Available(Box::new(available_block))) - } else { - write_lock.put(block_root, pending_components); - Ok(Availability::MissingComponents(block_root)) - } + self.check_availability_and_cache_components(block_root, pending_components, None) } #[allow(clippy::type_complexity)] @@ -500,50 +544,103 @@ impl DataAvailabilityCheckerInner { .peek() .map(|verified_blob| verified_blob.as_data_column().epoch()) else { - // Verified data_columns list should be non-empty. - return Err(AvailabilityCheckError::Unexpected( - "empty columns".to_owned(), - )); + // No columns are processed. This can occur if all received columns were filtered out + // before this point, e.g. due to a CGC change that caused extra columns to be downloaded + // // before the new CGC took effect. + // Return `Ok` without marking the block as available. + return Ok(Availability::MissingComponents(block_root)); }; - let mut write_lock = self.critical.write(); + let pending_components = + self.update_or_insert_pending_components(block_root, epoch, |pending_components| { + pending_components.merge_data_columns(kzg_verified_data_columns) + })?; - // Grab existing entry or create a new entry. - let mut pending_components = write_lock - .pop_entry(&block_root) - .map(|(_, v)| v) - .unwrap_or_else(|| { - PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) - }); + let num_expected_columns = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); - // Merge in the data columns. - pending_components.merge_data_columns(kzg_verified_data_columns)?; + pending_components.span.in_scope(|| { + debug!( + component = "data_columns", + status = pending_components.status_str(Some(num_expected_columns)), + "Component added to data availability checker" + ); + }); - debug!( - component = "data_columns", - ?block_root, - status = pending_components.status_str(epoch, &self.spec), - "Component added to data availability checker" - ); + self.check_availability_and_cache_components( + block_root, + pending_components, + Some(num_expected_columns), + ) + } - if let Some(available_block) = pending_components.make_available(&self.spec, |block| { - self.state_cache.recover_pending_executed_block(block) - })? { - // We keep the pending components in the availability cache during block import (#5845). - write_lock.put(block_root, pending_components); - drop(write_lock); + fn check_availability_and_cache_components( + &self, + block_root: Hash256, + pending_components: MappedRwLockReadGuard<'_, PendingComponents>, + num_expected_columns_opt: Option, + ) -> Result, AvailabilityCheckError> { + if let Some(available_block) = pending_components.make_available( + &self.spec, + num_expected_columns_opt, + |block, span| self.state_cache.recover_pending_executed_block(block, span), + )? { + // Explicitly drop read lock before acquiring write lock + drop(pending_components); + if let Some(components) = self.critical.write().get_mut(&block_root) { + // Clean up span now that block is available + components.span = Span::none(); + } + + // We never remove the pending components manually to avoid race conditions. + // This ensures components remain available during and right after block import, + // preventing a race condition where a component was removed after the block was + // imported, but re-inserted immediately, causing partial pending components to be + // stored and served to peers. + // Components are only removed via LRU eviction as finality advances. Ok(Availability::Available(Box::new(available_block))) } else { - write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) } } + /// Updates or inserts a new `PendingComponents` if it doesn't exist, and then apply the + /// `update_fn` while holding the write lock. + /// + /// Once the update is complete, the write lock is downgraded and a read guard with a + /// reference of the updated `PendingComponents` is returned. + fn update_or_insert_pending_components( + &self, + block_root: Hash256, + epoch: Epoch, + update_fn: F, + ) -> Result>, AvailabilityCheckError> + where + F: FnOnce(&mut PendingComponents) -> Result<(), AvailabilityCheckError>, + { + let mut write_lock = self.critical.write(); + + { + let pending_components = write_lock.get_or_insert_mut(block_root, || { + PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) + }); + update_fn(pending_components)? + } + + RwLockReadGuard::try_map(RwLockWriteGuard::downgrade(write_lock), |cache| { + cache.peek(&block_root) + }) + .map_err(|_| { + AvailabilityCheckError::Unexpected("pending components should exist".to_string()) + }) + } + /// Check whether data column reconstruction should be attempted. /// - /// Potentially trigger reconstruction if: - /// - Our custody requirement is all columns (supernode), and we haven't got all columns - /// - We have >= 50% of columns, but not all columns + /// Potentially trigger reconstruction if all the following satisfy: + /// - Our custody requirement is more than 50% of total columns, + /// - We haven't received all required columns /// - Reconstruction hasn't been started for the block /// /// If reconstruction is required, returns `PendingComponents` which contains the @@ -558,15 +655,25 @@ impl DataAvailabilityCheckerInner { return ReconstructColumnsDecision::No("block already imported"); }; - // If we're sampling all columns, it means we must be custodying all columns. - let total_column_count = self.spec.number_of_columns as usize; + let Some(epoch) = pending_components + .verified_data_columns + .first() + .map(|c| c.as_data_column().epoch()) + else { + return ReconstructColumnsDecision::No("not enough columns"); + }; + + let total_column_count = T::EthSpec::number_of_columns(); + let sampling_column_count = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); let received_column_count = pending_components.verified_data_columns.len(); if pending_components.reconstruction_started { return ReconstructColumnsDecision::No("already started"); } - if received_column_count >= total_column_count { - return ReconstructColumnsDecision::No("all columns received"); + if received_column_count >= sampling_column_count { + return ReconstructColumnsDecision::No("all sampling columns received"); } if received_column_count < total_column_count / 2 { return ReconstructColumnsDecision::No("not enough columns"); @@ -586,13 +693,50 @@ impl DataAvailabilityCheckerInner { } } + /// Inserts a pre executed block into the cache. + /// - This does NOT trigger the availability check as the block still needs to be executed. + /// - This does NOT override an existing cached block to avoid overwriting an executed block. + pub fn put_pre_execution_block( + &self, + block_root: Hash256, + block: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + let epoch = block.epoch(); + let pending_components = + self.update_or_insert_pending_components(block_root, epoch, |pending_components| { + pending_components.insert_pre_execution_block(block, source); + Ok(()) + })?; + + let num_expected_columns_opt = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "pre execution block", + status = pending_components.status_str(num_expected_columns_opt), + "Component added to data availability checker" + ); + }); + + Ok(()) + } + + /// Removes a pre-execution block from the cache. + /// This does NOT remove an existing executed block. + pub fn remove_pre_execution_block(&self, block_root: &Hash256) { + // The read lock is immediately dropped so we can safely remove the block from the cache. + if let Some(BlockProcessStatus::NotValidated(_, _)) = self.get_cached_block(block_root) { + self.critical.write().pop(block_root); + } + } + /// Check if we have all the blobs for a block. If we do, return the Availability variant that /// triggers import of the block. - pub fn put_pending_executed_block( + pub fn put_executed_block( &self, executed_block: AvailabilityPendingExecutedBlock, ) -> Result, AvailabilityCheckError> { - let mut write_lock = self.critical.write(); let epoch = executed_block.as_block().epoch(); let block_root = executed_block.import_data.block_root; @@ -601,40 +745,38 @@ impl DataAvailabilityCheckerInner { .state_cache .register_pending_executed_block(executed_block); - // Grab existing entry or create a new entry. - let mut pending_components = write_lock - .pop_entry(&block_root) - .map(|(_, v)| v) - .unwrap_or_else(|| { - PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) - }); + let pending_components = + self.update_or_insert_pending_components(block_root, epoch, |pending_components| { + pending_components.merge_block(diet_executed_block); + Ok(()) + })?; - // Merge in the block. - pending_components.merge_block(diet_executed_block); + let num_expected_columns_opt = self.get_num_expected_columns(epoch); - debug!( - component = "block", - ?block_root, - status = pending_components.status_str(epoch, &self.spec), - "Component added to data availability checker" - ); + pending_components.span.in_scope(|| { + debug!( + component = "block", + status = pending_components.status_str(num_expected_columns_opt), + "Component added to data availability checker" + ); + }); - // Check if we have all components and entire set is consistent. - if let Some(available_block) = pending_components.make_available(&self.spec, |block| { - self.state_cache.recover_pending_executed_block(block) - })? { - // We keep the pending components in the availability cache during block import (#5845). - write_lock.put(block_root, pending_components); - drop(write_lock); - Ok(Availability::Available(Box::new(available_block))) - } else { - write_lock.put(block_root, pending_components); - Ok(Availability::MissingComponents(block_root)) - } + self.check_availability_and_cache_components( + block_root, + pending_components, + num_expected_columns_opt, + ) } - pub fn remove_pending_components(&self, block_root: Hash256) { - self.critical.write().pop_entry(&block_root); + fn get_num_expected_columns(&self, epoch: Epoch) -> Option { + if self.spec.is_peer_das_enabled_for_epoch(epoch) { + let num_of_column_samples = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); + Some(num_of_column_samples) + } else { + None + } } /// maintain the cache @@ -646,10 +788,10 @@ impl DataAvailabilityCheckerInner { let mut write_lock = self.critical.write(); let mut keys_to_remove = vec![]; for (key, value) in write_lock.iter() { - if let Some(epoch) = value.epoch() { - if epoch < cutoff_epoch { - keys_to_remove.push(*key); - } + if let Some(epoch) = value.epoch() + && epoch < cutoff_epoch + { + keys_to_remove.push(*key); } } // Now remove keys @@ -681,26 +823,27 @@ impl DataAvailabilityCheckerInner { mod test { use super::*; + use crate::test_utils::generate_data_column_indices_rand_order; use crate::{ blob_verification::GossipVerifiedBlob, block_verification::PayloadVerificationOutcome, block_verification_types::{AsBlock, BlockImportData}, - data_availability_checker::STATE_LRU_CAPACITY, - eth1_finalization_cache::Eth1FinalizationData, + custody_context::NodeCustodyType, + data_availability_checker::STATE_LRU_CAPACITY_NON_ZERO, test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; use fork_choice::PayloadVerificationStatus; use logging::create_test_tracing_subscriber; use state_processing::ConsensusContext; use std::collections::VecDeque; - use store::{database::interface::BeaconNodeBackend, HotColdDB, ItemStore, StoreConfig}; - use tempfile::{tempdir, TempDir}; - use tracing::info; + use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; + use tempfile::{TempDir, tempdir}; + use tracing::{debug_span, info}; use types::non_zero_usize::new_non_zero_usize; use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; - const DEFAULT_TEST_CUSTODY_COLUMN_COUNT: usize = 8; + const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); fn get_store_with_spec( db_path: &TempDir, @@ -797,11 +940,6 @@ mod test { .expect("should get block") .expect("should have block"); - let parent_eth1_finalization_data = Eth1FinalizationData { - eth1_data: parent_block.message().body().eth1_data().clone(), - eth1_deposit_index: 0, - }; - let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness .add_block_at_slot(target_slot, parent_state) .await @@ -848,7 +986,6 @@ mod test { block_root, state, parent_block, - parent_eth1_finalization_data, consensus_context, }; @@ -861,7 +998,6 @@ mod test { block, import_data, payload_verification_outcome, - custody_columns_count: DEFAULT_TEST_CUSTODY_COLUMN_COUNT, }; (availability_pending_block, gossip_verified_blobs) @@ -877,10 +1013,10 @@ mod test { where E: EthSpec, T: BeaconChainTypes< - HotStore = BeaconNodeBackend, - ColdStore = BeaconNodeBackend, - EthSpec = E, - >, + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, + EthSpec = E, + >, { create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); @@ -888,9 +1024,19 @@ mod test { let spec = harness.spec.clone(); let test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); + let custody_context = Arc::new(CustodyContext::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + )); let cache = Arc::new( - DataAvailabilityCheckerInner::::new(capacity_non_zero, test_store, spec.clone()) - .expect("should create cache"), + DataAvailabilityCheckerInner::::new( + capacity_non_zero, + test_store, + custody_context, + spec.clone(), + ) + .expect("should create cache"), ); (harness, cache, chain_db_path) } @@ -913,7 +1059,7 @@ mod test { ); assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache - .put_pending_executed_block(pending_block) + .put_executed_block(pending_block) .expect("should put block"); if blobs_expected == 0 { assert!( @@ -925,13 +1071,6 @@ mod test { 1, "cache should still have block as it hasn't been imported yet" ); - // remove the blob to simulate successful import - cache.remove_pending_components(root); - assert_eq!( - cache.critical.read().len(), - 0, - "cache should be empty now that block has been imported" - ); } else { assert!( matches!(availability, Availability::MissingComponents(_)), @@ -961,12 +1100,6 @@ mod test { assert_eq!(cache.critical.read().len(), 1); } } - // remove the blob to simulate successful import - cache.remove_pending_components(root); - assert!( - cache.critical.read().is_empty(), - "cache should be empty now that all components available" - ); let (pending_block, blobs) = availability_pending_block(&harness).await; let blobs_expected = pending_block.num_blobs_expected(); @@ -986,10 +1119,14 @@ mod test { matches!(availability, Availability::MissingComponents(_)), "should be pending block" ); - assert_eq!(cache.critical.read().len(), 1); + assert_eq!( + cache.critical.read().len(), + 2, + "cache should have two blocks now" + ); } let availability = cache - .put_pending_executed_block(pending_block) + .put_executed_block(pending_block) .expect("should put block"); assert!( matches!(availability, Availability::Available(_)), @@ -997,14 +1134,8 @@ mod test { availability ); assert!( - cache.critical.read().len() == 1, - "cache should still have available block until import" - ); - // remove the blob to simulate successful import - cache.remove_pending_components(root); - assert!( - cache.critical.read().is_empty(), - "cache should be empty now that all components available" + cache.critical.read().len() == 2, + "cache should still have available block" ); } @@ -1057,7 +1188,7 @@ mod test { // put the block in the cache let availability = cache - .put_pending_executed_block(pending_block) + .put_executed_block(pending_block) .expect("should put block"); // grab the diet block from the cache for later testing @@ -1065,12 +1196,7 @@ mod test { .critical .read() .peek(&block_root) - .map(|pending_components| { - pending_components - .executed_block - .clone() - .expect("should exist") - }) + .and_then(|pending_components| pending_components.get_diet_block().cloned()) .expect("should exist"); pushed_diet_blocks.push_back(diet_block); @@ -1092,7 +1218,7 @@ mod test { // reconstruct the pending block by replaying the block on the parent state let recovered_pending_block = cache .state_lru_cache() - .recover_pending_executed_block(diet_block) + .recover_pending_executed_block(diet_block, &debug_span!("test")) .expect("should reconstruct pending block"); // assert the recovered state is the same as the original @@ -1118,7 +1244,7 @@ mod test { // recover the pending block from the cache let recovered_pending_block = cache .state_lru_cache() - .recover_pending_executed_block(diet_block) + .recover_pending_executed_block(diet_block, &debug_span!("test")) .expect("should reconstruct pending block"); // assert the recovered state is the same as the original assert_eq!( @@ -1126,33 +1252,23 @@ mod test { states.last(), "recovered state should be the same as the original" ); - // the state should no longer be in the cache - assert!( - state_cache - .read() - .peek(&last_block.as_block().state_root()) - .is_none(), - "last block state should no longer be in cache" - ); } } #[cfg(test)] mod pending_components_tests { use super::*; - use crate::block_verification_types::BlockImportData; - use crate::eth1_finalization_cache::Eth1FinalizationData; - use crate::test_utils::{generate_rand_block_and_blobs, test_spec, NumBlobs}; use crate::PayloadVerificationOutcome; + use crate::block_verification_types::BlockImportData; + use crate::test_utils::{NumBlobs, generate_rand_block_and_blobs, test_spec}; + use fixed_bytes::FixedBytesExtended; use fork_choice::PayloadVerificationStatus; use kzg::KzgCommitment; - use rand::rngs::StdRng; use rand::SeedableRng; + use rand::rngs::StdRng; use state_processing::ConsensusContext; use types::test_utils::TestRandom; - use types::{ - BeaconState, FixedBytesExtended, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot, - }; + use types::{BeaconState, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot}; type E = MainnetEthSpec; @@ -1167,7 +1283,7 @@ mod pending_components_tests { let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); let spec = test_spec::(); let (block, blobs_vec) = - generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng, &spec); + generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); let max_len = spec.max_blobs_per_block(block.epoch()) as usize; let mut blobs: RuntimeFixedVector>>> = RuntimeFixedVector::default(max_len); @@ -1229,24 +1345,18 @@ mod pending_components_tests { block_root: Default::default(), state: BeaconState::new(0, Default::default(), &ChainSpec::minimal()), parent_block: dummy_parent, - parent_eth1_finalization_data: Eth1FinalizationData { - eth1_data: Default::default(), - eth1_deposit_index: 0, - }, consensus_context: ConsensusContext::new(Slot::new(0)), }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }, - // Default custody columns count, doesn't matter here - custody_columns_count: 8, }; (block.into(), blobs, invalid_blobs) } pub fn assert_cache_consistent(cache: PendingComponents, max_len: usize) { - if let Some(cached_block) = cache.get_cached_block() { + if let Some(cached_block) = &cache.block { let cached_block_commitments = cached_block.get_commitments(); for index in 0..max_len { let block_commitment = cached_block_commitments.get(index).copied(); @@ -1352,4 +1462,38 @@ mod pending_components_tests { assert_cache_consistent(cache, max_len); } + + #[test] + fn should_not_insert_pre_execution_block_if_executed_block_exists() { + let (pre_execution_block, blobs, random_blobs, max_len) = pre_setup(); + let (executed_block, _blobs, _random_blobs) = + setup_pending_components(pre_execution_block.clone(), blobs, random_blobs); + + let block_root = pre_execution_block.canonical_root(); + let mut pending_component = >::empty(block_root, max_len); + + let pre_execution_block = Arc::new(pre_execution_block); + pending_component + .insert_pre_execution_block(pre_execution_block.clone(), BlockImportSource::Gossip); + assert!( + matches!( + pending_component.block, + Some(CachedBlock::PreExecution(_, _)) + ), + "pre execution block inserted" + ); + + pending_component.insert_executed_block(executed_block); + assert!( + matches!(pending_component.block, Some(CachedBlock::Executed(_))), + "executed block inserted" + ); + + pending_component + .insert_pre_execution_block(pre_execution_block, BlockImportSource::Gossip); + assert!( + matches!(pending_component.block, Some(CachedBlock::Executed(_))), + "executed block should remain" + ); + } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs index 5fe674f30c..24f9237e3c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs @@ -1,16 +1,15 @@ use crate::block_verification_types::AsBlock; use crate::{ + AvailabilityPendingExecutedBlock, BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, block_verification_types::BlockImportData, data_availability_checker::{AvailabilityCheckError, STATE_LRU_CAPACITY_NON_ZERO}, - eth1_finalization_cache::Eth1FinalizationData, - AvailabilityPendingExecutedBlock, BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, }; use lru::LruCache; use parking_lot::RwLock; use state_processing::BlockReplayer; use std::sync::Arc; use store::OnDiskConsensusContext; -use types::beacon_block_body::KzgCommitments; +use tracing::{Span, debug_span, instrument}; use types::{BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock}; /// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except @@ -21,10 +20,8 @@ pub struct DietAvailabilityPendingExecutedBlock { block: Arc>, state_root: Hash256, parent_block: SignedBeaconBlock>, - parent_eth1_finalization_data: Eth1FinalizationData, consensus_context: OnDiskConsensusContext, payload_verification_outcome: PayloadVerificationOutcome, - custody_columns_count: usize, } /// just implementing the same methods as `AvailabilityPendingExecutedBlock` @@ -45,19 +42,6 @@ impl DietAvailabilityPendingExecutedBlock { .map_or(0, |commitments| commitments.len()) } - pub fn get_commitments(&self) -> KzgCommitments { - self.as_block() - .message() - .body() - .blob_kzg_commitments() - .cloned() - .unwrap_or_default() - } - - pub fn custody_columns_count(&self) -> usize { - self.custody_columns_count - } - /// Returns the epoch corresponding to `self.slot()`. pub fn epoch(&self) -> Epoch { self.block.slot().epoch(E::slots_per_epoch()) @@ -102,12 +86,10 @@ impl StateLRUCache { block: executed_block.block, state_root, parent_block: executed_block.import_data.parent_block, - parent_eth1_finalization_data: executed_block.import_data.parent_eth1_finalization_data, consensus_context: OnDiskConsensusContext::from_consensus_context( executed_block.import_data.consensus_context, ), payload_verification_outcome: executed_block.payload_verification_outcome, - custody_columns_count: executed_block.custody_columns_count, } } @@ -115,12 +97,15 @@ impl StateLRUCache { /// This method will first check the cache and if the state is not found /// it will reconstruct the state by loading the parent state from disk and /// replaying the block. + #[instrument(skip_all, parent = _span, level = "debug")] pub fn recover_pending_executed_block( &self, diet_executed_block: DietAvailabilityPendingExecutedBlock, + _span: &Span, ) -> Result, AvailabilityCheckError> { - let state = if let Some(state) = self.states.write().pop(&diet_executed_block.state_root) { - state + // Keep the state in the cache to prevent reconstruction in race conditions + let state = if let Some(state) = self.states.write().get(&diet_executed_block.state_root) { + state.clone() } else { self.reconstruct_state(&diet_executed_block)? }; @@ -131,18 +116,17 @@ impl StateLRUCache { block_root, state, parent_block: diet_executed_block.parent_block, - parent_eth1_finalization_data: diet_executed_block.parent_eth1_finalization_data, consensus_context: diet_executed_block .consensus_context .into_consensus_context(), }, payload_verification_outcome: diet_executed_block.payload_verification_outcome, - custody_columns_count: diet_executed_block.custody_columns_count, }) } /// Reconstruct the state by loading the parent state from disk and replaying /// the block. + #[instrument(skip_all, level = "debug")] fn reconstruct_state( &self, diet_executed_block: &DietAvailabilityPendingExecutedBlock, @@ -175,8 +159,11 @@ impl StateLRUCache { .state_root_iter(state_roots.into_iter()) .minimal_block_root_verification(); + let block_replayer = debug_span!("reconstruct_state_apply_blocks").in_scope(|| { + block_replayer.apply_blocks(vec![diet_executed_block.block.clone_as_blinded()], None) + }); + block_replayer - .apply_blocks(vec![diet_executed_block.block.clone_as_blinded()], None) .map(|block_replayer| block_replayer.into_state()) .and_then(|mut state| { state @@ -219,12 +206,10 @@ impl From> block: value.block, state_root: value.import_data.state.canonical_root().unwrap(), parent_block: value.import_data.parent_block, - parent_eth1_finalization_data: value.import_data.parent_eth1_finalization_data, consensus_context: OnDiskConsensusContext::from_consensus_context( value.import_data.consensus_context, ), payload_verification_outcome: value.payload_verification_outcome, - custody_columns_count: value.custody_columns_count, } } } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index b43b259cf6..b998602566 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1,25 +1,24 @@ -use crate::beacon_proposer_cache::EpochBlockProposers; use crate::block_verification::{ - cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info, - BlockSlashInfo, + BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info, }; use crate::kzg_utils::{reconstruct_data_columns, validate_data_columns}; use crate::observed_data_sidecars::{ObservationStrategy, Observe}; -use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; -use derivative::Derivative; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; +use educe::Educe; use fork_choice::ProtoBlock; use kzg::{Error as KzgError, Kzg}; use proto_array::Block; use slot_clock::SlotClock; use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; use std::iter; use std::marker::PhantomData; use std::sync::Arc; -use tracing::debug; +use tracing::{debug, instrument}; use types::data_column_sidecar::ColumnIndex; use types::{ BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, - RuntimeVariableList, SignedBeaconBlockHeader, Slot, + SignedBeaconBlockHeader, Slot, }; /// An error occurred while validating a gossip data column. @@ -129,6 +128,10 @@ pub enum GossipDataColumnError { slot: Slot, index: ColumnIndex, }, + /// A column has already been processed from non-gossip source and have not yet been seen on + /// the gossip network. + /// This column should be accepted and forwarded over gossip. + PriorKnownUnpublished, /// Data column index must be between 0 and `NUMBER_OF_COLUMNS` (exclusive). /// /// ## Peer scoring @@ -158,6 +161,15 @@ pub enum GossipDataColumnError { /// /// The column sidecar is invalid and the peer is faulty InconsistentProofsLength { cells_len: usize, proofs_len: usize }, + /// The number of KZG commitments exceeds the maximum number of blobs allowed for the fork. The + /// sidecar is invalid. + /// + /// ## Peer scoring + /// The column sidecar is invalid and the peer is faulty + MaxBlobsPerBlockExceeded { + max_blobs_per_block: usize, + commitments_len: usize, + }, } impl From for GossipDataColumnError { @@ -181,10 +193,20 @@ pub struct GossipVerifiedDataColumn, } +impl Clone for GossipVerifiedDataColumn { + fn clone(&self) -> Self { + Self { + block_root: self.block_root, + data_column: self.data_column.clone(), + _phantom: PhantomData, + } + } +} + impl GossipVerifiedDataColumn { pub fn new( column_sidecar: Arc>, - subnet_id: u64, + subnet_id: DataColumnSubnetId, chain: &BeaconChain, ) -> Result { let header = column_sidecar.signed_block_header.clone(); @@ -200,6 +222,49 @@ impl GossipVerifiedDataColumn ) } + /// Create a `GossipVerifiedDataColumn` from `DataColumnSidecar` for block production ONLY. + /// When publishing a block constructed locally, the EL will have already verified the cell proofs. + /// When publishing a block constructed externally, there will be no columns here. + pub fn new_for_block_publishing( + column_sidecar: Arc>, + chain: &BeaconChain, + ) -> Result { + verify_data_column_sidecar(&column_sidecar, &chain.spec)?; + + // Check if the data column is already in the DA checker cache. This happens when data columns + // are made available through the `engine_getBlobs` method. If it exists in the cache, we know + // it has already passed the gossip checks, even though this particular instance hasn't been + // seen / published on the gossip network yet (passed the `verify_is_unknown_sidecar` check above). + // In this case, we should accept it for gossip propagation. + verify_is_unknown_sidecar(chain, &column_sidecar)?; + + if chain + .data_availability_checker + .is_data_column_cached(&column_sidecar.block_root(), &column_sidecar) + { + // Observe this data column so we don't process it again. + if O::observe() { + observe_gossip_data_column(&column_sidecar, chain)?; + } + return Err(GossipDataColumnError::PriorKnownUnpublished); + } + + Ok(Self { + block_root: column_sidecar.block_root(), + data_column: KzgVerifiedDataColumn::from_execution_verified(column_sidecar), + _phantom: Default::default(), + }) + } + + /// Create a `GossipVerifiedDataColumn` from `DataColumnSidecar` for testing ONLY. + pub fn __new_for_testing(column_sidecar: Arc>) -> Self { + Self { + block_root: column_sidecar.block_root(), + data_column: KzgVerifiedDataColumn::__new_for_testing(column_sidecar), + _phantom: Default::default(), + } + } + pub fn as_data_column(&self) -> &DataColumnSidecar { self.data_column.as_data_column() } @@ -231,31 +296,37 @@ impl GossipVerifiedDataColumn } /// Wrapper over a `DataColumnSidecar` for which we have completed kzg verification. -#[derive(Debug, Derivative, Clone, Encode, Decode)] -#[derivative(PartialEq, Eq)] +#[derive(Debug, Educe, Clone, Encode, Decode)] +#[educe(PartialEq, Eq)] #[ssz(struct_behaviour = "transparent")] pub struct KzgVerifiedDataColumn { data: Arc>, } impl KzgVerifiedDataColumn { - pub fn new(data_column: Arc>, kzg: &Kzg) -> Result { + pub fn new( + data_column: Arc>, + kzg: &Kzg, + ) -> Result, KzgError)> { verify_kzg_for_data_column(data_column, kzg) } - /// Create a `KzgVerifiedDataColumn` from `data_column` that are already KZG verified. - /// - /// This should be used with caution, as used incorrectly it could result in KZG verification - /// being skipped and invalid data_columns being deemed valid. - pub fn from_verified(data_column: Arc>) -> Self { + /// Mark a data column as KZG verified. Caller must ONLY use this on columns constructed + /// from EL blobs. + pub fn from_execution_verified(data_column: Arc>) -> Self { Self { data: data_column } } - pub fn from_batch( + /// Create a `KzgVerifiedDataColumn` from `DataColumnSidecar` for testing ONLY. + pub(crate) fn __new_for_testing(data_column: Arc>) -> Self { + Self { data: data_column } + } + + pub fn from_batch_with_scoring( data_columns: Vec>>, kzg: &Kzg, - ) -> Result, Vec<(ColumnIndex, KzgError)>> { - verify_kzg_for_data_column_list_with_scoring(data_columns.iter(), kzg)?; + ) -> Result, (Option, KzgError)> { + verify_kzg_for_data_column_list(data_columns.iter(), kzg)?; Ok(data_columns .into_iter() .map(|column| Self { data: column }) @@ -278,11 +349,12 @@ impl KzgVerifiedDataColumn { } } -pub type CustodyDataColumnList = RuntimeVariableList>; +pub type CustodyDataColumnList = + VariableList, ::NumberOfColumns>; /// Data column that we must custody -#[derive(Debug, Derivative, Clone, Encode, Decode)] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Educe, Clone, Encode, Decode)] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] #[ssz(struct_behaviour = "transparent")] pub struct CustodyDataColumn { data: Arc>, @@ -311,8 +383,8 @@ impl CustodyDataColumn { } /// Data column that we must custody and has completed kzg verification -#[derive(Debug, Derivative, Clone, Encode, Decode)] -#[derivative(PartialEq, Eq)] +#[derive(Debug, Educe, Clone, Encode, Decode)] +#[educe(PartialEq, Eq)] #[ssz(struct_behaviour = "transparent")] pub struct KzgVerifiedCustodyDataColumn { data: Arc>, @@ -328,7 +400,10 @@ impl KzgVerifiedCustodyDataColumn { } /// Verify a column already marked as custody column - pub fn new(data_column: CustodyDataColumn, kzg: &Kzg) -> Result { + pub fn new( + data_column: CustodyDataColumn, + kzg: &Kzg, + ) -> Result, KzgError)> { verify_kzg_for_data_column(data_column.clone_arc(), kzg)?; Ok(Self { data: data_column.data, @@ -342,7 +417,7 @@ impl KzgVerifiedCustodyDataColumn { ) -> Result>, KzgError> { let all_data_columns = reconstruct_data_columns( kzg, - &partial_set_of_columns + partial_set_of_columns .iter() .map(|d| d.clone_arc()) .collect::>(), @@ -375,24 +450,25 @@ impl KzgVerifiedCustodyDataColumn { /// Complete kzg verification for a `DataColumnSidecar`. /// /// Returns an error if the kzg verification check fails. +#[instrument(skip_all, level = "debug")] pub fn verify_kzg_for_data_column( data_column: Arc>, kzg: &Kzg, -) -> Result, KzgError> { +) -> Result, (Option, KzgError)> { let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES); validate_data_columns(kzg, iter::once(&data_column))?; Ok(KzgVerifiedDataColumn { data: data_column }) } /// Complete kzg verification for a list of `DataColumnSidecar`s. -/// Returns an error if any of the `DataColumnSidecar`s fails kzg verification. +/// Returns an error for the first `DataColumnSidecar`s that fails kzg verification. /// /// Note: This function should be preferred over calling `verify_kzg_for_data_column` /// in a loop since this function kzg verifies a list of data columns more efficiently. pub fn verify_kzg_for_data_column_list<'a, E: EthSpec, I>( data_column_iter: I, kzg: &'a Kzg, -) -> Result<(), KzgError> +) -> Result<(), (Option, KzgError)> where I: Iterator>> + Clone, { @@ -401,41 +477,10 @@ where Ok(()) } -/// Complete kzg verification for a list of `DataColumnSidecar`s. -/// -/// If there's at least one invalid column, it re-verifies all columns individually to identify the -/// first column that is invalid. This is necessary to attribute fault to the specific peer that -/// sent bad data. The re-verification cost should not be significant. If a peer sends invalid data it -/// will be quickly banned. -pub fn verify_kzg_for_data_column_list_with_scoring<'a, E: EthSpec, I>( - data_column_iter: I, - kzg: &'a Kzg, -) -> Result<(), Vec<(ColumnIndex, KzgError)>> -where - I: Iterator>> + Clone, -{ - if verify_kzg_for_data_column_list(data_column_iter.clone(), kzg).is_ok() { - return Ok(()); - }; - - // Find all columns that are invalid and identify by index. If we hit this condition there - // should be at least one invalid column - let errors = data_column_iter - .filter_map(|data_column| { - if let Err(e) = verify_kzg_for_data_column(data_column.clone(), kzg) { - Some((data_column.index, e)) - } else { - None - } - }) - .collect::>(); - - Err(errors) -} - +#[instrument(skip_all, level = "debug")] pub fn validate_data_column_sidecar_for_gossip( data_column: Arc>, - subnet: u64, + subnet: DataColumnSubnetId, chain: &BeaconChain, ) -> Result, GossipDataColumnError> { let column_slot = data_column.slot(); @@ -443,14 +488,31 @@ pub fn validate_data_column_sidecar_for_gossip( data_column: &DataColumnSidecar, spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { - if data_column.index >= spec.number_of_columns { + if data_column.index >= E::number_of_columns() as u64 { return Err(GossipDataColumnError::InvalidColumnIndex(data_column.index)); } if data_column.kzg_commitments.is_empty() { @@ -488,6 +550,14 @@ fn verify_data_column_sidecar( let cells_len = data_column.column.len(); let commitments_len = data_column.kzg_commitments.len(); let proofs_len = data_column.kzg_proofs.len(); + let max_blobs_per_block = spec.max_blobs_per_block(data_column.epoch()) as usize; + + if commitments_len > max_blobs_per_block { + return Err(GossipDataColumnError::MaxBlobsPerBlockExceeded { + max_blobs_per_block, + commitments_len, + }); + } if cells_len != commitments_len { return Err(GossipDataColumnError::InconsistentCommitmentsLength { @@ -506,22 +576,22 @@ fn verify_data_column_sidecar( Ok(()) } -// Verify that this is the first column sidecar received for the tuple: -// (block_header.slot, block_header.proposer_index, column_sidecar.index) -fn verify_is_first_sidecar( +/// Verify that `column_sidecar` is not yet known, i.e. this is the first time `column_sidecar` has been received for the tuple: +/// `(block_header.slot, block_header.proposer_index, column_sidecar.index)` +fn verify_is_unknown_sidecar( chain: &BeaconChain, - data_column: &DataColumnSidecar, + column_sidecar: &DataColumnSidecar, ) -> Result<(), GossipDataColumnError> { if chain .observed_column_sidecars .read() - .proposer_is_known(data_column) + .proposer_is_known(column_sidecar) .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? { return Err(GossipDataColumnError::PriorKnown { - proposer: data_column.block_proposer_index(), - slot: data_column.slot(), - index: data_column.index, + proposer: column_sidecar.block_proposer_index(), + slot: column_sidecar.slot(), + index: column_sidecar.index, }); } Ok(()) @@ -587,64 +657,34 @@ fn verify_proposer_and_signature( let block_root = data_column.block_root(); let block_parent_root = data_column.block_parent_root(); - let proposer_shuffling_root = if parent_block.slot.epoch(slots_per_epoch) == column_epoch { - parent_block - .next_epoch_shuffling_id - .shuffling_decision_block - } else { - parent_block.root - }; + let proposer_shuffling_root = + parent_block.proposer_shuffling_root_for_child_block(column_epoch, &chain.spec); - // We lock the cache briefly to get or insert a OnceCell, then drop the lock - // before doing proposer shuffling calculation via `OnceCell::get_or_try_init`. This avoids - // holding the lock during the computation, while still ensuring the result is cached and - // initialised only once. - // - // This approach exposes the cache internals (`OnceCell` & `EpochBlockProposers`) - // as a trade-off for avoiding lock contention. - let epoch_proposers_cell = chain - .beacon_proposer_cache - .lock() - .get_or_insert_key(column_epoch, proposer_shuffling_root); - - let epoch_proposers = epoch_proposers_cell.get_or_try_init(move || { - debug!( - %block_root, - index = %column_index, - "Proposer shuffling cache miss for column verification" - ); - let (parent_state_root, mut parent_state) = chain - .store - .get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root) - .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? - .ok_or_else(|| { - BeaconChainError::DBInconsistent(format!( - "Missing state for parent block {block_parent_root:?}", - )) - })?; - - let state = cheap_state_advance_to_obtain_committees::<_, GossipDataColumnError>( - &mut parent_state, - Some(parent_state_root), - column_slot, - &chain.spec, - )?; - - let proposers = state.get_beacon_proposer_indices(&chain.spec)?; - // Prime the proposer shuffling cache with the newly-learned value. - Ok::<_, GossipDataColumnError>(EpochBlockProposers { - epoch: column_epoch, - fork: state.fork(), - proposers: proposers.into(), - }) - })?; - - let proposer_index = *epoch_proposers - .proposers - .get(column_slot.as_usize() % slots_per_epoch as usize) - .ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?; - - let fork = epoch_proposers.fork; + let proposer = chain.with_proposer_cache( + proposer_shuffling_root, + column_epoch, + |proposers| proposers.get_slot::(column_slot), + || { + debug!( + %block_root, + index = %column_index, + "Proposer shuffling cache miss for column verification" + ); + chain + .store + .get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root) + .map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))? + .ok_or_else(|| { + GossipDataColumnError::BeaconChainError(Box::new( + BeaconChainError::DBInconsistent(format!( + "Missing state for parent block {block_parent_root:?}", + )), + )) + }) + }, + )?; + let proposer_index = proposer.index; + let fork = proposer.fork; // Signature verify the signed block header. let signature_is_valid = { @@ -680,15 +720,14 @@ fn verify_proposer_and_signature( fn verify_index_matches_subnet( data_column: &DataColumnSidecar, - subnet: u64, + subnet: DataColumnSubnetId, spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { - let expected_subnet: u64 = - DataColumnSubnetId::from_column_index(data_column.index, spec).into(); + let expected_subnet = DataColumnSubnetId::from_column_index(data_column.index, spec); if expected_subnet != subnet { return Err(GossipDataColumnError::InvalidSubnetId { - received: subnet, - expected: expected_subnet, + received: subnet.into(), + expected: expected_subnet.into(), }); } Ok(()) @@ -762,16 +801,22 @@ pub fn observe_gossip_data_column( #[cfg(test)] mod test { use crate::data_column_verification::{ - validate_data_column_sidecar_for_gossip, GossipDataColumnError, + GossipDataColumnError, GossipVerifiedDataColumn, validate_data_column_sidecar_for_gossip, }; use crate::observed_data_sidecars::Observe; - use crate::test_utils::BeaconChainHarness; - use types::{DataColumnSidecar, EthSpec, ForkName, MainnetEthSpec}; + use crate::test_utils::{ + BeaconChainHarness, EphemeralHarnessType, generate_data_column_sidecars_from_block, + }; + use eth2::types::BlobsBundle; + use execution_layer::test_utils::generate_blobs; + use std::sync::Arc; + use types::{DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkName, MainnetEthSpec}; type E = MainnetEthSpec; #[tokio::test] - async fn empty_data_column_sidecars_fails_validation() { + async fn test_validate_data_column_sidecar_for_gossip() { + // Setting up harness is slow, we initialise once and use it for all gossip validation tests. let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); let harness = BeaconChainHarness::builder(E::default()) .spec(spec.into()) @@ -781,20 +826,58 @@ mod test { .build(); harness.advance_slot(); + let verify_fn = |column_sidecar: DataColumnSidecar| { + let col_index = column_sidecar.index; + validate_data_column_sidecar_for_gossip::<_, Observe>( + column_sidecar.into(), + DataColumnSubnetId::from_column_index(col_index, &harness.spec), + &harness.chain, + ) + }; + empty_data_column_sidecars_fails_validation(&harness, &verify_fn).await; + data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await; + } + + #[tokio::test] + async fn test_new_for_block_publishing() { + // Setting up harness is slow, we initialise once and use it for all gossip validation tests. + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.into()) + .deterministic_keypairs(64) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + harness.advance_slot(); + + let verify_fn = |column_sidecar: DataColumnSidecar| { + GossipVerifiedDataColumn::<_>::new_for_block_publishing( + column_sidecar.into(), + &harness.chain, + ) + }; + empty_data_column_sidecars_fails_validation(&harness, &verify_fn).await; + data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await; + } + + async fn empty_data_column_sidecars_fails_validation( + harness: &BeaconChainHarness>, + verify_fn: &impl Fn(DataColumnSidecar) -> Result, + ) { let slot = harness.get_current_slot(); let state = harness.get_current_state(); let ((block, _blobs_opt), _state) = harness .make_block_with_modifier(state, slot, |block| { - *block.body_mut().blob_kzg_commitments_mut().unwrap() = vec![].into(); + *block.body_mut().blob_kzg_commitments_mut().unwrap() = vec![].try_into().unwrap(); }) .await; let index = 0; let column_sidecar = DataColumnSidecar:: { index, - column: vec![].into(), - kzg_commitments: vec![].into(), - kzg_proofs: vec![].into(), + column: vec![].try_into().unwrap(), + kzg_commitments: vec![].try_into().unwrap(), + kzg_proofs: vec![].try_into().unwrap(), signed_block_header: block.signed_block_header(), kzg_commitments_inclusion_proof: block .message() @@ -803,14 +886,49 @@ mod test { .unwrap(), }; - let result = validate_data_column_sidecar_for_gossip::<_, Observe>( - column_sidecar.into(), - index, - &harness.chain, - ); + let result = verify_fn(column_sidecar); assert!(matches!( result.err(), Some(GossipDataColumnError::UnexpectedDataColumn) )); } + + async fn data_column_sidecar_commitments_exceed_max_blobs_per_block( + harness: &BeaconChainHarness>, + verify_fn: &impl Fn(DataColumnSidecar) -> Result, + ) { + let slot = harness.get_current_slot(); + let epoch = slot.epoch(E::slots_per_epoch()); + let state = harness.get_current_state(); + let max_blobs_per_block = harness.spec.max_blobs_per_block(epoch) as usize; + let fork = harness.spec.fork_name_at_epoch(epoch); + + // Generate data column sidecar with blob count exceeding max_blobs_per_block. + let blob_count = max_blobs_per_block + 1; + let BlobsBundle:: { + commitments: preloaded_commitments_single, + proofs: _, + blobs: _, + } = generate_blobs(1, fork).unwrap().0; + + let ((block, _blobs_opt), _state) = harness + .make_block_with_modifier(state, slot, |block| { + *block.body_mut().blob_kzg_commitments_mut().unwrap() = + vec![preloaded_commitments_single[0]; blob_count] + .try_into() + .unwrap(); + }) + .await; + + let column_sidecar = generate_data_column_sidecars_from_block(&block, &harness.spec) + .into_iter() + .next() + .unwrap(); + + let result = verify_fn(Arc::try_unwrap(column_sidecar).unwrap()); + assert!(matches!( + result.err(), + Some(GossipDataColumnError::MaxBlobsPerBlockExceeded { .. }) + )); + } } diff --git a/beacon_node/beacon_chain/src/deneb_readiness.rs b/beacon_node/beacon_chain/src/deneb_readiness.rs deleted file mode 100644 index e11070a1f4..0000000000 --- a/beacon_node/beacon_chain/src/deneb_readiness.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Provides tools for checking if a node is ready for the Deneb upgrade. - -use crate::{BeaconChain, BeaconChainTypes}; -use execution_layer::http::{ - ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_GET_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V3, -}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::time::Duration; -use types::*; - -/// The time before the Deneb fork when we will start issuing warnings about preparation. -use super::bellatrix_readiness::SECONDS_IN_A_WEEK; -pub const DENEB_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; -pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type")] -pub enum DenebReadiness { - /// The execution engine is deneb-enabled (as far as we can tell) - Ready, - /// We are connected to an execution engine which doesn't support the V3 engine api methods - V3MethodsNotSupported { error: String }, - /// The transition configuration with the EL failed, there might be a problem with - /// connectivity, authentication or a difference in configuration. - ExchangeCapabilitiesFailed { error: String }, - /// The user has not configured an execution endpoint - NoExecutionEndpoint, -} - -impl fmt::Display for DenebReadiness { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DenebReadiness::Ready => { - write!(f, "This node appears ready for Deneb.") - } - DenebReadiness::ExchangeCapabilitiesFailed { error } => write!( - f, - "Could not exchange capabilities with the \ - execution endpoint: {}", - error - ), - DenebReadiness::NoExecutionEndpoint => write!( - f, - "The --execution-endpoint flag is not specified, this is a \ - requirement post-merge" - ), - DenebReadiness::V3MethodsNotSupported { error } => write!( - f, - "Execution endpoint does not support Deneb methods: {}", - error - ), - } - } -} - -impl BeaconChain { - /// Returns `true` if deneb epoch is set and Deneb fork has occurred or will - /// occur within `DENEB_READINESS_PREPARATION_SECONDS` - pub fn is_time_to_prepare_for_deneb(&self, current_slot: Slot) -> bool { - if let Some(deneb_epoch) = self.spec.deneb_fork_epoch { - let deneb_slot = deneb_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let deneb_readiness_preparation_slots = - DENEB_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; - // Return `true` if Deneb has happened or is within the preparation time. - current_slot + deneb_readiness_preparation_slots > deneb_slot - } else { - // The Deneb fork epoch has not been defined yet, no need to prepare. - false - } - } - - /// Attempts to connect to the EL and confirm that it is ready for capella. - pub async fn check_deneb_readiness(&self) -> DenebReadiness { - if let Some(el) = self.execution_layer.as_ref() { - match el - .get_engine_capabilities(Some(Duration::from_secs( - ENGINE_CAPABILITIES_REFRESH_INTERVAL, - ))) - .await - { - Err(e) => { - // The EL was either unreachable or responded with an error - DenebReadiness::ExchangeCapabilitiesFailed { - error: format!("{:?}", e), - } - } - Ok(capabilities) => { - let mut missing_methods = String::from("Required Methods Unsupported:"); - let mut all_good = true; - if !capabilities.get_payload_v3 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_GET_PAYLOAD_V3); - all_good = false; - } - if !capabilities.forkchoice_updated_v3 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_FORKCHOICE_UPDATED_V3); - all_good = false; - } - if !capabilities.new_payload_v3 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_NEW_PAYLOAD_V3); - all_good = false; - } - - if all_good { - DenebReadiness::Ready - } else { - DenebReadiness::V3MethodsNotSupported { - error: missing_methods, - } - } - } - } - } else { - DenebReadiness::NoExecutionEndpoint - } - } -} diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index a296163adc..385a89c544 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -3,20 +3,22 @@ use crate::beacon_block_streamer::Error as BlockStreamerError; use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; use crate::data_availability_checker::AvailabilityCheckError; -use crate::eth1_chain::Error as Eth1ChainError; use crate::migrate::PruningError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; use crate::observed_attesters::Error as ObservedAttestersError; use crate::observed_block_producers::Error as ObservedBlockProducersError; use crate::observed_data_sidecars::Error as ObservedDataSidecarsError; +use bls::PublicKeyBytes; use execution_layer::PayloadStatus; use fork_choice::ExecutionStatus; use futures::channel::mpsc::TrySendError; +use milhouse::Error as MilhouseError; use operation_pool::OpPoolError; use safe_arith::ArithError; use ssz_types::Error as SszTypesError; use state_processing::{ + BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, block_signature_verifier::Error as BlockSignatureVerifierError, per_block_processing::errors::{ AttestationValidationError, AttesterSlashingValidationError, @@ -25,11 +27,9 @@ use state_processing::{ }, signature_sets::Error as SignatureSetError, state_advance::Error as StateAdvanceError, - BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, }; use task_executor::ShutdownReason; use tokio::task::JoinError; -use types::milhouse::Error as MilhouseError; use types::*; macro_rules! easy_from_to { @@ -233,6 +233,24 @@ pub enum BeaconChainError { columns_found: usize, }, FailedToReconstructBlobs(String), + ProposerCacheIncorrectState { + state_decision_block_root: Hash256, + requested_decision_block_root: Hash256, + }, + ProposerCacheAccessorFailure { + decision_block_root: Hash256, + proposal_epoch: Epoch, + }, + ProposerCacheOutOfBounds { + slot: Slot, + epoch: Epoch, + }, + ProposerCacheWrongEpoch { + request_epoch: Epoch, + cache_epoch: Epoch, + }, + SkipProposerPreparation, + FailedColumnCustodyInfoUpdate, } easy_from_to!(SlotProcessingError, BeaconChainError); @@ -273,7 +291,6 @@ pub enum BlockProductionError { BlockProcessingError(BlockProcessingError), EpochCacheError(EpochCacheError), ForkChoiceError(ForkChoiceError), - Eth1ChainError(Eth1ChainError), BeaconStateError(BeaconStateError), StateAdvanceError(StateAdvanceError), OpPoolError(OpPoolError), @@ -304,12 +321,14 @@ pub enum BlockProductionError { KzgError(kzg::Error), FailedToBuildBlobSidecars(String), MissingExecutionRequests, + SszTypesError(ssz_types::Error), + // TODO(gloas): Remove this once Gloas is implemented + GloasNotImplemented, } easy_from_to!(BlockProcessingError, BlockProductionError); easy_from_to!(BeaconStateError, BlockProductionError); easy_from_to!(SlotProcessingError, BlockProductionError); -easy_from_to!(Eth1ChainError, BlockProductionError); easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); easy_from_to!(EpochCacheError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs deleted file mode 100644 index 8a79bff4c7..0000000000 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ /dev/null @@ -1,1208 +0,0 @@ -use crate::metrics; -use eth1::{Config as Eth1Config, Eth1Block, Service as HttpService}; -use eth2::lighthouse::Eth1SyncStatusData; -use ethereum_hashing::hash; -use int_to_bytes::int_to_bytes32; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::get_new_eth1_data; -use std::cmp::Ordering; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use store::{DBColumn, Error as StoreError, StoreItem}; -use task_executor::TaskExecutor; -use tracing::{debug, error, trace}; -use types::{ - BeaconState, BeaconStateError, ChainSpec, Deposit, Eth1Data, EthSpec, Hash256, Slot, Unsigned, -}; - -type BlockNumber = u64; -type Eth1DataVoteCount = HashMap<(Eth1Data, BlockNumber), u64>; - -/// We will declare ourself synced with the Eth1 chain, even if we are this many blocks behind. -/// -/// This number (8) was chosen somewhat arbitrarily. -const ETH1_SYNC_TOLERANCE: u64 = 8; - -#[derive(Debug)] -pub enum Error { - /// Unable to return an Eth1Data for the given epoch. - EpochUnavailable, - /// An error from the backend service (e.g., the web3 data fetcher). - BackendError(String), - /// The deposit index of the state is higher than the deposit contract. This is a critical - /// consensus error. - DepositIndexTooHigh, - /// The current state was unable to return the root for the state at the start of the eth1 - /// voting period. - UnableToGetPreviousStateRoot(BeaconStateError), - /// The state required to find the previous eth1 block was not found in the store. - PreviousStateNotInDB(Hash256), - /// There was an error accessing an object in the database. - StoreError(StoreError), - /// The eth1 head block at the start of the eth1 voting period is unknown. - /// - /// The eth1 caches are likely stale. - UnknownVotingPeriodHead, - /// The block that was previously voted into the state is unknown. - /// - /// The eth1 caches are stale, or a junk value was voted into the chain. - UnknownPreviousEth1BlockHash, - /// An arithmetic error occurred. - ArithError(safe_arith::ArithError), -} - -impl From for Error { - fn from(e: safe_arith::ArithError) -> Self { - Self::ArithError(e) - } -} - -/// Returns an `Eth1SyncStatusData` given some parameters: -/// -/// - `latest_cached_block`: The latest eth1 block in our cache, if any. -/// - `head_block`: The block at the very head of our eth1 node (ignoring follow distance, etc). -/// - `genesis_time`: beacon chain genesis time. -/// - `current_slot`: current beacon chain slot. -/// - `spec`: current beacon chain specification. -fn get_sync_status( - latest_cached_block: Option<&Eth1Block>, - head_block: Option<&Eth1Block>, - genesis_time: u64, - current_slot: Option, - spec: &ChainSpec, -) -> Option { - let eth1_follow_distance_seconds = spec - .seconds_per_eth1_block - .saturating_mul(spec.eth1_follow_distance); - - // The voting target timestamp needs to be special-cased when we're before - // genesis (as defined by `current_slot == None`). - // - // For the sake of this status, when prior to genesis we want to invent some voting periods - // that are *before* genesis, so that we can indicate to users that we're actually adequately - // cached for where they are in time. - let voting_target_timestamp = if let Some(current_slot) = current_slot { - let period = E::SlotsPerEth1VotingPeriod::to_u64(); - let voting_period_start_slot = (current_slot / period) * period; - - let period_start = slot_start_seconds( - genesis_time, - spec.seconds_per_slot, - voting_period_start_slot, - ); - - period_start.saturating_sub(eth1_follow_distance_seconds) - } else { - // The number of seconds in an eth1 voting period. - let voting_period_duration = - E::slots_per_eth1_voting_period() as u64 * spec.seconds_per_slot; - - let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); - - // The number of seconds between now and genesis. - let seconds_till_genesis = genesis_time.saturating_sub(now); - - // Determine how many voting periods are contained in distance between - // now and genesis, rounding up. - let voting_periods_past = seconds_till_genesis.div_ceil(voting_period_duration); - - // Return the start time of the current voting period*. - // - // *: This voting period doesn't *actually* exist, we're just using it to - // give useful logs prior to genesis. - genesis_time - .saturating_sub(voting_periods_past * voting_period_duration) - .saturating_sub(eth1_follow_distance_seconds) - }; - - let latest_cached_block_number = latest_cached_block.map(|b| b.number); - let latest_cached_block_timestamp = latest_cached_block.map(|b| b.timestamp); - let head_block_number = head_block.map(|b| b.number); - let head_block_timestamp = head_block.map(|b| b.timestamp); - - let eth1_node_sync_status_percentage = if let Some(head_block) = head_block { - let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); - let head_age = now.saturating_sub(head_block.timestamp); - - if head_age < ETH1_SYNC_TOLERANCE * spec.seconds_per_eth1_block { - // Always indicate we are fully synced if it's within the sync threshold. - 100.0 - } else { - let blocks_behind = head_age - .checked_div(spec.seconds_per_eth1_block) - .unwrap_or(0); - - let part = f64::from(head_block.number as u32); - let whole = f64::from(head_block.number.saturating_add(blocks_behind) as u32); - - if whole > 0.0 { - (part / whole) * 100.0 - } else { - // Avoids a divide-by-zero. - 0.0 - } - } - } else { - // Always return 0% synced if the head block of the eth1 chain is unknown. - 0.0 - }; - - // Lighthouse is "cached and ready" when it has cached enough blocks to cover the start of the - // current voting period. - let lighthouse_is_cached_and_ready = - latest_cached_block_timestamp.is_some_and(|t| t >= voting_target_timestamp); - - Some(Eth1SyncStatusData { - head_block_number, - head_block_timestamp, - latest_cached_block_number, - latest_cached_block_timestamp, - voting_target_timestamp, - eth1_node_sync_status_percentage, - lighthouse_is_cached_and_ready, - }) -} - -#[derive(Encode, Decode, Clone)] -pub struct SszEth1 { - pub use_dummy_backend: bool, - pub backend_bytes: Vec, -} - -impl StoreItem for SszEth1 { - fn db_column() -> DBColumn { - DBColumn::Eth1Cache - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Self::from_ssz_bytes(bytes).map_err(Into::into) - } -} - -/// Holds an `Eth1ChainBackend` and serves requests from the `BeaconChain`. -pub struct Eth1Chain -where - T: Eth1ChainBackend, - E: EthSpec, -{ - backend: T, - /// When `true`, the backend will be ignored and dummy data from the 2019 Canada interop method - /// will be used instead. - use_dummy_backend: bool, - _phantom: PhantomData, -} - -impl Eth1Chain -where - T: Eth1ChainBackend, - E: EthSpec, -{ - pub fn new(backend: T) -> Self { - Self { - backend, - use_dummy_backend: false, - _phantom: PhantomData, - } - } - - pub fn new_dummy(backend: T) -> Self { - Self { - use_dummy_backend: true, - ..Self::new(backend) - } - } - - /// Returns `true` if the "dummy" backend is being used. - pub fn is_dummy_backend(&self) -> bool { - self.use_dummy_backend - } - - /// Returns the `Eth1Data` that should be included in a block being produced for the given - /// `state`. - pub fn eth1_data_for_block_production( - &self, - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { - if self.use_dummy_backend { - let dummy_backend: DummyEth1ChainBackend = DummyEth1ChainBackend::default(); - dummy_backend.eth1_data(state, spec) - } else { - self.backend.eth1_data(state, spec) - } - } - - /// Returns a list of `Deposits` that may be included in a block. - /// - /// Including all of the returned `Deposits` in a block should _not_ cause it to become - /// invalid (i.e., this function should respect the maximum). - /// - /// `eth1_data_vote` is the `Eth1Data` that the block producer would include in their - /// block. This vote may change the `state.eth1_data` value, which would change the deposit - /// count and therefore change the output of this function. - pub fn deposits_for_block_inclusion( - &self, - state: &BeaconState, - eth1_data_vote: &Eth1Data, - spec: &ChainSpec, - ) -> Result, Error> { - if self.use_dummy_backend { - let dummy_backend: DummyEth1ChainBackend = DummyEth1ChainBackend::default(); - dummy_backend.queued_deposits(state, eth1_data_vote, spec) - } else { - self.backend.queued_deposits(state, eth1_data_vote, spec) - } - } - - /// Returns a status indicating how synced our caches are with the eth1 chain. - pub fn sync_status( - &self, - genesis_time: u64, - current_slot: Option, - spec: &ChainSpec, - ) -> Option { - get_sync_status::( - self.backend.latest_cached_block().as_ref(), - self.backend.head_block().as_ref(), - genesis_time, - current_slot, - spec, - ) - } - - /// Instantiate `Eth1Chain` from a persisted `SszEth1`. - /// - /// The `Eth1Chain` will have the same caches as the persisted `SszEth1`. - pub fn from_ssz_container( - ssz_container: &SszEth1, - config: Eth1Config, - spec: Arc, - ) -> Result { - let backend = Eth1ChainBackend::from_bytes(&ssz_container.backend_bytes, config, spec)?; - Ok(Self { - use_dummy_backend: ssz_container.use_dummy_backend, - backend, - _phantom: PhantomData, - }) - } - - /// Return a `SszEth1` containing the state of `Eth1Chain`. - pub fn as_ssz_container(&self) -> SszEth1 { - SszEth1 { - use_dummy_backend: self.use_dummy_backend, - backend_bytes: self.backend.as_bytes(), - } - } - - /// Set in motion the finalization of `Eth1Data`. This method is called during block import - /// so it should be fast. - pub fn finalize_eth1_data(&self, eth1_data: Eth1Data) { - self.backend.finalize_eth1_data(eth1_data); - } - - /// Consumes `self`, returning the backend. - pub fn into_backend(self) -> T { - self.backend - } -} - -pub trait Eth1ChainBackend: Sized + Send + Sync { - /// Returns the `Eth1Data` that should be included in a block being produced for the given - /// `state`. - fn eth1_data(&self, beacon_state: &BeaconState, spec: &ChainSpec) - -> Result; - - /// Returns all `Deposits` between `state.eth1_deposit_index` and - /// `state.eth1_data.deposit_count`. - /// - /// # Note: - /// - /// It is possible that not all returned `Deposits` can be included in a block. E.g., there may - /// be more than `MAX_DEPOSIT_COUNT` or the churn may be too high. - fn queued_deposits( - &self, - beacon_state: &BeaconState, - eth1_data_vote: &Eth1Data, - spec: &ChainSpec, - ) -> Result, Error>; - - /// Returns the latest block stored in the cache. Used to obtain an idea of how up-to-date the - /// beacon node eth1 cache is. - fn latest_cached_block(&self) -> Option; - - /// Set in motion the finalization of `Eth1Data`. This method is called during block import - /// so it should be fast. - fn finalize_eth1_data(&self, eth1_data: Eth1Data); - - /// Returns the block at the head of the chain (ignoring follow distance, etc). Used to obtain - /// an idea of how up-to-date the remote eth1 node is. - fn head_block(&self) -> Option; - - /// Encode the `Eth1ChainBackend` instance to bytes. - fn as_bytes(&self) -> Vec; - - /// Create a `Eth1ChainBackend` instance given encoded bytes. - fn from_bytes(bytes: &[u8], config: Eth1Config, spec: Arc) -> Result; -} - -/// Provides a simple, testing-only backend that generates deterministic, meaningless eth1 data. -/// -/// Never creates deposits, therefore the validator set is static. -/// -/// This was used in the 2019 Canada interop workshops. -pub struct DummyEth1ChainBackend(PhantomData); - -impl Eth1ChainBackend for DummyEth1ChainBackend { - /// Produce some deterministic junk based upon the current epoch. - fn eth1_data(&self, state: &BeaconState, _spec: &ChainSpec) -> Result { - // [New in Electra:EIP6110] - if let Ok(deposit_requests_start_index) = state.deposit_requests_start_index() { - if state.eth1_deposit_index() == deposit_requests_start_index { - return Ok(state.eth1_data().clone()); - } - } - let current_epoch = state.current_epoch(); - let slots_per_voting_period = E::slots_per_eth1_voting_period() as u64; - let current_voting_period: u64 = current_epoch.as_u64() / slots_per_voting_period; - - let deposit_root = hash(&int_to_bytes32(current_voting_period)); - let block_hash = hash(&deposit_root); - - Ok(Eth1Data { - deposit_root: Hash256::from_slice(&deposit_root), - deposit_count: state.eth1_deposit_index(), - block_hash: Hash256::from_slice(&block_hash), - }) - } - - /// The dummy back-end never produces deposits. - fn queued_deposits( - &self, - _: &BeaconState, - _: &Eth1Data, - _: &ChainSpec, - ) -> Result, Error> { - Ok(vec![]) - } - - fn latest_cached_block(&self) -> Option { - None - } - - fn finalize_eth1_data(&self, _eth1_data: Eth1Data) {} - - fn head_block(&self) -> Option { - None - } - - /// Return empty Vec for dummy backend. - fn as_bytes(&self) -> Vec { - Vec::new() - } - - /// Create dummy eth1 backend. - fn from_bytes( - _bytes: &[u8], - _config: Eth1Config, - _spec: Arc, - ) -> Result { - Ok(Self(PhantomData)) - } -} - -impl Default for DummyEth1ChainBackend { - fn default() -> Self { - Self(PhantomData) - } -} - -/// Maintains a cache of eth1 blocks and deposits and provides functions to allow block producers -/// to include new deposits and vote on `Eth1Data`. -/// -/// The `core` connects to some external eth1 client (e.g., Parity/Geth) and polls it for -/// information. -#[derive(Clone)] -pub struct CachingEth1Backend { - pub core: HttpService, - _phantom: PhantomData, -} - -impl CachingEth1Backend { - /// Instantiates `self` with empty caches. - /// - /// Does not connect to the eth1 node or start any tasks to keep the cache updated. - pub fn new(config: Eth1Config, spec: Arc) -> Result { - Ok(Self { - core: HttpService::new(config, spec) - .map_err(|e| format!("Failed to create eth1 http service: {:?}", e))?, - _phantom: PhantomData, - }) - } - - /// Starts the routine which connects to the external eth1 node and updates the caches. - pub fn start(&self, handle: TaskExecutor) { - HttpService::auto_update(self.core.clone(), handle); - } - - /// Instantiates `self` from an existing service. - pub fn from_service(service: HttpService) -> Self { - Self { - core: service, - _phantom: PhantomData, - } - } -} - -impl Eth1ChainBackend for CachingEth1Backend { - fn eth1_data(&self, state: &BeaconState, spec: &ChainSpec) -> Result { - // [New in Electra:EIP6110] - if let Ok(deposit_requests_start_index) = state.deposit_requests_start_index() { - if state.eth1_deposit_index() == deposit_requests_start_index { - return Ok(state.eth1_data().clone()); - } - } - let period = E::SlotsPerEth1VotingPeriod::to_u64(); - let voting_period_start_slot = (state.slot() / period) * period; - let voting_period_start_seconds = slot_start_seconds( - state.genesis_time(), - spec.seconds_per_slot, - voting_period_start_slot, - ); - - let votes_to_consider = { - let blocks = self.core.blocks().read(); - get_votes_to_consider(blocks.iter(), voting_period_start_seconds, spec) - }; - - trace!( - votes_to_consider = votes_to_consider.len(), - "Found eth1 data votes_to_consider" - ); - let valid_votes = collect_valid_votes(state, &votes_to_consider); - - let eth1_data = if let Some(eth1_data) = find_winning_vote(valid_votes) { - eth1_data - } else { - // In this case, there are no valid votes available. - // - // Here we choose the eth1_data corresponding to the latest block in our voting window. - // If no votes exist, choose `state.eth1_data` as default vote. - votes_to_consider - .iter() - .max_by_key(|(_, block_number)| *block_number) - .map(|vote| { - let vote = vote.0.clone(); - debug!( - outcome = "Casting vote corresponding to last candidate eth1 block", - ?vote, - "No valid eth1_data votes" - ); - vote - }) - .unwrap_or_else(|| { - let vote = state.eth1_data().clone(); - error!( - lowest_block_number = self.core.lowest_block_number(), - earliest_block_timestamp = self.core.earliest_block_timestamp(), - genesis_time = state.genesis_time(), - outcome = "casting `state.eth1_data` as eth1 vote", - "No valid eth1_data votes, `votes_to_consider` empty" - ); - metrics::inc_counter(&metrics::DEFAULT_ETH1_VOTES); - vote - }) - }; - - debug!( - deposit_root = ?eth1_data.deposit_root, - deposit_count = eth1_data.deposit_count, - block_hash = ?eth1_data.block_hash, - "Produced vote for eth1 chain" - ); - - Ok(eth1_data) - } - - fn queued_deposits( - &self, - state: &BeaconState, - eth1_data_vote: &Eth1Data, - _spec: &ChainSpec, - ) -> Result, Error> { - let deposit_index = state.eth1_deposit_index(); - let deposit_count = if let Some(new_eth1_data) = get_new_eth1_data(state, eth1_data_vote)? { - new_eth1_data.deposit_count - } else { - state.eth1_data().deposit_count - }; - - // [New in Electra:EIP6110] - let deposit_index_limit = - if let Ok(deposit_requests_start_index) = state.deposit_requests_start_index() { - std::cmp::min(deposit_count, deposit_requests_start_index) - } else { - deposit_count - }; - - match deposit_index.cmp(&deposit_index_limit) { - Ordering::Greater => Err(Error::DepositIndexTooHigh), - Ordering::Equal => Ok(vec![]), - Ordering::Less => { - let next = deposit_index; - let last = std::cmp::min(deposit_index_limit, next + E::MaxDeposits::to_u64()); - - self.core - .deposits() - .read() - .cache - .get_deposits(next, last, deposit_count) - .map_err(|e| Error::BackendError(format!("Failed to get deposits: {:?}", e))) - .map(|(_deposit_root, deposits)| deposits) - } - } - } - - fn latest_cached_block(&self) -> Option { - self.core.latest_cached_block() - } - - /// This only writes the eth1_data to a temporary cache so that the service - /// thread can later do the actual finalizing of the deposit tree. - fn finalize_eth1_data(&self, eth1_data: Eth1Data) { - self.core.set_to_finalize(Some(eth1_data)); - } - - fn head_block(&self) -> Option { - self.core.head_block() - } - - /// Return encoded byte representation of the block and deposit caches. - fn as_bytes(&self) -> Vec { - self.core.as_bytes() - } - - /// Recover the cached backend from encoded bytes. - fn from_bytes(bytes: &[u8], config: Eth1Config, spec: Arc) -> Result { - let inner = HttpService::from_bytes(bytes, config, spec)?; - Ok(Self { - core: inner, - _phantom: PhantomData, - }) - } -} - -/// Get all votes from eth1 blocks which are in the list of candidate blocks for the -/// current eth1 voting period. -/// -/// Returns a hashmap of `Eth1Data` to its associated eth1 `block_number`. -fn get_votes_to_consider<'a, I>( - blocks: I, - voting_period_start_seconds: u64, - spec: &ChainSpec, -) -> HashMap -where - I: DoubleEndedIterator + Clone, -{ - blocks - .rev() - .skip_while(|eth1_block| !is_candidate_block(eth1_block, voting_period_start_seconds, spec)) - .take_while(|eth1_block| is_candidate_block(eth1_block, voting_period_start_seconds, spec)) - .filter_map(|eth1_block| { - eth1_block - .clone() - .eth1_data() - .map(|eth1_data| (eth1_data, eth1_block.number)) - }) - .collect() -} - -/// Collect all valid votes that are cast during the current voting period. -/// Return hashmap with count of each vote cast. -fn collect_valid_votes( - state: &BeaconState, - votes_to_consider: &HashMap, -) -> Eth1DataVoteCount { - let mut valid_votes = HashMap::new(); - state - .eth1_data_votes() - .iter() - .filter_map(|vote| { - votes_to_consider - .get(vote) - .map(|block_num| (vote.clone(), *block_num)) - }) - .for_each(|(eth1_data, block_number)| { - valid_votes - .entry((eth1_data, block_number)) - .and_modify(|count| *count += 1) - .or_insert(1_u64); - }); - valid_votes -} - -/// Selects the winning vote from `valid_votes`. -fn find_winning_vote(valid_votes: Eth1DataVoteCount) -> Option { - valid_votes - .iter() - .max_by_key(|((_eth1_data, block_number), vote_count)| (*vote_count, block_number)) - .map(|((eth1_data, _), _)| eth1_data.clone()) -} - -/// Returns the unix-epoch seconds at the start of the given `slot`. -fn slot_start_seconds(genesis_unix_seconds: u64, seconds_per_slot: u64, slot: Slot) -> u64 { - genesis_unix_seconds + slot.as_u64() * seconds_per_slot -} - -/// Returns a boolean denoting if a given `Eth1Block` is a candidate for `Eth1Data` calculation -/// at the timestamp `period_start`. -/// -/// Note: `period_start` needs to be atleast (`spec.seconds_per_eth1_block * spec.eth1_follow_distance * 2`) -/// for this function to return meaningful values. -fn is_candidate_block(block: &Eth1Block, period_start: u64, spec: &ChainSpec) -> bool { - block.timestamp - <= period_start.saturating_sub(spec.seconds_per_eth1_block * spec.eth1_follow_distance) - && block.timestamp - >= period_start - .saturating_sub(spec.seconds_per_eth1_block * spec.eth1_follow_distance * 2) -} - -#[cfg(test)] -mod test { - use super::*; - use types::{DepositData, FixedBytesExtended, MinimalEthSpec, Signature}; - - type E = MinimalEthSpec; - - fn get_eth1_data(i: u64) -> Eth1Data { - Eth1Data { - block_hash: Hash256::from_low_u64_be(i), - deposit_root: Hash256::from_low_u64_be(u64::MAX - i), - deposit_count: i, - } - } - - fn get_voting_period_start_seconds(state: &BeaconState, spec: &ChainSpec) -> u64 { - let period = ::SlotsPerEth1VotingPeriod::to_u64(); - let voting_period_start_slot = (state.slot() / period) * period; - slot_start_seconds( - state.genesis_time(), - spec.seconds_per_slot, - voting_period_start_slot, - ) - } - - #[test] - fn slot_start_time() { - let zero_sec = 0; - assert_eq!(slot_start_seconds(100, zero_sec, Slot::new(2)), 100); - - let one_sec = 1; - assert_eq!(slot_start_seconds(100, one_sec, Slot::new(0)), 100); - assert_eq!(slot_start_seconds(100, one_sec, Slot::new(1)), 101); - assert_eq!(slot_start_seconds(100, one_sec, Slot::new(2)), 102); - - let three_sec = 3; - assert_eq!(slot_start_seconds(100, three_sec, Slot::new(0)), 100); - assert_eq!(slot_start_seconds(100, three_sec, Slot::new(1)), 103); - assert_eq!(slot_start_seconds(100, three_sec, Slot::new(2)), 106); - - let five_sec = 5; - assert_eq!(slot_start_seconds(100, five_sec, Slot::new(0)), 100); - assert_eq!(slot_start_seconds(100, five_sec, Slot::new(1)), 105); - assert_eq!(slot_start_seconds(100, five_sec, Slot::new(2)), 110); - assert_eq!(slot_start_seconds(100, five_sec, Slot::new(3)), 115); - } - - fn get_eth1_block(timestamp: u64, number: u64) -> Eth1Block { - Eth1Block { - number, - timestamp, - hash: Hash256::from_low_u64_be(number), - deposit_root: Some(Hash256::from_low_u64_be(number)), - deposit_count: Some(number), - } - } - - mod eth1_chain_json_backend { - use super::*; - use eth1::DepositLog; - use logging::create_test_tracing_subscriber; - use types::{test_utils::generate_deterministic_keypair, MainnetEthSpec}; - - fn get_eth1_chain() -> Eth1Chain, E> { - create_test_tracing_subscriber(); - - let eth1_config = Eth1Config { - ..Eth1Config::default() - }; - - Eth1Chain::new( - CachingEth1Backend::new(eth1_config, Arc::new(MainnetEthSpec::default_spec())) - .unwrap(), - ) - } - - fn get_deposit_log(i: u64, spec: &ChainSpec) -> DepositLog { - let keypair = generate_deterministic_keypair(i as usize); - let mut deposit = DepositData { - pubkey: keypair.pk.into(), - withdrawal_credentials: Hash256::zero(), - amount: spec.max_effective_balance, - signature: Signature::empty().into(), - }; - - deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); - - DepositLog { - deposit_data: deposit, - block_number: i, - index: i, - signature_is_valid: true, - } - } - - #[test] - fn deposits_empty_cache() { - let spec = &E::default_spec(); - - let eth1_chain = get_eth1_chain(); - - assert!( - !eth1_chain.use_dummy_backend, - "test should not use dummy backend" - ); - - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - *state.eth1_deposit_index_mut() = 0; - state.eth1_data_mut().deposit_count = 0; - - assert!( - eth1_chain - .deposits_for_block_inclusion(&state, &Eth1Data::default(), spec) - .is_ok(), - "should succeed if cache is empty but no deposits are required" - ); - - state.eth1_data_mut().deposit_count = 1; - - assert!( - eth1_chain - .deposits_for_block_inclusion(&state, &Eth1Data::default(), spec) - .is_err(), - "should fail to get deposits if required, but cache is empty" - ); - } - - #[test] - fn deposits_with_cache() { - let spec = &E::default_spec(); - - let eth1_chain = get_eth1_chain(); - let max_deposits = ::MaxDeposits::to_u64(); - - assert!( - !eth1_chain.use_dummy_backend, - "test should not use dummy backend" - ); - - let deposits: Vec<_> = (0..max_deposits + 2) - .map(|i| get_deposit_log(i, spec)) - .inspect(|log| { - eth1_chain - .backend - .core - .deposits() - .write() - .cache - .insert_log(log.clone()) - .expect("should insert log"); - }) - .collect(); - - assert_eq!( - eth1_chain.backend.core.deposits().write().cache.len(), - deposits.len(), - "cache should store all logs" - ); - - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - *state.eth1_deposit_index_mut() = 0; - state.eth1_data_mut().deposit_count = 0; - - assert!( - eth1_chain - .deposits_for_block_inclusion(&state, &Eth1Data::default(), spec) - .is_ok(), - "should succeed if no deposits are required" - ); - - (0..3).for_each(|initial_deposit_index| { - *state.eth1_deposit_index_mut() = initial_deposit_index as u64; - - (initial_deposit_index..deposits.len()).for_each(|i| { - state.eth1_data_mut().deposit_count = i as u64; - - let deposits_for_inclusion = eth1_chain - .deposits_for_block_inclusion(&state, &Eth1Data::default(), spec) - .unwrap_or_else(|_| panic!("should find deposit for {}", i)); - - let expected_len = - std::cmp::min(i - initial_deposit_index, max_deposits as usize); - - assert_eq!( - deposits_for_inclusion.len(), - expected_len, - "should find {} deposits", - expected_len - ); - - let deposit_data_for_inclusion: Vec<_> = deposits_for_inclusion - .into_iter() - .map(|deposit| deposit.data) - .collect(); - - let expected_deposit_data: Vec<_> = deposits[initial_deposit_index - ..std::cmp::min(initial_deposit_index + expected_len, deposits.len())] - .iter() - .map(|log| log.deposit_data.clone()) - .collect(); - - assert_eq!( - deposit_data_for_inclusion, expected_deposit_data, - "should find the correct deposits for {}", - i - ); - }); - }) - } - - #[test] - fn eth1_data_empty_cache() { - let spec = &E::default_spec(); - - let eth1_chain = get_eth1_chain(); - - assert!( - !eth1_chain.use_dummy_backend, - "test should not use dummy backend" - ); - - let state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - let a = eth1_chain - .eth1_data_for_block_production(&state, spec) - .expect("should produce default eth1 data vote"); - assert_eq!( - a, - *state.eth1_data(), - "default vote should be same as state.eth1_data" - ); - } - - #[test] - fn default_vote() { - let spec = &E::default_spec(); - let slots_per_eth1_voting_period = ::SlotsPerEth1VotingPeriod::to_u64(); - let eth1_follow_distance = spec.eth1_follow_distance; - - let eth1_chain = get_eth1_chain(); - - assert!( - !eth1_chain.use_dummy_backend, - "test should not use dummy backend" - ); - - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - *state.slot_mut() = Slot::from(slots_per_eth1_voting_period * 10); - let follow_distance_seconds = eth1_follow_distance * spec.seconds_per_eth1_block; - let voting_period_start = get_voting_period_start_seconds(&state, spec); - let start_eth1_block = voting_period_start - follow_distance_seconds * 2; - let end_eth1_block = voting_period_start - follow_distance_seconds; - - // Populate blocks cache with candidate eth1 blocks - let blocks = (start_eth1_block..end_eth1_block) - .map(|i| get_eth1_block(i, i)) - .collect::>(); - - blocks.iter().for_each(|block| { - eth1_chain - .backend - .core - .blocks() - .write() - .insert_root_or_child(block.clone()) - .expect("should add blocks to cache"); - }); - - let vote = eth1_chain - .eth1_data_for_block_production(&state, spec) - .expect("should produce default eth1 data vote"); - - assert_eq!( - vote, - blocks - .last() - .expect("should have blocks") - .clone() - .eth1_data() - .expect("should have valid eth1 data"), - "default vote must correspond to last block in candidate blocks" - ); - } - } - - mod eth1_data_sets { - use super::*; - - #[test] - fn empty_cache() { - let spec = &E::default_spec(); - let state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - let blocks = []; - - assert_eq!( - get_votes_to_consider( - blocks.iter(), - get_voting_period_start_seconds(&state, spec), - spec, - ), - HashMap::new() - ); - } - - #[test] - fn ideal_scenario() { - let spec = E::default_spec(); - - let slots_per_eth1_voting_period = ::SlotsPerEth1VotingPeriod::to_u64(); - let eth1_follow_distance = spec.eth1_follow_distance; - - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), &spec); - *state.genesis_time_mut() = 0; - *state.slot_mut() = Slot::from(slots_per_eth1_voting_period * 10); - - let follow_distance_seconds = eth1_follow_distance * spec.seconds_per_eth1_block; - let voting_period_start = get_voting_period_start_seconds(&state, &spec); - let start_eth1_block = voting_period_start - follow_distance_seconds * 2; - let end_eth1_block = voting_period_start - follow_distance_seconds; - let blocks = (start_eth1_block..end_eth1_block) - .map(|i| get_eth1_block(i, i)) - .collect::>(); - - let votes_to_consider = - get_votes_to_consider(blocks.iter(), voting_period_start, &spec); - assert_eq!( - votes_to_consider.len() as u64, - end_eth1_block - start_eth1_block, - "all produced eth1 blocks should be in votes to consider" - ); - - (start_eth1_block..end_eth1_block) - .map(|i| get_eth1_block(i, i)) - .for_each(|eth1_block| { - assert_eq!( - eth1_block.number, - *votes_to_consider - .get(ð1_block.clone().eth1_data().unwrap()) - .expect("votes_to_consider should have expected block numbers") - ) - }); - } - } - - mod collect_valid_votes { - use super::*; - use types::List; - - fn get_eth1_data_vec(n: u64, block_number_offset: u64) -> Vec<(Eth1Data, BlockNumber)> { - (0..n) - .map(|i| (get_eth1_data(i), i + block_number_offset)) - .collect() - } - - macro_rules! assert_votes { - ($votes: expr, $expected: expr, $text: expr) => { - let expected: Vec<(Eth1Data, BlockNumber)> = $expected; - assert_eq!( - $votes.len(), - expected.len(), - "map should have the same number of elements" - ); - expected.iter().for_each(|(eth1_data, block_number)| { - $votes - .get(&(eth1_data.clone(), *block_number)) - .expect("should contain eth1 data"); - }) - }; - } - - #[test] - fn no_votes_in_state() { - let slots = ::SlotsPerEth1VotingPeriod::to_u64(); - let spec = &E::default_spec(); - let state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - let votes_to_consider = get_eth1_data_vec(slots, 0); - - let votes = collect_valid_votes(&state, &votes_to_consider.into_iter().collect()); - assert_eq!( - votes.len(), - 0, - "should not find any votes when state has no votes" - ); - } - - #[test] - fn distinct_votes_in_state() { - let slots = ::SlotsPerEth1VotingPeriod::to_u64(); - let spec = &E::default_spec(); - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - let votes_to_consider = get_eth1_data_vec(slots, 0); - - *state.eth1_data_votes_mut() = List::new( - votes_to_consider[0..slots as usize / 4] - .iter() - .map(|(eth1_data, _)| eth1_data) - .cloned() - .collect::>(), - ) - .unwrap(); - - let votes = - collect_valid_votes(&state, &votes_to_consider.clone().into_iter().collect()); - assert_votes!( - votes, - votes_to_consider[0..slots as usize / 4].to_vec(), - "should find as many votes as were in the state" - ); - } - - #[test] - fn duplicate_votes_in_state() { - let slots = ::SlotsPerEth1VotingPeriod::to_u64(); - let spec = &E::default_spec(); - let mut state: BeaconState = BeaconState::new(0, get_eth1_data(0), spec); - - let votes_to_consider = get_eth1_data_vec(slots, 0); - - let duplicate_eth1_data = votes_to_consider - .last() - .expect("should have some eth1 data") - .clone(); - - *state.eth1_data_votes_mut() = List::new( - vec![duplicate_eth1_data.clone(); 4] - .iter() - .map(|(eth1_data, _)| eth1_data) - .cloned() - .collect::>(), - ) - .unwrap(); - - let votes = collect_valid_votes(&state, &votes_to_consider.into_iter().collect()); - assert_votes!( - votes, - // There should only be one value if there's a duplicate - vec![duplicate_eth1_data.clone()], - "should find as many votes as were in the state" - ); - - assert_eq!( - *votes - .get(&duplicate_eth1_data) - .expect("should contain vote"), - 4, - "should have four votes" - ); - } - } - - mod winning_vote { - use super::*; - - type Vote = ((Eth1Data, u64), u64); - - fn vote(block_number: u64, vote_count: u64) -> Vote { - ( - ( - Eth1Data { - deposit_root: Hash256::from_low_u64_be(block_number), - deposit_count: block_number, - block_hash: Hash256::from_low_u64_be(block_number), - }, - block_number, - ), - vote_count, - ) - } - - fn vote_data(vote: &Vote) -> Eth1Data { - (vote.0).0.clone() - } - - #[test] - fn no_votes() { - let no_votes = vec![vote(0, 0), vote(1, 0), vote(3, 0), vote(2, 0)]; - - assert_eq!( - // Favour the highest block number when there are no votes. - vote_data(&no_votes[2]), - find_winning_vote(no_votes.into_iter().collect()).expect("should find winner") - ); - } - - #[test] - fn equal_votes() { - let votes = vec![vote(0, 1), vote(1, 1), vote(3, 1), vote(2, 1)]; - - assert_eq!( - // Favour the highest block number when there are equal votes. - vote_data(&votes[2]), - find_winning_vote(votes.into_iter().collect()).expect("should find winner") - ); - } - - #[test] - fn some_votes() { - let votes = vec![vote(0, 0), vote(1, 1), vote(3, 1), vote(2, 2)]; - - assert_eq!( - // Favour the highest vote over the highest block number. - vote_data(&votes[3]), - find_winning_vote(votes.into_iter().collect()).expect("should find winner") - ); - } - - #[test] - fn tying_votes() { - let votes = vec![vote(0, 0), vote(1, 1), vote(2, 2), vote(3, 2)]; - - assert_eq!( - // Favour the highest block number for tying votes. - vote_data(&votes[3]), - find_winning_vote(votes.into_iter().collect()).expect("should find winner") - ); - } - - #[test] - fn all_tying_votes() { - let votes = vec![vote(3, 42), vote(2, 42), vote(1, 42), vote(0, 42)]; - - assert_eq!( - // Favour the highest block number for tying votes. - vote_data(&votes[0]), - find_winning_vote(votes.into_iter().collect()).expect("should find winner") - ); - } - } -} diff --git a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs b/beacon_node/beacon_chain/src/eth1_finalization_cache.rs deleted file mode 100644 index 8c3bb8c483..0000000000 --- a/beacon_node/beacon_chain/src/eth1_finalization_cache.rs +++ /dev/null @@ -1,482 +0,0 @@ -use ssz_derive::{Decode, Encode}; -use std::cmp; -use std::collections::BTreeMap; -use tracing::debug; -use types::{Checkpoint, Epoch, Eth1Data, Hash256 as Root}; - -/// The default size of the cache. -/// The beacon chain only looks at the last 4 epochs for finalization. -/// Add 1 for current epoch and 4 earlier epochs. -pub const DEFAULT_ETH1_CACHE_SIZE: usize = 5; - -/// These fields are named the same as the corresponding fields in the `BeaconState` -/// as this structure stores these values from the `BeaconState` at a `Checkpoint` -#[derive(Clone, Debug, PartialEq, Encode, Decode)] -pub struct Eth1FinalizationData { - pub eth1_data: Eth1Data, - pub eth1_deposit_index: u64, -} - -impl Eth1FinalizationData { - /// Ensures the deposit finalization conditions have been met. See: - /// https://eips.ethereum.org/EIPS/eip-4881#deposit-finalization-conditions - fn fully_imported(&self) -> bool { - self.eth1_deposit_index >= self.eth1_data.deposit_count - } -} - -/// Implements map from Checkpoint -> Eth1CacheData -pub struct CheckpointMap { - capacity: usize, - // There shouldn't be more than a couple of potential checkpoints at the same - // epoch. Searching through a vector for the matching Root should be faster - // than using another map from Root->Eth1CacheData - store: BTreeMap>, -} - -impl Default for CheckpointMap { - fn default() -> Self { - Self::new() - } -} - -/// Provides a map of `Eth1CacheData` referenced by `Checkpoint` -/// -/// ## Cache Queuing -/// -/// The cache keeps a maximum number of (`capacity`) epochs. Because there may be -/// forks at the epoch boundary, it's possible that there exists more than one -/// `Checkpoint` for the same `Epoch`. This cache will store all checkpoints for -/// a given `Epoch`. When adding data for a new `Checkpoint` would cause the number -/// of `Epoch`s stored to exceed `capacity`, the data for oldest `Epoch` is dropped -impl CheckpointMap { - pub fn new() -> Self { - CheckpointMap { - capacity: DEFAULT_ETH1_CACHE_SIZE, - store: BTreeMap::new(), - } - } - - pub fn with_capacity(capacity: usize) -> Self { - CheckpointMap { - capacity: cmp::max(1, capacity), - store: BTreeMap::new(), - } - } - - pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) { - self.store - .entry(checkpoint.epoch) - .or_default() - .push((checkpoint.root, eth1_finalization_data)); - - // faster to reduce size after the fact than do pre-checking to see - // if the current data would increase the size of the BTreeMap - while self.store.len() > self.capacity { - let oldest_stored_epoch = self.store.keys().next().cloned().unwrap(); - self.store.remove(&oldest_stored_epoch); - } - } - - pub fn get(&self, checkpoint: &Checkpoint) -> Option<&Eth1FinalizationData> { - match self.store.get(&checkpoint.epoch) { - Some(vec) => { - for (root, data) in vec { - if *root == checkpoint.root { - return Some(data); - } - } - None - } - None => None, - } - } - - #[cfg(test)] - pub fn len(&self) -> usize { - self.store.len() - } -} - -/// This cache stores `Eth1CacheData` that could potentially be finalized within 4 -/// future epochs. -#[derive(Default)] -pub struct Eth1FinalizationCache { - by_checkpoint: CheckpointMap, - pending_eth1: BTreeMap, - last_finalized: Option, -} - -/// Provides a cache of `Eth1CacheData` at epoch boundaries. This is used to -/// finalize deposits when a new epoch is finalized. -/// -impl Eth1FinalizationCache { - pub fn with_capacity(capacity: usize) -> Self { - Eth1FinalizationCache { - by_checkpoint: CheckpointMap::with_capacity(capacity), - pending_eth1: BTreeMap::new(), - last_finalized: None, - } - } - - pub fn insert(&mut self, checkpoint: Checkpoint, eth1_finalization_data: Eth1FinalizationData) { - if !eth1_finalization_data.fully_imported() { - self.pending_eth1.insert( - eth1_finalization_data.eth1_data.deposit_count, - eth1_finalization_data.eth1_data.clone(), - ); - debug!( - eth1_data.deposit_count = eth1_finalization_data.eth1_data.deposit_count, - eth1_deposit_index = eth1_finalization_data.eth1_deposit_index, - "Eth1Cache: inserted pending eth1" - ); - } - self.by_checkpoint - .insert(checkpoint, eth1_finalization_data); - } - - pub fn finalize(&mut self, checkpoint: &Checkpoint) -> Option { - if let Some(eth1_finalized_data) = self.by_checkpoint.get(checkpoint) { - let finalized_deposit_index = eth1_finalized_data.eth1_deposit_index; - let mut result = None; - while let Some(pending_count) = self.pending_eth1.keys().next().cloned() { - if finalized_deposit_index >= pending_count { - result = self.pending_eth1.remove(&pending_count); - debug!( - pending_count, - finalized_deposit_index, "Eth1Cache: dropped pending eth1" - ); - } else { - break; - } - } - if eth1_finalized_data.fully_imported() { - result = Some(eth1_finalized_data.eth1_data.clone()) - } - if result.is_some() { - self.last_finalized = result; - } - self.last_finalized.clone() - } else { - debug!( - epoch = %checkpoint.epoch, - "Eth1Cache: cache miss" - ); - None - } - } - - #[cfg(test)] - pub fn by_checkpoint(&self) -> &CheckpointMap { - &self.by_checkpoint - } - - #[cfg(test)] - pub fn pending_eth1(&self) -> &BTreeMap { - &self.pending_eth1 - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use std::collections::HashMap; - - const SLOTS_PER_EPOCH: u64 = 32; - const MAX_DEPOSITS: u64 = 16; - const EPOCHS_PER_ETH1_VOTING_PERIOD: u64 = 64; - - fn eth1cache() -> Eth1FinalizationCache { - Eth1FinalizationCache::default() - } - - fn random_eth1_data(deposit_count: u64) -> Eth1Data { - Eth1Data { - deposit_root: Root::random(), - deposit_count, - block_hash: Root::random(), - } - } - - fn random_checkpoint(epoch: u64) -> Checkpoint { - Checkpoint { - epoch: epoch.into(), - root: Root::random(), - } - } - - fn random_checkpoints(n: usize) -> Vec { - let mut result = Vec::with_capacity(n); - for epoch in 0..n { - result.push(random_checkpoint(epoch as u64)) - } - result - } - - #[test] - fn fully_imported_deposits() { - let epochs = 16; - let deposits_imported = 128; - - let eth1data = random_eth1_data(deposits_imported); - let checkpoints = random_checkpoints(epochs as usize); - let mut eth1cache = eth1cache(); - - for epoch in 4..epochs { - assert_eq!( - eth1cache.by_checkpoint().len(), - cmp::min((epoch - 4) as usize, DEFAULT_ETH1_CACHE_SIZE), - "Unexpected cache size" - ); - - let checkpoint = checkpoints - .get(epoch as usize) - .expect("should get checkpoint"); - eth1cache.insert( - *checkpoint, - Eth1FinalizationData { - eth1_data: eth1data.clone(), - eth1_deposit_index: deposits_imported, - }, - ); - - let finalized_checkpoint = checkpoints - .get((epoch - 4) as usize) - .expect("should get finalized checkpoint"); - assert!( - eth1cache.pending_eth1().is_empty(), - "Deposits are fully imported so pending cache should be empty" - ); - if epoch < 8 { - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - None, - "Should have cache miss" - ); - } else { - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - Some(eth1data.clone()), - "Should have cache hit" - ) - } - } - } - - #[test] - fn partially_imported_deposits() { - let epochs = 16; - let initial_deposits_imported = 1024; - let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH; - let full_import_epoch = 13; - let total_deposits = - initial_deposits_imported + deposits_imported_per_epoch * full_import_epoch; - - let eth1data = random_eth1_data(total_deposits); - let checkpoints = random_checkpoints(epochs as usize); - let mut eth1cache = eth1cache(); - - for epoch in 0..epochs { - assert_eq!( - eth1cache.by_checkpoint().len(), - cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE), - "Unexpected cache size" - ); - - let checkpoint = checkpoints - .get(epoch as usize) - .expect("should get checkpoint"); - let deposits_imported = cmp::min( - total_deposits, - initial_deposits_imported + deposits_imported_per_epoch * epoch, - ); - eth1cache.insert( - *checkpoint, - Eth1FinalizationData { - eth1_data: eth1data.clone(), - eth1_deposit_index: deposits_imported, - }, - ); - - if epoch >= 4 { - let finalized_epoch = epoch - 4; - let finalized_checkpoint = checkpoints - .get(finalized_epoch as usize) - .expect("should get finalized checkpoint"); - if finalized_epoch < full_import_epoch { - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - None, - "Deposits not fully finalized so cache should return no Eth1Data", - ); - assert_eq!( - eth1cache.pending_eth1().len(), - 1, - "Deposits not fully finalized. Pending eth1 cache should have 1 entry" - ); - } else { - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - Some(eth1data.clone()), - "Deposits fully imported and finalized. Cache should return Eth1Data. finalized_deposits[{}]", - (initial_deposits_imported + deposits_imported_per_epoch * finalized_epoch), - ); - assert!( - eth1cache.pending_eth1().is_empty(), - "Deposits fully imported and finalized. Pending cache should be empty" - ); - } - } - } - } - - #[test] - fn fork_at_epoch_boundary() { - let epochs = 12; - let deposits_imported = 128; - - let eth1data = random_eth1_data(deposits_imported); - let checkpoints = random_checkpoints(epochs as usize); - let mut forks = HashMap::new(); - let mut eth1cache = eth1cache(); - - for epoch in 0..epochs { - assert_eq!( - eth1cache.by_checkpoint().len(), - cmp::min(epoch as usize, DEFAULT_ETH1_CACHE_SIZE), - "Unexpected cache size" - ); - - let checkpoint = checkpoints - .get(epoch as usize) - .expect("should get checkpoint"); - eth1cache.insert( - *checkpoint, - Eth1FinalizationData { - eth1_data: eth1data.clone(), - eth1_deposit_index: deposits_imported, - }, - ); - // lets put a fork at every third epoch - if epoch % 3 == 0 { - let fork = random_checkpoint(epoch); - eth1cache.insert( - fork, - Eth1FinalizationData { - eth1_data: eth1data.clone(), - eth1_deposit_index: deposits_imported, - }, - ); - forks.insert(epoch as usize, fork); - } - - assert!( - eth1cache.pending_eth1().is_empty(), - "Deposits are fully imported so pending cache should be empty" - ); - if epoch >= 4 { - let finalized_epoch = (epoch - 4) as usize; - let finalized_checkpoint = if finalized_epoch % 3 == 0 { - forks.get(&finalized_epoch).expect("should get fork") - } else { - checkpoints - .get(finalized_epoch) - .expect("should get checkpoint") - }; - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - Some(eth1data.clone()), - "Should have cache hit" - ); - if finalized_epoch >= 3 { - let dropped_epoch = finalized_epoch - 3; - if let Some(dropped_checkpoint) = forks.get(&dropped_epoch) { - // got checkpoint for an old fork that should no longer - // be in the cache because it is from too long ago - assert_eq!( - eth1cache.finalize(dropped_checkpoint), - None, - "Should have cache miss" - ); - } - } - } - } - } - - #[test] - fn massive_deposit_queue() { - // Simulating a situation where deposits don't get imported within an eth1 voting period - let eth1_voting_periods = 8; - let initial_deposits_imported = 1024; - let deposits_imported_per_epoch = MAX_DEPOSITS * SLOTS_PER_EPOCH; - let initial_deposit_queue = - deposits_imported_per_epoch * EPOCHS_PER_ETH1_VOTING_PERIOD * 2 + 32; - let new_deposits_per_voting_period = - EPOCHS_PER_ETH1_VOTING_PERIOD * deposits_imported_per_epoch / 2; - - let mut epoch_data = BTreeMap::new(); - let mut eth1s_by_count = BTreeMap::new(); - let mut eth1cache = eth1cache(); - let mut last_period_deposits = initial_deposits_imported; - for period in 0..eth1_voting_periods { - let period_deposits = initial_deposits_imported - + initial_deposit_queue - + period * new_deposits_per_voting_period; - let period_eth1_data = random_eth1_data(period_deposits); - eth1s_by_count.insert(period_eth1_data.deposit_count, period_eth1_data.clone()); - - for epoch_mod_period in 0..EPOCHS_PER_ETH1_VOTING_PERIOD { - let epoch = period * EPOCHS_PER_ETH1_VOTING_PERIOD + epoch_mod_period; - let checkpoint = random_checkpoint(epoch); - let deposits_imported = cmp::min( - period_deposits, - last_period_deposits + deposits_imported_per_epoch * epoch_mod_period, - ); - eth1cache.insert( - checkpoint, - Eth1FinalizationData { - eth1_data: period_eth1_data.clone(), - eth1_deposit_index: deposits_imported, - }, - ); - epoch_data.insert(epoch, (checkpoint, deposits_imported)); - - if epoch >= 4 { - let finalized_epoch = epoch - 4; - let (finalized_checkpoint, finalized_deposits) = epoch_data - .get(&finalized_epoch) - .expect("should get epoch data"); - - let pending_eth1s = eth1s_by_count.range((finalized_deposits + 1)..).count(); - let last_finalized_eth1 = eth1s_by_count - .range(0..(finalized_deposits + 1)) - .map(|(_, eth1)| eth1) - .next_back() - .cloned(); - assert_eq!( - eth1cache.finalize(finalized_checkpoint), - last_finalized_eth1, - "finalized checkpoint mismatch", - ); - assert_eq!( - eth1cache.pending_eth1().len(), - pending_eth1s, - "pending eth1 mismatch" - ); - } - } - - // remove unneeded stuff from old epochs - while epoch_data.len() > DEFAULT_ETH1_CACHE_SIZE { - let oldest_stored_epoch = epoch_data - .keys() - .next() - .cloned() - .expect("should get oldest epoch"); - epoch_data.remove(&oldest_stored_epoch); - } - last_period_deposits = period_deposits; - } - } -} diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 248b3a8163..9c4a920654 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -1,6 +1,6 @@ pub use eth2::types::{EventKind, SseBlock, SseFinalizedCheckpoint, SseHead}; use tokio::sync::broadcast; -use tokio::sync::broadcast::{error::SendError, Receiver, Sender}; +use tokio::sync::broadcast::{Receiver, Sender, error::SendError}; use tracing::trace; use types::EthSpec; @@ -11,6 +11,7 @@ pub struct ServerSentEventHandler { single_attestation_tx: Sender>, block_tx: Sender>, blob_sidecar_tx: Sender>, + data_column_sidecar_tx: Sender>, finalized_tx: Sender>, head_tx: Sender>, exit_tx: Sender>, @@ -38,6 +39,7 @@ impl ServerSentEventHandler { let (single_attestation_tx, _) = broadcast::channel(capacity); let (block_tx, _) = broadcast::channel(capacity); let (blob_sidecar_tx, _) = broadcast::channel(capacity); + let (data_column_sidecar_tx, _) = broadcast::channel(capacity); let (finalized_tx, _) = broadcast::channel(capacity); let (head_tx, _) = broadcast::channel(capacity); let (exit_tx, _) = broadcast::channel(capacity); @@ -59,6 +61,7 @@ impl ServerSentEventHandler { single_attestation_tx, block_tx, blob_sidecar_tx, + data_column_sidecar_tx, finalized_tx, head_tx, exit_tx, @@ -102,6 +105,10 @@ impl ServerSentEventHandler { .blob_sidecar_tx .send(kind) .map(|count| log_count("blob sidecar", count)), + EventKind::DataColumnSidecar(_) => self + .data_column_sidecar_tx + .send(kind) + .map(|count| log_count("data_column_sidecar", count)), EventKind::FinalizedCheckpoint(_) => self .finalized_tx .send(kind) @@ -184,6 +191,10 @@ impl ServerSentEventHandler { self.blob_sidecar_tx.subscribe() } + pub fn subscribe_data_column_sidecar(&self) -> Receiver> { + self.data_column_sidecar_tx.subscribe() + } + pub fn subscribe_finalized(&self) -> Receiver> { self.finalized_tx.subscribe() } @@ -260,6 +271,10 @@ impl ServerSentEventHandler { self.blob_sidecar_tx.receiver_count() > 0 } + pub fn has_data_column_sidecar_subscribers(&self) -> bool { + self.data_column_sidecar_tx.receiver_count() > 0 + } + pub fn has_finalized_subscribers(&self) -> bool { self.finalized_tx.receiver_count() > 0 } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 6b8f835c04..5cef219a19 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -18,13 +18,14 @@ use execution_layer::{ use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; use slot_clock::SlotClock; +use ssz_types::VariableList; use state_processing::per_block_processing::{ compute_timestamp_at_slot, get_expected_withdrawals, is_execution_enabled, is_merge_transition_complete, partially_verify_execution_payload, }; use std::sync::Arc; use tokio::task::JoinHandle; -use tracing::{debug, info, warn}; +use tracing::{Instrument, debug, debug_span, info, warn}; use tree_hash::TreeHash; use types::payload::BlockProductionVersion; use types::*; @@ -108,11 +109,14 @@ impl PayloadNotifier { { // Inclusion lists are those submitted for the prior slot. let il_slot = block.slot().saturating_sub(1_u64); - let inclusion_list_transactions = chain - .inclusion_list_cache - .read() - .get_inclusion_list_transactions(il_slot) - .unwrap_or(vec![].into()); + let inclusion_list_transactions = + chain + .inclusion_list_cache + .read() + .get_inclusion_list_transactions(il_slot) + .unwrap_or(VariableList::new(vec![]).map_err(|_| { + BlockError::InternalError("Cant create empty IL".to_string()) + })?); info!( tx_count = inclusion_list_transactions.len(), @@ -121,7 +125,9 @@ impl PayloadNotifier { ); inclusion_list_transactions } else { - vec![].into() + // TODO(eip7805) what should be done here in terms of error handling + VariableList::new(vec![]) + .map_err(|_| BlockError::InternalError("Cant create empty IL".to_string()))? }; Ok(Self { @@ -168,7 +174,7 @@ async fn notify_new_payload( let execution_block_hash = block.execution_payload()?.block_hash(); // TODO(eip-7805) we can remove this later - if il_transactions.len() > 0 { + if !il_transactions.is_empty() { info!( il_tx_count = il_transactions.len(), "Submit new payload with il_transactions" @@ -371,7 +377,7 @@ pub fn validate_execution_payload_for_gossip( ExecutionStatus::Invalid(_) => { return Err(BlockError::ParentExecutionPayloadInvalid { parent_root: parent_block.root, - }) + }); } }; @@ -464,8 +470,9 @@ pub fn get_execution_payload( block_production_version, ) .await - }, - "get_execution_payload", + } + .instrument(debug_span!("prepare_execution_payload")), + "prepare_execution_payload", ) .ok_or(BlockProductionError::ShuttingDown)?; @@ -564,6 +571,7 @@ where }, "prepare_execution_payload_forkchoice_update_params", ) + .instrument(debug_span!("forkchoice_update_params")) .await .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?; diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs deleted file mode 100644 index d91f103b9d..0000000000 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ /dev/null @@ -1,365 +0,0 @@ -//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL. -//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for -//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which -//! allows the CL to load the blobs quickly from the EL's blob pool. -//! -//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen -//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count, -//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity -//! supernodes. - -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; -use crate::kzg_utils::blobs_to_data_column_sidecars; -use crate::observed_data_sidecars::DoNotObserve; -use crate::{ - metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, - BlockError, -}; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; -use execution_layer::Error as ExecutionLayerError; -use metrics::{inc_counter, TryExt}; -use ssz_types::FixedVector; -use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; -use std::collections::HashSet; -use std::sync::Arc; -use tracing::debug; -use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; -use types::data_column_sidecar::DataColumnSidecarError; -use types::{ - BeaconStateError, Blob, BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecarList, EthSpec, - FullPayload, Hash256, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, VersionedHash, -}; - -/// Blobs or data column to be published to the gossip network. -pub enum BlobsOrDataColumns { - Blobs(Vec>), - DataColumns(DataColumnSidecarList), -} - -/// Result from engine get blobs to be passed onto `DataAvailabilityChecker`. -/// -/// The blobs are retrieved from a trusted EL and columns are computed locally, therefore they are -/// considered valid without requiring extra validation. -pub enum EngineGetBlobsOutput { - Blobs(FixedBlobSidecarList), - /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. - CustodyColumns(DataColumnSidecarList), -} - -#[derive(Debug)] -pub enum FetchEngineBlobError { - BeaconStateError(BeaconStateError), - BeaconChainError(Box), - BlobProcessingError(BlockError), - BlobSidecarError(BlobSidecarError), - DataColumnSidecarError(DataColumnSidecarError), - ExecutionLayerMissing, - InternalError(String), - GossipBlob(GossipBlobError), - RequestFailed(ExecutionLayerError), - RuntimeShutdown, -} - -/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or -/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`. -pub async fn fetch_and_process_engine_blobs( - chain: Arc>, - block_root: Hash256, - block: Arc>>, - custody_columns: HashSet, - publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, -) -> Result, FetchEngineBlobError> { - let versioned_hashes = if let Some(kzg_commitments) = block - .message() - .body() - .blob_kzg_commitments() - .ok() - .filter(|blobs| !blobs.is_empty()) - { - kzg_commitments - .iter() - .map(kzg_commitment_to_versioned_hash) - .collect::>() - } else { - debug!("Fetch blobs not triggered - none required"); - return Ok(None); - }; - - debug!( - num_expected_blobs = versioned_hashes.len(), - "Fetching blobs from the EL" - ); - - if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { - fetch_and_process_blobs_v2( - chain, - block_root, - block, - versioned_hashes, - custody_columns, - publish_fn, - ) - .await - } else { - fetch_and_process_blobs_v1(chain, block_root, block, versioned_hashes, publish_fn).await - } -} - -async fn fetch_and_process_blobs_v1( - chain: Arc>, - block_root: Hash256, - block: Arc>, - versioned_hashes: Vec, - publish_fn: impl Fn(BlobsOrDataColumns) + Send + Sized, -) -> Result, FetchEngineBlobError> { - let num_expected_blobs = versioned_hashes.len(); - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - - metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); - debug!(num_expected_blobs, "Fetching blobs from the EL"); - let response = execution_layer - .get_blobs_v1(versioned_hashes) - .await - .inspect_err(|_| { - inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); - }) - .map_err(FetchEngineBlobError::RequestFailed)?; - - let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); - metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); - - if num_fetched_blobs == 0 { - debug!(num_expected_blobs, "No blobs fetched from the EL"); - inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); - return Ok(None); - } else { - inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); - } - - let (signed_block_header, kzg_commitments_proof) = block - .signed_block_header_and_kzg_commitments_proof() - .map_err(FetchEngineBlobError::BeaconStateError)?; - - let fixed_blob_sidecar_list = build_blob_sidecars( - &block, - response, - signed_block_header, - &kzg_commitments_proof, - &chain.spec, - )?; - - // Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from - // the EL making it into the data availability checker. We do not immediately add these - // blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip - // and be accepted (and propagated) while we are waiting to publish. Just before publishing - // we will observe the blobs/columns and only proceed with publishing if they are not yet seen. - let blobs_to_import_and_publish = fixed_blob_sidecar_list - .iter() - .filter_map(|opt_blob| { - let blob = opt_blob.as_ref()?; - match GossipVerifiedBlob::::new(blob.clone(), blob.index, &chain) { - Ok(verified) => Some(Ok(verified)), - // Ignore already seen blobs. - Err(GossipBlobError::RepeatBlob { .. }) => None, - Err(e) => Some(Err(e)), - } - }) - .collect::, _>>() - .map_err(FetchEngineBlobError::GossipBlob)?; - - if !blobs_to_import_and_publish.is_empty() { - publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); - } - - debug!(num_fetched_blobs, "Processing engine blobs"); - - let availability_processing_status = chain - .process_engine_blobs( - block.slot(), - block_root, - EngineGetBlobsOutput::Blobs(fixed_blob_sidecar_list.clone()), - ) - .await - .map_err(FetchEngineBlobError::BlobProcessingError)?; - - Ok(Some(availability_processing_status)) -} - -async fn fetch_and_process_blobs_v2( - chain: Arc>, - block_root: Hash256, - block: Arc>, - versioned_hashes: Vec, - custody_columns_indices: HashSet, - publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, -) -> Result, FetchEngineBlobError> { - let num_expected_blobs = versioned_hashes.len(); - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - - metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); - debug!(num_expected_blobs, "Fetching blobs from the EL"); - let response = execution_layer - .get_blobs_v2(versioned_hashes) - .await - .inspect_err(|_| { - inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); - }) - .map_err(FetchEngineBlobError::RequestFailed)?; - - let (blobs, proofs): (Vec<_>, Vec<_>) = response - .into_iter() - .filter_map(|blob_and_proof_opt| { - blob_and_proof_opt.map(|blob_and_proof| { - let BlobAndProofV2 { blob, proofs } = blob_and_proof; - (blob, proofs) - }) - }) - .unzip(); - - let num_fetched_blobs = blobs.len(); - metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); - - // Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns. - if num_fetched_blobs != num_expected_blobs { - debug!( - info = "Unable to compute data columns", - num_fetched_blobs, num_expected_blobs, "Not all blobs fetched from the EL" - ); - inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); - return Ok(None); - } else { - inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); - } - - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - // Avoid computing columns if block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - let custody_columns = compute_and_publish_data_columns( - &chain, - block.clone(), - blobs, - proofs, - custody_columns_indices, - publish_fn, - ) - .await?; - - debug!(num_fetched_blobs, "Processing engine blobs"); - - let availability_processing_status = chain - .process_engine_blobs( - block.slot(), - block_root, - EngineGetBlobsOutput::CustodyColumns(custody_columns), - ) - .await - .map_err(FetchEngineBlobError::BlobProcessingError)?; - - Ok(Some(availability_processing_status)) -} - -/// Offload the data column computation to a blocking task to avoid holding up the async runtime. -async fn compute_and_publish_data_columns( - chain: &Arc>, - block: Arc>>, - blobs: Vec>, - proofs: Vec>, - custody_columns_indices: HashSet, - publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, -) -> Result, FetchEngineBlobError> { - let chain_cloned = chain.clone(); - chain - .spawn_blocking_handle( - move || { - let mut timer = metrics::start_timer_vec( - &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, - &[&blobs.len().to_string()], - ); - - let blob_refs = blobs.iter().collect::>(); - let cell_proofs = proofs.into_iter().flatten().collect(); - let data_columns_result = blobs_to_data_column_sidecars( - &blob_refs, - cell_proofs, - &block, - &chain_cloned.kzg, - &chain_cloned.spec, - ) - .discard_timer_on_break(&mut timer); - drop(timer); - - // This filtering ensures we only import and publish the custody columns. - // `DataAvailabilityChecker` requires a strict match on custody columns count to - // consider a block available. - let custody_columns = data_columns_result - .map(|mut data_columns| { - data_columns.retain(|col| custody_columns_indices.contains(&col.index)); - data_columns - }) - .map_err(FetchEngineBlobError::DataColumnSidecarError)?; - - publish_fn(BlobsOrDataColumns::DataColumns(custody_columns.clone())); - Ok(custody_columns) - }, - "compute_and_publish_data_columns", - ) - .await - .map_err(|e| FetchEngineBlobError::BeaconChainError(Box::new(e))) - .and_then(|r| r) -} - -fn build_blob_sidecars( - block: &Arc>>, - response: Vec>>, - signed_block_header: SignedBeaconBlockHeader, - kzg_commitments_inclusion_proof: &FixedVector, - spec: &ChainSpec, -) -> Result, FetchEngineBlobError> { - let epoch = block.epoch(); - let mut fixed_blob_sidecar_list = - FixedBlobSidecarList::default(spec.max_blobs_per_block(epoch) as usize); - for (index, blob_and_proof) in response - .into_iter() - .enumerate() - .filter_map(|(i, opt_blob)| Some((i, opt_blob?))) - { - match BlobSidecar::new_with_existing_proof( - index, - blob_and_proof.blob, - block, - signed_block_header.clone(), - kzg_commitments_inclusion_proof, - blob_and_proof.proof, - ) { - Ok(blob) => { - if let Some(blob_mut) = fixed_blob_sidecar_list.get_mut(index) { - *blob_mut = Some(Arc::new(blob)); - } else { - return Err(FetchEngineBlobError::InternalError(format!( - "Blobs from EL contains blob with invalid index {index}" - ))); - } - } - Err(e) => { - return Err(FetchEngineBlobError::BlobSidecarError(e)); - } - } - } - Ok(fixed_blob_sidecar_list) -} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs new file mode 100644 index 0000000000..9526921da7 --- /dev/null +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -0,0 +1,124 @@ +use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; +use crate::observed_block_producers::ProposalKey; +use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; +use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; +use kzg::Kzg; +#[cfg(test)] +use mockall::automock; +use std::collections::HashSet; +use std::sync::Arc; +use task_executor::TaskExecutor; +use types::{ChainSpec, ColumnIndex, Hash256, Slot}; + +/// An adapter to the `BeaconChain` functionalities to remove `BeaconChain` from direct dependency to enable testing fetch blobs logic. +pub(crate) struct FetchBlobsBeaconAdapter { + chain: Arc>, + spec: Arc, +} + +#[cfg_attr(test, automock, allow(dead_code))] +impl FetchBlobsBeaconAdapter { + pub(crate) fn new(chain: Arc>) -> Self { + let spec = chain.spec.clone(); + Self { chain, spec } + } + + pub(crate) fn spec(&self) -> &Arc { + &self.spec + } + + pub(crate) fn kzg(&self) -> &Arc { + &self.chain.kzg + } + + pub(crate) fn executor(&self) -> &TaskExecutor { + &self.chain.task_executor + } + + pub(crate) async fn get_blobs_v1( + &self, + versioned_hashes: Vec, + ) -> Result>>, FetchEngineBlobError> { + let execution_layer = self + .chain + .execution_layer + .as_ref() + .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; + + execution_layer + .get_blobs_v1(versioned_hashes) + .await + .map_err(FetchEngineBlobError::RequestFailed) + } + + pub(crate) async fn get_blobs_v2( + &self, + versioned_hashes: Vec, + ) -> Result>>, FetchEngineBlobError> { + let execution_layer = self + .chain + .execution_layer + .as_ref() + .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; + + execution_layer + .get_blobs_v2(versioned_hashes) + .await + .map_err(FetchEngineBlobError::RequestFailed) + } + + pub(crate) fn blobs_known_for_proposal( + &self, + proposer: u64, + slot: Slot, + ) -> Option> { + let proposer_key = ProposalKey::new(proposer, slot); + self.chain + .observed_blob_sidecars + .read() + .known_for_proposal(&proposer_key) + .cloned() + } + + pub(crate) fn data_column_known_for_proposal( + &self, + proposal_key: ProposalKey, + ) -> Option> { + self.chain + .observed_column_sidecars + .read() + .known_for_proposal(&proposal_key) + .cloned() + } + + pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { + self.chain + .data_availability_checker + .cached_blob_indexes(block_root) + } + + pub(crate) fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + self.chain + .data_availability_checker + .cached_data_column_indexes(block_root) + } + + pub(crate) async fn process_engine_blobs( + &self, + slot: Slot, + block_root: Hash256, + blobs: EngineGetBlobsOutput, + ) -> Result { + self.chain + .process_engine_blobs(slot, block_root, blobs) + .await + .map_err(FetchEngineBlobError::BlobProcessingError) + } + + pub(crate) fn fork_choice_contains_block(&self, block_root: &Hash256) -> bool { + self.chain + .canonical_head + .fork_choice_read_lock() + .contains_block(block_root) + } +} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs new file mode 100644 index 0000000000..4c6b2d10a9 --- /dev/null +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -0,0 +1,441 @@ +//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL. +//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for +//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which +//! allows the CL to load the blobs quickly from the EL's blob pool. +//! +//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen +//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count, +//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity +//! supernodes. + +mod fetch_blobs_beacon_adapter; +#[cfg(test)] +mod tests; + +use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; +use crate::block_verification_types::AsBlock; +use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; +#[cfg_attr(test, double)] +use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter; +use crate::kzg_utils::blobs_to_data_column_sidecars; +use crate::observed_block_producers::ProposalKey; +use crate::validator_monitor::timestamp_now; +use crate::{ + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, + metrics, +}; +use execution_layer::Error as ExecutionLayerError; +use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; +use metrics::{TryExt, inc_counter}; +#[cfg(test)] +use mockall_double::double; +use ssz_types::FixedVector; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use std::sync::Arc; +use tracing::{Span, debug, instrument, warn}; +use types::blob_sidecar::BlobSidecarError; +use types::data_column_sidecar::DataColumnSidecarError; +use types::{ + BeaconStateError, Blob, BlobSidecar, ColumnIndex, EthSpec, FullPayload, Hash256, KzgProofs, + SignedBeaconBlock, SignedBeaconBlockHeader, VersionedHash, +}; + +/// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the +/// gossip network. The blobs / data columns have not been marked as observed yet, as they may not +/// be published immediately. +#[derive(Debug)] +pub enum EngineGetBlobsOutput { + Blobs(Vec>), + /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. + CustodyColumns(Vec>), +} + +#[derive(Debug)] +pub enum FetchEngineBlobError { + BeaconStateError(BeaconStateError), + BeaconChainError(Box), + BlobProcessingError(BlockError), + BlobSidecarError(BlobSidecarError), + DataColumnSidecarError(DataColumnSidecarError), + ExecutionLayerMissing, + InternalError(String), + GossipBlob(GossipBlobError), + KzgError(kzg::Error), + RequestFailed(ExecutionLayerError), + RuntimeShutdown, + TokioJoin(tokio::task::JoinError), +} + +/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or +/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`. +#[instrument(skip_all)] +pub async fn fetch_and_process_engine_blobs( + chain: Arc>, + block_root: Hash256, + block: Arc>>, + custody_columns: &[ColumnIndex], + publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, +) -> Result, FetchEngineBlobError> { + fetch_and_process_engine_blobs_inner( + FetchBlobsBeaconAdapter::new(chain), + block_root, + block, + custody_columns, + publish_fn, + ) + .await +} + +/// Internal implementation of fetch blobs, which uses `FetchBlobsBeaconAdapter` instead of +/// `BeaconChain` for better testability. +async fn fetch_and_process_engine_blobs_inner( + chain_adapter: FetchBlobsBeaconAdapter, + block_root: Hash256, + block: Arc>>, + custody_columns: &[ColumnIndex], + publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, +) -> Result, FetchEngineBlobError> { + let versioned_hashes = if let Some(kzg_commitments) = block + .message() + .body() + .blob_kzg_commitments() + .ok() + .filter(|blobs| !blobs.is_empty()) + { + kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect::>() + } else { + debug!("Fetch blobs not triggered - none required"); + return Ok(None); + }; + + debug!( + num_expected_blobs = versioned_hashes.len(), + "Fetching blobs from the EL" + ); + + if chain_adapter + .spec() + .is_peer_das_enabled_for_epoch(block.epoch()) + { + fetch_and_process_blobs_v2( + chain_adapter, + block_root, + block, + versioned_hashes, + custody_columns, + publish_fn, + ) + .await + } else { + fetch_and_process_blobs_v1( + chain_adapter, + block_root, + block, + versioned_hashes, + publish_fn, + ) + .await + } +} + +#[instrument(skip_all, level = "debug")] +async fn fetch_and_process_blobs_v1( + chain_adapter: FetchBlobsBeaconAdapter, + block_root: Hash256, + block: Arc>, + versioned_hashes: Vec, + publish_fn: impl Fn(EngineGetBlobsOutput) + Send + Sized, +) -> Result, FetchEngineBlobError> { + let num_expected_blobs = versioned_hashes.len(); + metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); + debug!(num_expected_blobs, "Fetching blobs from the EL"); + let response = chain_adapter + .get_blobs_v1(versioned_hashes) + .await + .inspect_err(|_| { + inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); + })?; + + let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); + metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); + + if num_fetched_blobs == 0 { + debug!(num_expected_blobs, "No blobs fetched from the EL"); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); + } else { + debug!( + num_expected_blobs, + num_fetched_blobs, "Received blobs from the EL" + ); + inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); + } + + if chain_adapter.fork_choice_contains_block(&block_root) { + // Avoid computing sidecars if the block has already been imported. + debug!( + info = "block has already been imported", + "Ignoring EL blobs response" + ); + return Ok(None); + } + + let (signed_block_header, kzg_commitments_proof) = block + .signed_block_header_and_kzg_commitments_proof() + .map_err(FetchEngineBlobError::BeaconStateError)?; + + let mut blob_sidecar_list = build_blob_sidecars( + &block, + response, + signed_block_header, + &kzg_commitments_proof, + )?; + + if let Some(observed_blobs) = + chain_adapter.blobs_known_for_proposal(block.message().proposer_index(), block.slot()) + { + blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index())); + if blob_sidecar_list.is_empty() { + debug!( + info = "blobs have already been seen on gossip", + "Ignoring EL blobs response" + ); + return Ok(None); + } + } + + if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) { + blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index())); + if blob_sidecar_list.is_empty() { + debug!( + info = "blobs have already been imported into data availability checker", + "Ignoring EL blobs response" + ); + return Ok(None); + } + } + + // Up until this point we have not observed the blobs in the gossip cache, which allows them to + // arrive independently while this function is running. In `publish_fn` we will observe them + // and then publish any blobs that had not already been observed. + publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone())); + + let availability_processing_status = chain_adapter + .process_engine_blobs( + block.slot(), + block_root, + EngineGetBlobsOutput::Blobs(blob_sidecar_list), + ) + .await?; + + Ok(Some(availability_processing_status)) +} + +#[instrument(skip_all, level = "debug")] +async fn fetch_and_process_blobs_v2( + chain_adapter: FetchBlobsBeaconAdapter, + block_root: Hash256, + block: Arc>, + versioned_hashes: Vec, + custody_columns_indices: &[ColumnIndex], + publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, +) -> Result, FetchEngineBlobError> { + let num_expected_blobs = versioned_hashes.len(); + + metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); + debug!(num_expected_blobs, "Fetching blobs from the EL"); + let response = chain_adapter + .get_blobs_v2(versioned_hashes) + .await + .inspect_err(|_| { + inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); + })?; + + let Some(blobs_and_proofs) = response else { + debug!(num_expected_blobs, "No blobs fetched from the EL"); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); + }; + + let (blobs, proofs): (Vec<_>, Vec<_>) = blobs_and_proofs + .into_iter() + .map(|blob_and_proof| { + let BlobAndProofV2 { blob, proofs } = blob_and_proof; + (blob, proofs) + }) + .unzip(); + + let num_fetched_blobs = blobs.len(); + metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); + + if num_fetched_blobs != num_expected_blobs { + // This scenario is not supposed to happen if the EL is spec compliant. + // It should either return all requested blobs or none, but NOT partial responses. + // If we attempt to compute columns with partial blobs, we'd end up with invalid columns. + warn!( + num_fetched_blobs, + num_expected_blobs, "The EL did not return all requested blobs" + ); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); + } + + debug!(num_fetched_blobs, "All expected blobs received from the EL"); + inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); + + if chain_adapter.fork_choice_contains_block(&block_root) { + // Avoid computing columns if the block has already been imported. + debug!( + info = "block has already been imported", + "Ignoring EL blobs response" + ); + return Ok(None); + } + + let chain_adapter = Arc::new(chain_adapter); + let custody_columns_to_import = compute_custody_columns_to_import( + &chain_adapter, + block_root, + block.clone(), + blobs, + proofs, + custody_columns_indices, + ) + .await?; + + if custody_columns_to_import.is_empty() { + debug!( + info = "No new data columns to import", + "Ignoring EL blobs response" + ); + return Ok(None); + } + + // Up until this point we have not observed the data columns in the gossip cache, which allows + // them to arrive independently while this function is running. In publish_fn we will observe + // them and then publish any columns that had not already been observed. + publish_fn(EngineGetBlobsOutput::CustodyColumns( + custody_columns_to_import.clone(), + )); + + let availability_processing_status = chain_adapter + .process_engine_blobs( + block.slot(), + block_root, + EngineGetBlobsOutput::CustodyColumns(custody_columns_to_import), + ) + .await?; + + Ok(Some(availability_processing_status)) +} + +/// Offload the data column computation to a blocking task to avoid holding up the async runtime. +async fn compute_custody_columns_to_import( + chain_adapter: &Arc>, + block_root: Hash256, + block: Arc>>, + blobs: Vec>, + proofs: Vec>, + custody_columns_indices: &[ColumnIndex], +) -> Result>, FetchEngineBlobError> { + let kzg = chain_adapter.kzg().clone(); + let spec = chain_adapter.spec().clone(); + let chain_adapter_cloned = chain_adapter.clone(); + let custody_columns_indices = custody_columns_indices.to_vec(); + let current_span = Span::current(); + chain_adapter + .executor() + .spawn_blocking_handle( + move || { + let _guard = current_span.enter(); + let mut timer = metrics::start_timer_vec( + &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, + &[&blobs.len().to_string()], + ); + + let blob_refs = blobs.iter().collect::>(); + let cell_proofs = proofs.into_iter().flatten().collect(); + let data_columns_result = + blobs_to_data_column_sidecars(&blob_refs, cell_proofs, &block, &kzg, &spec) + .discard_timer_on_break(&mut timer); + drop(timer); + + // This filtering ensures we only import and publish the custody columns. + // `DataAvailabilityChecker` requires a strict match on custody columns count to + // consider a block available. + let mut custody_columns = data_columns_result + .map(|data_columns| { + data_columns + .into_iter() + .filter(|col| custody_columns_indices.contains(&col.index)) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::from_execution_verified(col), + ) + }) + .collect::>() + }) + .map_err(FetchEngineBlobError::DataColumnSidecarError)?; + + // Only consider columns that are not already observed on gossip. + if let Some(observed_columns) = chain_adapter_cloned.data_column_known_for_proposal( + ProposalKey::new(block.message().proposer_index(), block.slot()), + ) { + custody_columns.retain(|col| !observed_columns.contains(&col.index())); + if custody_columns.is_empty() { + return Ok(vec![]); + } + } + + // Only consider columns that are not already known to data availability. + if let Some(known_columns) = + chain_adapter_cloned.cached_data_column_indexes(&block_root) + { + custody_columns.retain(|col| !known_columns.contains(&col.index())); + if custody_columns.is_empty() { + return Ok(vec![]); + } + } + + Ok(custody_columns) + }, + "compute_custody_columns_to_import", + ) + .ok_or(FetchEngineBlobError::RuntimeShutdown)? + .await + .map_err(FetchEngineBlobError::TokioJoin)? +} + +fn build_blob_sidecars( + block: &Arc>>, + response: Vec>>, + signed_block_header: SignedBeaconBlockHeader, + kzg_commitments_inclusion_proof: &FixedVector, +) -> Result>, FetchEngineBlobError> { + let mut sidecars = vec![]; + for (index, blob_and_proof) in response + .into_iter() + .enumerate() + .filter_map(|(index, opt_blob)| Some((index, opt_blob?))) + { + let blob_sidecar = BlobSidecar::new_with_existing_proof( + index, + blob_and_proof.blob, + block, + signed_block_header.clone(), + kzg_commitments_inclusion_proof, + blob_and_proof.proof, + ) + .map_err(FetchEngineBlobError::BlobSidecarError)?; + + sidecars.push(KzgVerifiedBlob::from_execution_verified( + Arc::new(blob_sidecar), + timestamp_now(), + )); + } + + Ok(sidecars) +} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs new file mode 100644 index 0000000000..cbe2f78fbd --- /dev/null +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -0,0 +1,621 @@ +use crate::AvailabilityProcessingStatus; +use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter; +use crate::fetch_blobs::{ + EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs_inner, +}; +use crate::test_utils::{EphemeralHarnessType, get_kzg}; +use bls::Signature; +use eth2::types::BlobsBundle; +use execution_layer::json_structures::{BlobAndProof, BlobAndProofV1, BlobAndProofV2}; +use execution_layer::test_utils::generate_blobs; +use maplit::hashset; +use std::sync::{Arc, Mutex}; +use task_executor::test_utils::TestRuntime; +use types::{ + BeaconBlock, BeaconBlockFulu, EmptyBlock, EthSpec, ForkName, Hash256, MainnetEthSpec, + SignedBeaconBlock, SignedBeaconBlockFulu, +}; + +type E = MainnetEthSpec; +type T = EphemeralHarnessType; + +mod get_blobs_v2 { + use super::*; + use types::ColumnIndex; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_no_blobs_in_block() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, _s) = mock_publish_fn(); + let block = SignedBeaconBlock::::Fulu(SignedBeaconBlockFulu { + message: BeaconBlockFulu::empty(mock_adapter.spec()), + signature: Signature::empty(), + }); + let block_root = block.canonical_root(); + + // Expectations: engine fetch blobs should not be triggered + mock_adapter.expect_get_blobs_v2().times(0); + mock_adapter.expect_process_engine_blobs().times(0); + + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + Arc::new(block), + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + assert_eq!(processing_status, None); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_no_blobs_returned() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, _) = mock_publish_fn(); + let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // No blobs in EL response + mock_get_blobs_v2_response(&mut mock_adapter, None); + + // Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + assert_eq!(processing_status, None); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_partial_blobs_returned() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, mut blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // Missing blob in EL response + blobs_and_proofs.pop(); + mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs)); + // No blobs should be processed + mock_adapter.expect_process_engine_blobs().times(0); + + // Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + assert_eq!(processing_status, None); + assert_eq!( + publish_fn_args.lock().unwrap().len(), + 0, + "no columns should be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_block_imported_after_el_response() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // All blobs returned, but fork choice already imported the block + mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs)); + mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]); + // No blobs should be processed + mock_adapter.expect_process_engine_blobs().times(0); + + // Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + assert_eq!(processing_status, None); + assert_eq!( + publish_fn_args.lock().unwrap().len(), + 0, + "no columns should be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_no_new_columns_to_import() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // **GIVEN**: + // All blobs returned + mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs)); + // block not yet imported into fork choice + mock_fork_choice_contains_block(&mut mock_adapter, vec![]); + // All data columns already seen on gossip + mock_adapter + .expect_data_column_known_for_proposal() + .returning(|_| Some(hashset![0, 1, 2])); + // No blobs should be processed + mock_adapter.expect_process_engine_blobs().times(0); + + // **WHEN**: Trigger `fetch_blobs` on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // **THEN**: Should NOT be processed and no columns should be published. + assert_eq!(processing_status, None); + assert_eq!( + publish_fn_args.lock().unwrap().len(), + 0, + "no columns should be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v2_success() { + let mut mock_adapter = mock_beacon_adapter(ForkName::Fulu); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // All blobs returned, fork choice doesn't contain block + mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs)); + mock_fork_choice_contains_block(&mut mock_adapter, vec![]); + mock_adapter + .expect_data_column_known_for_proposal() + .returning(|_| None); + mock_adapter + .expect_cached_data_column_indexes() + .returning(|_| None); + mock_process_engine_blobs_result( + &mut mock_adapter, + Ok(AvailabilityProcessingStatus::Imported(block_root)), + ); + + // Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + assert_eq!( + processing_status, + Some(AvailabilityProcessingStatus::Imported(block_root)) + ); + + let published_columns = extract_published_blobs(publish_fn_args); + assert!( + matches!( + published_columns, + EngineGetBlobsOutput::CustodyColumns(columns) if columns.len() == custody_columns.len() + ), + "should publish custody columns" + ); + } + + fn mock_get_blobs_v2_response( + mock_adapter: &mut MockFetchBlobsBeaconAdapter, + blobs_and_proofs_opt: Option>>, + ) { + let blobs_and_proofs_v2_opt = blobs_and_proofs_opt.map(|blobs_and_proofs| { + blobs_and_proofs + .into_iter() + .map(|blob_and_proof| match blob_and_proof { + BlobAndProof::V2(inner) => inner, + _ => panic!("BlobAndProofV2 not expected"), + }) + .collect() + }); + mock_adapter + .expect_get_blobs_v2() + .return_once(move |_| Ok(blobs_and_proofs_v2_opt)); + } +} + +mod get_blobs_v1 { + use super::*; + use crate::block_verification_types::AsBlock; + use std::collections::HashSet; + use types::ColumnIndex; + + const ELECTRA_FORK: ForkName = ForkName::Electra; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_no_blobs_in_block() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let spec = mock_adapter.spec(); + let (publish_fn, _s) = mock_publish_fn(); + let block_no_blobs = + SignedBeaconBlock::from_block(BeaconBlock::empty(spec), Signature::empty()); + let block_root = block_no_blobs.canonical_root(); + + // Expectations: engine fetch blobs should not be triggered + mock_adapter.expect_get_blobs_v1().times(0); + + // WHEN: Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + Arc::new(block_no_blobs), + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // THEN: No blob is processed + assert_eq!(processing_status, None); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_no_blobs_returned() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let (publish_fn, _) = mock_publish_fn(); + let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // GIVEN: No blobs in EL response + let expected_blob_count = block.message().body().blob_kzg_commitments().unwrap().len(); + mock_get_blobs_v1_response(&mut mock_adapter, vec![None; expected_blob_count]); + + // WHEN: Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // THEN: No blob is processed + assert_eq!(processing_status, None); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_partial_blobs_returned() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let blob_count = 2; + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); + let block_slot = block.slot(); + let block_root = block.canonical_root(); + + // GIVEN: Missing a blob in EL response (remove 1 blob from response) + let mut blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); + blob_and_proof_opts.first_mut().unwrap().take(); + mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); + // AND block is not imported into fork choice + mock_fork_choice_contains_block(&mut mock_adapter, vec![]); + // AND all blobs have not yet been seen + mock_adapter + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(|_, _| None); + // Returned blobs should be processed + mock_process_engine_blobs_result( + &mut mock_adapter, + Ok(AvailabilityProcessingStatus::MissingComponents( + block_slot, block_root, + )), + ); + + // WHEN: Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // THEN: Returned blobs are processed and published + assert_eq!( + processing_status, + Some(AvailabilityProcessingStatus::MissingComponents( + block_slot, block_root, + )) + ); + assert!( + matches!( + extract_published_blobs(publish_fn_args), + EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - 1 + ), + "partial blob results should still be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_block_imported_after_el_response() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // GIVEN: All blobs returned, but fork choice already imported the block + let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); + mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); + mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]); + + // WHEN: Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // THEN: Returned blobs should NOT be processed or published. + assert_eq!(processing_status, None); + assert_eq!( + publish_fn_args.lock().unwrap().len(), + 0, + "no blobs should be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_no_new_blobs_to_import() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); + let block_root = block.canonical_root(); + + // **GIVEN**: + // All blobs returned + let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); + let all_blob_indices = blob_and_proof_opts + .iter() + .enumerate() + .map(|(i, _)| i as u64) + .collect::>(); + + mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); + // block not yet imported into fork choice + mock_fork_choice_contains_block(&mut mock_adapter, vec![]); + // All blobs already seen on gossip + mock_adapter + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(move |_, _| Some(all_blob_indices.clone())); + + // **WHEN**: Trigger `fetch_blobs` on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // **THEN**: Should NOT be processed and no blobs should be published. + assert_eq!(processing_status, None); + assert_eq!( + publish_fn_args.lock().unwrap().len(), + 0, + "no blobs should be published" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_fetch_blobs_v1_success() { + let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK); + let (publish_fn, publish_fn_args) = mock_publish_fn(); + let blob_count = 2; + let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); + let block_root = block.canonical_root(); + + // All blobs returned, fork choice doesn't contain block + let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); + mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); + mock_fork_choice_contains_block(&mut mock_adapter, vec![]); + mock_adapter + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(|_, _| None); + mock_process_engine_blobs_result( + &mut mock_adapter, + Ok(AvailabilityProcessingStatus::Imported(block_root)), + ); + + // Trigger fetch blobs on the block + let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; + let processing_status = fetch_and_process_engine_blobs_inner( + mock_adapter, + block_root, + block, + &custody_columns, + publish_fn, + ) + .await + .expect("fetch blobs should succeed"); + + // THEN all fetched blobs are processed and published + assert_eq!( + processing_status, + Some(AvailabilityProcessingStatus::Imported(block_root)) + ); + + let published_blobs = extract_published_blobs(publish_fn_args); + assert!( + matches!( + published_blobs, + EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count + ), + "should publish fetched blobs" + ); + } + + fn mock_get_blobs_v1_response( + mock_adapter: &mut MockFetchBlobsBeaconAdapter, + blobs_and_proofs_opt: Vec>>, + ) { + let blobs_and_proofs_v1 = blobs_and_proofs_opt + .into_iter() + .map(|blob_and_proof_opt| { + blob_and_proof_opt.map(|blob_and_proof| match blob_and_proof { + BlobAndProof::V1(inner) => inner, + _ => panic!("BlobAndProofV1 not expected"), + }) + }) + .collect(); + mock_adapter + .expect_get_blobs_v1() + .return_once(move |_| Ok(blobs_and_proofs_v1)); + } +} + +/// Extract the `EngineGetBlobsOutput` passed to the `publish_fn`. +fn extract_published_blobs( + publish_fn_args: Arc>>>, +) -> EngineGetBlobsOutput { + let mut calls = publish_fn_args.lock().unwrap(); + assert_eq!(calls.len(), 1); + calls.pop().unwrap() +} + +fn mock_process_engine_blobs_result( + mock_adapter: &mut MockFetchBlobsBeaconAdapter, + result: Result, +) { + mock_adapter + .expect_process_engine_blobs() + .return_once(move |_, _, _| result); +} + +fn mock_fork_choice_contains_block( + mock_adapter: &mut MockFetchBlobsBeaconAdapter, + block_roots: Vec, +) { + mock_adapter + .expect_fork_choice_contains_block() + .returning(move |block_root| block_roots.contains(block_root)); +} + +fn create_test_block_and_blobs( + mock_adapter: &MockFetchBlobsBeaconAdapter, + blob_count: usize, +) -> (Arc>, Vec>) { + let mut block = + SignedBeaconBlock::from_block(BeaconBlock::empty(mock_adapter.spec()), Signature::empty()); + let fork = block.fork_name_unchecked(); + let (blobs_bundle, _tx) = generate_blobs::(blob_count, fork).unwrap(); + let BlobsBundle { + commitments, + proofs, + blobs, + } = blobs_bundle; + + *block + .message_mut() + .body_mut() + .blob_kzg_commitments_mut() + .unwrap() = commitments; + + let blobs_and_proofs = if fork.fulu_enabled() { + let proofs_len = proofs.len() / blobs.len(); + blobs + .into_iter() + .zip(proofs.chunks(proofs_len)) + .map(|(blob, proofs)| { + BlobAndProof::V2(BlobAndProofV2 { + blob, + proofs: proofs.to_vec().try_into().unwrap(), + }) + }) + .collect() + } else { + blobs + .into_iter() + .zip(proofs) + .map(|(blob, proof)| BlobAndProof::V1(BlobAndProofV1 { blob, proof })) + .collect() + }; + + (Arc::new(block), blobs_and_proofs) +} + +#[allow(clippy::type_complexity)] +fn mock_publish_fn() -> ( + impl Fn(EngineGetBlobsOutput) + Send + 'static, + Arc>>>, +) { + // Keep track of the arguments captured by `publish_fn`. + let captured_args = Arc::new(Mutex::new(vec![])); + let captured_args_clone = captured_args.clone(); + let publish_fn = move |args| { + let mut lock = captured_args_clone.lock().unwrap(); + lock.push(args); + }; + (publish_fn, captured_args) +} + +fn mock_beacon_adapter(fork_name: ForkName) -> MockFetchBlobsBeaconAdapter { + let test_runtime = TestRuntime::default(); + let spec = Arc::new(fork_name.make_genesis_spec(E::default_spec())); + let kzg = get_kzg(&spec); + + let mut mock_adapter = MockFetchBlobsBeaconAdapter::default(); + mock_adapter.expect_spec().return_const(spec.clone()); + mock_adapter.expect_kzg().return_const(kzg.clone()); + mock_adapter + .expect_executor() + .return_const(test_runtime.task_executor.clone()); + mock_adapter +} diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index cde2950c89..4db79790d3 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -3,12 +3,12 @@ use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; use state_processing::state_advance::complete_state_advance; use state_processing::{ - per_block_processing, per_block_processing::BlockSignatureStrategy, ConsensusContext, - VerifyBlockRoot, + ConsensusContext, VerifyBlockRoot, per_block_processing, + per_block_processing::BlockSignatureStrategy, }; use std::sync::Arc; use std::time::Duration; -use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; +use store::{HotColdDB, ItemStore, iter::ParentRootBlockIterator}; use tracing::{info, warn}; use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; @@ -142,8 +142,9 @@ pub fn reset_fork_choice_to_finalization, Cold: It beacon_state: finalized_state, }; - let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), &finalized_snapshot) - .map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?; + let fc_store = + BeaconForkChoiceStore::get_forkchoice_store(store.clone(), finalized_snapshot.clone()) + .map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?; let mut fork_choice = ForkChoice::from_anchor( fc_store, diff --git a/beacon_node/beacon_chain/src/fulu_readiness.rs b/beacon_node/beacon_chain/src/fulu_readiness.rs deleted file mode 100644 index 1107acad74..0000000000 --- a/beacon_node/beacon_chain/src/fulu_readiness.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Provides tools for checking if a node is ready for the Fulu upgrade. - -use crate::{BeaconChain, BeaconChainTypes}; -use execution_layer::http::{ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V4}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::time::Duration; -use types::*; - -/// The time before the Fulu fork when we will start issuing warnings about preparation. -use super::bellatrix_readiness::SECONDS_IN_A_WEEK; -pub const FULU_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; -pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type")] -pub enum FuluReadiness { - /// The execution engine is fulu-enabled (as far as we can tell) - Ready, - /// We are connected to an execution engine which doesn't support the V5 engine api methods - V5MethodsNotSupported { error: String }, - /// The transition configuration with the EL failed, there might be a problem with - /// connectivity, authentication or a difference in configuration. - ExchangeCapabilitiesFailed { error: String }, - /// The user has not configured an execution endpoint - NoExecutionEndpoint, -} - -impl fmt::Display for FuluReadiness { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FuluReadiness::Ready => { - write!(f, "This node appears ready for Fulu.") - } - FuluReadiness::ExchangeCapabilitiesFailed { error } => write!( - f, - "Could not exchange capabilities with the \ - execution endpoint: {}", - error - ), - FuluReadiness::NoExecutionEndpoint => write!( - f, - "The --execution-endpoint flag is not specified, this is a \ - requirement post-merge" - ), - FuluReadiness::V5MethodsNotSupported { error } => write!( - f, - "Execution endpoint does not support Fulu methods: {}", - error - ), - } - } -} - -impl BeaconChain { - /// Returns `true` if fulu epoch is set and Fulu fork has occurred or will - /// occur within `FULU_READINESS_PREPARATION_SECONDS` - pub fn is_time_to_prepare_for_fulu(&self, current_slot: Slot) -> bool { - if let Some(fulu_epoch) = self.spec.fulu_fork_epoch { - let fulu_slot = fulu_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let fulu_readiness_preparation_slots = - FULU_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; - // Return `true` if Fulu has happened or is within the preparation time. - current_slot + fulu_readiness_preparation_slots > fulu_slot - } else { - // The Fulu fork epoch has not been defined yet, no need to prepare. - false - } - } - - /// Attempts to connect to the EL and confirm that it is ready for fulu. - pub async fn check_fulu_readiness(&self) -> FuluReadiness { - if let Some(el) = self.execution_layer.as_ref() { - match el - .get_engine_capabilities(Some(Duration::from_secs( - ENGINE_CAPABILITIES_REFRESH_INTERVAL, - ))) - .await - { - Err(e) => { - // The EL was either unreachable or responded with an error - FuluReadiness::ExchangeCapabilitiesFailed { - error: format!("{:?}", e), - } - } - Ok(capabilities) => { - let mut missing_methods = String::from("Required Methods Unsupported:"); - let mut all_good = true; - if !capabilities.get_payload_v5 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_GET_PAYLOAD_V5); - all_good = false; - } - // TODO(fulu) switch to v5 when the EL is ready - if !capabilities.new_payload_v4 { - missing_methods.push(' '); - missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4); - all_good = false; - } - - if all_good { - FuluReadiness::Ready - } else { - FuluReadiness::V5MethodsNotSupported { - error: missing_methods, - } - } - } - } - } else { - FuluReadiness::NoExecutionEndpoint - } - } -} diff --git a/beacon_node/beacon_chain/src/graffiti_calculator.rs b/beacon_node/beacon_chain/src/graffiti_calculator.rs index 23d1d69b1c..85470715c9 100644 --- a/beacon_node/beacon_chain/src/graffiti_calculator.rs +++ b/beacon_node/beacon_chain/src/graffiti_calculator.rs @@ -1,13 +1,14 @@ use crate::BeaconChain; use crate::BeaconChainTypes; -use execution_layer::{http::ENGINE_GET_CLIENT_VERSION_V1, CommitPrefix, ExecutionLayer}; +use eth2::types::GraffitiPolicy; +use execution_layer::{CommitPrefix, ExecutionLayer, http::ENGINE_GET_CLIENT_VERSION_V1}; use logging::crit; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use std::{fmt::Debug, time::Duration}; use task_executor::TaskExecutor; use tracing::{debug, error, warn}; -use types::{EthSpec, Graffiti, GRAFFITI_BYTES_LEN}; +use types::{EthSpec, GRAFFITI_BYTES_LEN, Graffiti}; const ENGINE_VERSION_AGE_LIMIT_EPOCH_MULTIPLE: u32 = 6; // 6 epochs const ENGINE_VERSION_CACHE_REFRESH_EPOCH_MULTIPLE: u32 = 2; // 2 epochs @@ -48,6 +49,25 @@ impl Debug for GraffitiOrigin { } } +pub enum GraffitiSettings { + Unspecified, + Specified { + graffiti: Graffiti, + policy: GraffitiPolicy, + }, +} + +impl GraffitiSettings { + pub fn new(validator_graffiti: Option, policy: Option) -> Self { + validator_graffiti + .map(|graffiti| Self::Specified { + graffiti, + policy: policy.unwrap_or(GraffitiPolicy::PreserveUserGraffiti), + }) + .unwrap_or(Self::Unspecified) + } +} + pub struct GraffitiCalculator { pub beacon_graffiti: GraffitiOrigin, execution_layer: Option>, @@ -73,18 +93,28 @@ impl GraffitiCalculator { /// 2. Graffiti specified by the user via beacon node CLI options. /// 3. The EL & CL client version string, applicable when the EL supports version specification. /// 4. The default lighthouse version string, used if the EL lacks version specification support. - pub async fn get_graffiti(&self, validator_graffiti: Option) -> Graffiti { - if let Some(graffiti) = validator_graffiti { - return graffiti; + pub async fn get_graffiti(&self, graffiti_settings: GraffitiSettings) -> Graffiti { + match graffiti_settings { + GraffitiSettings::Specified { graffiti, policy } => match policy { + GraffitiPolicy::PreserveUserGraffiti => graffiti, + GraffitiPolicy::AppendClientVersions => { + self.calculate_combined_graffiti(Some(graffiti)).await + } + }, + GraffitiSettings::Unspecified => self.calculate_combined_graffiti(None).await, } + } + async fn calculate_combined_graffiti(&self, validator_graffiti: Option) -> Graffiti { match self.beacon_graffiti { GraffitiOrigin::UserSpecified(graffiti) => graffiti, GraffitiOrigin::Calculated(default_graffiti) => { let Some(execution_layer) = self.execution_layer.as_ref() else { // Return default graffiti if there is no execution layer. This // shouldn't occur if we're actually producing blocks. - crit!("No execution layer available for graffiti calculation during block production!"); + crit!( + "No execution layer available for graffiti calculation during block production!" + ); return default_graffiti; }; @@ -131,7 +161,7 @@ impl GraffitiCalculator { CommitPrefix("00000000".to_string()) }); - engine_version.calculate_graffiti(lighthouse_commit_prefix) + engine_version.calculate_graffiti(lighthouse_commit_prefix, validator_graffiti) } } } @@ -221,15 +251,18 @@ async fn engine_version_cache_refresh_service( #[cfg(test)] mod tests { - use crate::test_utils::{test_spec, BeaconChainHarness, EphemeralHarnessType}; use crate::ChainConfig; - use execution_layer::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_ENGINE_CAPABILITIES}; + use crate::graffiti_calculator::GraffitiSettings; + use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType, test_spec}; + use bls::Keypair; + use eth2::types::GraffitiPolicy; use execution_layer::EngineCapabilities; + use execution_layer::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_ENGINE_CAPABILITIES}; use std::sync::Arc; use std::sync::LazyLock; use std::time::Duration; use tracing::info; - use types::{ChainSpec, Graffiti, Keypair, MinimalEthSpec, GRAFFITI_BYTES_LEN}; + use types::{ChainSpec, GRAFFITI_BYTES_LEN, Graffiti, MinimalEthSpec}; const VALIDATOR_COUNT: usize = 48; /// A cached set of keys. @@ -278,8 +311,12 @@ mod tests { let version_bytes = std::cmp::min(lighthouse_version::VERSION.len(), GRAFFITI_BYTES_LEN); // grab the slice of the graffiti that corresponds to the lighthouse version - let graffiti_slice = - &harness.chain.graffiti_calculator.get_graffiti(None).await.0[..version_bytes]; + let graffiti_slice = &harness + .chain + .graffiti_calculator + .get_graffiti(GraffitiSettings::Unspecified) + .await + .0[..version_bytes]; // convert graffiti bytes slice to ascii for easy debugging if this test should fail let graffiti_str = @@ -300,7 +337,12 @@ mod tests { let spec = Arc::new(test_spec::()); let harness = get_harness(VALIDATOR_COUNT, spec, None); - let found_graffiti_bytes = harness.chain.graffiti_calculator.get_graffiti(None).await.0; + let found_graffiti_bytes = harness + .chain + .graffiti_calculator + .get_graffiti(GraffitiSettings::Unspecified) + .await + .0; let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone(); let expected_graffiti_string = format!( @@ -349,7 +391,10 @@ mod tests { let found_graffiti = harness .chain .graffiti_calculator - .get_graffiti(Some(Graffiti::from(graffiti_bytes))) + .get_graffiti(GraffitiSettings::new( + Some(Graffiti::from(graffiti_bytes)), + Some(GraffitiPolicy::PreserveUserGraffiti), + )) .await; assert_eq!( @@ -357,4 +402,98 @@ mod tests { "0x6e6963652067726166666974692062726f000000000000000000000000000000" ); } + + #[tokio::test] + async fn check_append_el_version_graffiti_various_length() { + let spec = Arc::new(test_spec::()); + let harness = get_harness(VALIDATOR_COUNT, spec, None); + + let graffiti_vec = vec![ + // less than 20 characters, example below is 19 characters + "This is my graffiti", + // 20-23 characters, example below is 22 characters + "This is my graffiti yo", + // 24-27 characters, example below is 26 characters + "This is my graffiti string", + // 28-29 characters, example below is 29 characters + "This is my graffiti string yo", + // 30-32 characters, example below is 32 characters + "This is my graffiti string yo yo", + ]; + + for graffiti in graffiti_vec { + let mut graffiti_bytes = [0; GRAFFITI_BYTES_LEN]; + graffiti_bytes[..graffiti.len()].copy_from_slice(graffiti.as_bytes()); + + // To test appending client version info with user specified graffiti + let policy = GraffitiPolicy::AppendClientVersions; + let found_graffiti_bytes = harness + .chain + .graffiti_calculator + .get_graffiti(GraffitiSettings::Specified { + graffiti: Graffiti::from(graffiti_bytes), + policy, + }) + .await + .0; + + let mock_commit = DEFAULT_CLIENT_VERSION.commit.clone(); + + let graffiti_length = graffiti.len(); + let append_graffiti_string = match graffiti_length { + 0..=19 => format!( + "{}{}{}{}", + DEFAULT_CLIENT_VERSION.code, + mock_commit + .strip_prefix("0x") + .unwrap_or("&mock_commit") + .get(0..4) + .expect("should get first 2 bytes in hex"), + "LH", + lighthouse_version::COMMIT_PREFIX + .get(0..4) + .expect("should get first 2 bytes in hex") + ), + 20..=23 => format!( + "{}{}{}{}", + DEFAULT_CLIENT_VERSION.code, + mock_commit + .strip_prefix("0x") + .unwrap_or("&mock_commit") + .get(0..2) + .expect("should get first 2 bytes in hex"), + "LH", + lighthouse_version::COMMIT_PREFIX + .get(0..2) + .expect("should get first 2 bytes in hex") + ), + 24..=27 => format!("{}{}", DEFAULT_CLIENT_VERSION.code, "LH",), + 28..=29 => DEFAULT_CLIENT_VERSION.code.to_string(), + // when user graffiti length is 30-32 characters, append nothing + 30..=32 => String::new(), + _ => panic!( + "graffiti length should be less than or equal to GRAFFITI_BYTES_LEN (32 characters)" + ), + }; + + let expected_graffiti_string = if append_graffiti_string.is_empty() { + // for the case of empty append_graffiti_string, i.e., user-specified graffiti is 30-32 characters + graffiti.to_string() + } else { + // There is a space between the client version info and user graffiti + // as defined in calculate_graffiti function in engine_api.rs + format!("{} {}", append_graffiti_string, graffiti) + }; + + let expected_graffiti_prefix_bytes = expected_graffiti_string.as_bytes(); + let expected_graffiti_prefix_len = + std::cmp::min(expected_graffiti_prefix_bytes.len(), GRAFFITI_BYTES_LEN); + + let found_graffiti_string = + std::str::from_utf8(&found_graffiti_bytes[..expected_graffiti_prefix_len]) + .expect("bytes should convert nicely to ascii"); + + assert_eq!(expected_graffiti_string, found_graffiti_string); + } + } } diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 348e6d52a6..91b0f12cbb 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -1,9 +1,10 @@ use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; -use crate::{metrics, BeaconChain, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainTypes, WhenSlotSkipped, metrics}; +use fixed_bytes::FixedBytesExtended; use itertools::Itertools; use state_processing::{ per_block_processing::ParallelSignatureSets, - signature_sets::{block_proposal_signature_set_from_parts, Error as SignatureSetError}, + signature_sets::{Error as SignatureSetError, block_proposal_signature_set_from_parts}, }; use std::borrow::Cow; use std::iter; @@ -11,8 +12,8 @@ use std::time::Duration; use store::metadata::DataColumnInfo; use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; -use tracing::debug; -use types::{FixedBytesExtended, Hash256, Slot}; +use tracing::{debug, instrument}; +use types::{Hash256, Slot}; /// Use a longer timeout on the pubkey cache. /// @@ -34,6 +35,8 @@ pub enum HistoricalBlockError { ValidatorPubkeyCacheTimeout, /// Logic error: should never occur. IndexOutOfBounds, + /// Logic error: should never occur. + MissingOldestBlockRoot { slot: Slot }, /// Internal store error StoreError(StoreError), } @@ -56,13 +59,15 @@ impl BeaconChain { /// `SignatureSetError` or `InvalidSignature` will be returned. /// /// To align with sync we allow some excess blocks with slots greater than or equal to - /// `oldest_block_slot` to be provided. They will be ignored without being checked. + /// `oldest_block_slot` to be provided. They will be re-imported to fill the columns of the + /// checkpoint sync block. /// /// This function should not be called concurrently with any other function that mutates /// the anchor info (including this function itself). If a concurrent mutation occurs that /// would violate consistency then an `AnchorInfoConcurrentMutation` error will be returned. /// /// Return the number of blocks successfully imported. + #[instrument(skip_all)] pub fn import_historical_block_batch( &self, mut blocks: Vec>, @@ -71,9 +76,12 @@ impl BeaconChain { let blob_info = self.store.get_blob_info(); let data_column_info = self.store.get_data_column_info(); - // Take all blocks with slots less than the oldest block slot. + // Take all blocks with slots less than or equal to the oldest block slot. + // + // This allows for reimport of the blobs/columns for the finalized block after checkpoint + // sync. let num_relevant = blocks.partition_point(|available_block| { - available_block.block().slot() < anchor_info.oldest_block_slot + available_block.block().slot() <= anchor_info.oldest_block_slot }); let total_blocks = blocks.len(); @@ -94,6 +102,7 @@ impl BeaconChain { } let mut expected_block_root = anchor_info.oldest_block_parent; + let mut last_block_root = expected_block_root; let mut prev_block_slot = anchor_info.oldest_block_slot; let mut new_oldest_blob_slot = blob_info.oldest_blob_slot; let mut new_oldest_data_column_slot = data_column_info.oldest_data_column_slot; @@ -106,7 +115,27 @@ impl BeaconChain { for available_block in blocks_to_import.into_iter().rev() { let (block_root, block, block_data) = available_block.deconstruct(); - if block_root != expected_block_root { + if block.slot() == anchor_info.oldest_block_slot { + // When reimporting, verify that this is actually the same block (same block root). + let oldest_block_root = self + .block_root_at_slot(block.slot(), WhenSlotSkipped::None) + .ok() + .flatten() + .ok_or(HistoricalBlockError::MissingOldestBlockRoot { slot: block.slot() })?; + if block_root != oldest_block_root { + return Err(HistoricalBlockError::MismatchedBlockRoot { + block_root, + expected_block_root: oldest_block_root, + }); + } + + debug!( + ?block_root, + slot = %block.slot(), + "Re-importing historic block" + ); + last_block_root = block_root; + } else if block_root != expected_block_root { return Err(HistoricalBlockError::MismatchedBlockRoot { block_root, expected_block_root, @@ -139,7 +168,7 @@ impl BeaconChain { // Store the blobs or data columns too if let Some(op) = self - .get_blobs_or_columns_store_op(block_root, block_data) + .get_blobs_or_columns_store_op(block_root, block.slot(), block_data) .map_err(|e| { HistoricalBlockError::StoreError(StoreError::DBError { message: format!("get_blobs_or_columns_store_op error {e:?}"), @@ -151,6 +180,7 @@ impl BeaconChain { // Store block roots, including at all skip slots in the freezer DB. for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { + debug!(%slot, ?block_root, "Storing frozen block to root mapping"); cold_batch.push(KeyValueStoreOp::PutKeyValue( DBColumn::BeaconBlockRoots, slot.to_be_bytes().to_vec(), @@ -196,11 +226,11 @@ impl BeaconChain { .ok_or(HistoricalBlockError::IndexOutOfBounds)? .iter() .map(|block| block.parent_root()) - .chain(iter::once(anchor_info.oldest_block_parent)); + .chain(iter::once(last_block_root)); let signature_set = signed_blocks .iter() .zip_eq(block_roots) - .filter(|&(_block, block_root)| (block_root != self.genesis_block_root)) + .filter(|&(_block, block_root)| block_root != self.genesis_block_root) .map(|(block, block_root)| { block_proposal_signature_set_from_parts( block, @@ -235,30 +265,30 @@ impl BeaconChain { let mut anchor_and_blob_batch = Vec::with_capacity(3); // Update the blob info. - if new_oldest_blob_slot != blob_info.oldest_blob_slot { - if let Some(oldest_blob_slot) = new_oldest_blob_slot { - let new_blob_info = BlobInfo { - oldest_blob_slot: Some(oldest_blob_slot), - ..blob_info.clone() - }; - anchor_and_blob_batch.push( - self.store - .compare_and_set_blob_info(blob_info, new_blob_info)?, - ); - } + if new_oldest_blob_slot != blob_info.oldest_blob_slot + && let Some(oldest_blob_slot) = new_oldest_blob_slot + { + let new_blob_info = BlobInfo { + oldest_blob_slot: Some(oldest_blob_slot), + ..blob_info.clone() + }; + anchor_and_blob_batch.push( + self.store + .compare_and_set_blob_info(blob_info, new_blob_info)?, + ); } // Update the data column info. - if new_oldest_data_column_slot != data_column_info.oldest_data_column_slot { - if let Some(oldest_data_column_slot) = new_oldest_data_column_slot { - let new_data_column_info = DataColumnInfo { - oldest_data_column_slot: Some(oldest_data_column_slot), - }; - anchor_and_blob_batch.push( - self.store - .compare_and_set_data_column_info(data_column_info, new_data_column_info)?, - ); - } + if new_oldest_data_column_slot != data_column_info.oldest_data_column_slot + && let Some(oldest_data_column_slot) = new_oldest_data_column_slot + { + let new_data_column_info = DataColumnInfo { + oldest_data_column_slot: Some(oldest_data_column_slot), + }; + anchor_and_blob_batch.push( + self.store + .compare_and_set_data_column_info(data_column_info, new_data_column_info)?, + ); } // Update the anchor. diff --git a/beacon_node/beacon_chain/src/historical_data_columns.rs b/beacon_node/beacon_chain/src/historical_data_columns.rs new file mode 100644 index 0000000000..6cf947adcb --- /dev/null +++ b/beacon_node/beacon_chain/src/historical_data_columns.rs @@ -0,0 +1,147 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, + data_column_verification::verify_kzg_for_data_column_list, +}; +use store::{Error as StoreError, KeyValueStore}; +use tracing::{Span, debug, instrument}; +use types::{ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, Hash256, Slot}; + +#[derive(Debug)] +pub enum HistoricalDataColumnError { + // The provided data column sidecar pertains to a block that doesn't exist in the database. + NoBlockFound { + data_column_block_root: Hash256, + expected_block_root: Hash256, + }, + + /// Logic error: should never occur. + IndexOutOfBounds, + + /// The provided data column sidecar list doesn't contain columns for the full range of slots for the given epoch. + MissingDataColumns { + missing_slots_and_data_columns: Vec<(Slot, ColumnIndex)>, + }, + + /// The provided data column sidecar list contains at least one column with an invalid kzg commitment. + InvalidKzg, + + /// Internal store error + StoreError(StoreError), + + /// Internal beacon chain error + BeaconChainError(Box), +} + +impl From for HistoricalDataColumnError { + fn from(e: StoreError) -> Self { + Self::StoreError(e) + } +} + +impl BeaconChain { + /// Store a batch of historical data columns in the database. + /// + /// The data columns block roots and proposer signatures are verified with the existing + /// block stored in the DB. This function also verifies the columns KZG committments. + /// + /// This function requires that the data column sidecar list contains columns for a full epoch. + /// + /// Return the number of `data_columns` successfully imported. + #[instrument(skip_all, fields(columns_imported_count = tracing::field::Empty ))] + pub fn import_historical_data_column_batch( + &self, + epoch: Epoch, + historical_data_column_sidecar_list: DataColumnSidecarList, + expected_cgc: u64, + ) -> Result { + let mut total_imported = 0; + let mut ops = vec![]; + + let unique_column_indices = historical_data_column_sidecar_list + .iter() + .map(|item| item.index) + .collect::>(); + + let mut slot_and_column_index_to_data_columns = historical_data_column_sidecar_list + .iter() + .map(|data_column| ((data_column.slot(), data_column.index), data_column)) + .collect::>(); + + let forward_blocks_iter = self + .forwards_iter_block_roots_until( + epoch.start_slot(T::EthSpec::slots_per_epoch()), + epoch.end_slot(T::EthSpec::slots_per_epoch()), + ) + .map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?; + + for block_iter_result in forward_blocks_iter { + let (block_root, slot) = block_iter_result + .map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?; + + for column_index in unique_column_indices.clone() { + if let Some(data_column) = + slot_and_column_index_to_data_columns.remove(&(slot, column_index)) + { + if self + .store + .get_data_column(&block_root, &data_column.index)? + .is_some() + { + continue; + } + if block_root != data_column.block_root() { + return Err(HistoricalDataColumnError::NoBlockFound { + data_column_block_root: data_column.block_root(), + expected_block_root: block_root, + }); + } + self.store.data_column_as_kv_store_ops( + &block_root, + data_column.clone(), + &mut ops, + ); + total_imported += 1; + } + } + } + + // If we've made it to here with no columns to import, this means there are no blobs for this epoch. + // `RangeDataColumnBatchRequest` logic should have caught any bad peers withholding columns + if historical_data_column_sidecar_list.is_empty() { + if !ops.is_empty() { + // This shouldn't be a valid case. If there are no columns to import, + // there should be no generated db operations. + return Err(HistoricalDataColumnError::IndexOutOfBounds); + } + } else { + verify_kzg_for_data_column_list(historical_data_column_sidecar_list.iter(), &self.kzg) + .map_err(|_| HistoricalDataColumnError::InvalidKzg)?; + + self.store.blobs_db.do_atomically(ops)?; + } + + if !slot_and_column_index_to_data_columns.is_empty() { + debug!( + ?epoch, + extra_data = ?slot_and_column_index_to_data_columns.keys().map(|(slot, _)| slot), + "We've received unexpected extra data columns, these will not be imported" + ); + } + + self.data_availability_checker + .custody_context() + .update_and_backfill_custody_count_at_epoch(epoch, expected_cgc); + + self.safely_backfill_data_column_custody_info(epoch) + .map_err(|e| HistoricalDataColumnError::BeaconChainError(Box::new(e)))?; + + debug!(?epoch, total_imported, "Imported historical data columns"); + + let current_span = Span::current(); + current_span.record("columns_imported_count", total_imported); + + Ok(total_imported) + } +} diff --git a/beacon_node/beacon_chain/src/inclusion_list_verification.rs b/beacon_node/beacon_chain/src/inclusion_list_verification.rs index 6c51b8b55b..763e04a3f5 100644 --- a/beacon_node/beacon_chain/src/inclusion_list_verification.rs +++ b/beacon_node/beacon_chain/src/inclusion_list_verification.rs @@ -1,8 +1,8 @@ use std::time::Duration; use crate::{ - validator_monitor::{get_slot_delay_ms, timestamp_now}, BeaconChain, BeaconChainError, BeaconChainTypes, + validator_monitor::{get_slot_delay_ms, timestamp_now}, }; use slot_clock::SlotClock; @@ -119,7 +119,7 @@ impl GossipVerifiedInclusionList { return Err(GossipInclusionListError::InvalidSignature); } - if chain.inclusion_list_seen(&signed_il) { + if chain.inclusion_list_seen(signed_il) { return Err(GossipInclusionListError::PriorInclusionListKnown); } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 704fb3663f..334124419b 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -1,10 +1,11 @@ use kzg::{ Blob as KzgBlob, Bytes48, Cell as KzgCell, CellRef as KzgCellRef, CellsAndKzgProofs, - Error as KzgError, Kzg, CELLS_PER_EXT_BLOB, + Error as KzgError, Kzg, KzgBlobRef, }; use rayon::prelude::*; use ssz_types::{FixedVector, VariableList}; use std::sync::Arc; +use tracing::instrument; use types::beacon_block_body::KzgCommitments; use types::data_column_sidecar::{Cell, DataColumn, DataColumnSidecarError}; use types::{ @@ -25,11 +26,11 @@ fn ssz_blob_to_crypto_blob_boxed(blob: &Blob) -> Result(cell: &Cell) -> Result { +fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result, KzgError> { let cell_bytes: &[u8] = cell.as_ref(); - Ok(cell_bytes + cell_bytes .try_into() - .expect("expected cell to have size {BYTES_PER_CELL}. This should be guaranteed by the `FixedVector type")) + .map_err(|e| KzgError::InconsistentArrayLength(format!("expected cell to have size BYTES_PER_CELL. This should be guaranteed by the `FixedVector` type: {e:?}"))) } /// Validate a single blob-commitment-proof triplet from a `BlobSidecar`. @@ -44,38 +45,11 @@ pub fn validate_blob( kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) } -/// Validates a list of blobs along with their corresponding KZG commitments and -/// cell proofs for the extended blobs. -pub fn validate_blobs_and_cell_proofs( - kzg: &Kzg, - blobs: Vec<&Blob>, - cell_proofs: &[KzgProof], - kzg_commitments: &KzgCommitments, -) -> Result<(), KzgError> { - let cells = compute_cells::(&blobs, kzg)?; - let cell_refs = cells.iter().map(|cell| cell.as_ref()).collect::>(); - let cell_indices = (0..blobs.len()) - .flat_map(|_| 0..CELLS_PER_EXT_BLOB as u64) - .collect::>(); - - let proofs = cell_proofs - .iter() - .map(|&proof| Bytes48::from(proof)) - .collect::>(); - - let commitments = kzg_commitments - .iter() - .flat_map(|&commitment| std::iter::repeat_n(Bytes48::from(commitment), CELLS_PER_EXT_BLOB)) - .collect::>(); - - kzg.verify_cell_proof_batch(&cell_refs, &proofs, cell_indices, &commitments) -} - /// Validate a batch of `DataColumnSidecar`. pub fn validate_data_columns<'a, E: EthSpec, I>( kzg: &Kzg, data_column_iter: I, -) -> Result<(), KzgError> +) -> Result<(), (Option, KzgError)> where I: Iterator>> + Clone, { @@ -87,8 +61,12 @@ where for data_column in data_column_iter { let col_index = data_column.index; + if data_column.column.is_empty() { + return Err((Some(col_index), KzgError::KzgVerificationFailed)); + } + for cell in &data_column.column { - cells.push(ssz_cell_to_crypto_cell::(cell)?); + cells.push(ssz_cell_to_crypto_cell::(cell).map_err(|e| (Some(col_index), e))?); column_indices.push(col_index); } @@ -99,6 +77,19 @@ where for &commitment in &data_column.kzg_commitments { commitments.push(Bytes48::from(commitment)); } + + let expected_len = column_indices.len(); + + // We make this check at each iteration so that the error is attributable to a specific column + if cells.len() != expected_len + || proofs.len() != expected_len + || commitments.len() != expected_len + { + return Err(( + Some(col_index), + KzgError::InconsistentArrayLength("Invalid data column".to_string()), + )); + } } kzg.verify_cell_proof_batch(&cells, &proofs, column_indices, &commitments) @@ -163,6 +154,7 @@ pub fn verify_kzg_proof( } /// Build data column sidecars from a signed beacon block and its blobs. +#[instrument(skip_all, level = "debug", fields(blob_count = blobs.len()))] pub fn blobs_to_data_column_sidecars( blobs: &[&Blob], cell_proofs: Vec, @@ -182,27 +174,35 @@ pub fn blobs_to_data_column_sidecars( let kzg_commitments_inclusion_proof = block.message().body().kzg_commitments_merkle_proof()?; let signed_block_header = block.signed_block_header(); + if cell_proofs.len() != blobs.len() * E::number_of_columns() { + return Err(DataColumnSidecarError::InvalidCellProofLength { + expected: blobs.len() * E::number_of_columns(), + actual: cell_proofs.len(), + }); + } + let proof_chunks = cell_proofs - .chunks_exact(spec.number_of_columns as usize) + .chunks_exact(E::number_of_columns()) .collect::>(); // NOTE: assumes blob sidecars are ordered by index - let blob_cells_and_proofs_vec = blobs + let zipped: Vec<_> = blobs.iter().zip(proof_chunks).collect(); + let blob_cells_and_proofs_vec = zipped .into_par_iter() - .zip(proof_chunks.into_par_iter()) .map(|(blob, proofs)| { - let blob = blob - .as_ref() - .try_into() - .expect("blob should have a guaranteed size due to FixedVector"); + let blob = blob.as_ref().try_into().map_err(|e| { + KzgError::InconsistentArrayLength(format!( + "blob should have a guaranteed size due to FixedVector: {e:?}" + )) + })?; - kzg.compute_cells(blob).map(|cells| { - ( - cells, - proofs - .try_into() - .expect("proof chunks should have exactly `number_of_columns` proofs"), - ) + kzg.compute_cells(blob).and_then(|cells| { + let proofs = proofs.try_into().map_err(|e| { + KzgError::InconsistentArrayLength(format!( + "proof chunks should have exactly `number_of_columns` proofs: {e:?}" + )) + })?; + Ok((cells, proofs)) }) }) .collect::, KzgError>>()?; @@ -221,10 +221,11 @@ pub fn compute_cells(blobs: &[&Blob], kzg: &Kzg) -> Result = blob.as_ref().try_into().map_err(|e| { + KzgError::InconsistentArrayLength(format!( + "blob should have a guaranteed size due to FixedVector: {e:?}", + )) + })?; kzg.compute_cells(blob) }) @@ -241,7 +242,7 @@ pub(crate) fn build_data_column_sidecars( blob_cells_and_proofs_vec: Vec, spec: &ChainSpec, ) -> Result, String> { - let number_of_columns = spec.number_of_columns as usize; + let number_of_columns = E::number_of_columns(); let max_blobs_per_block = spec .max_blobs_per_block(signed_block_header.message.slot.epoch(E::slots_per_epoch())) as usize; @@ -257,7 +258,8 @@ pub(crate) fn build_data_column_sidecars( .get(col) .ok_or(format!("Missing blob cell at index {col}"))?; let cell: Vec = cell.to_vec(); - let cell = Cell::::from(cell); + let cell = + Cell::::try_from(cell).map_err(|e| format!("BytesPerCell exceeded: {e:?}"))?; let proof = blob_cell_proofs .get(col) @@ -275,37 +277,45 @@ pub(crate) fn build_data_column_sidecars( } } - let sidecars: Vec>> = columns + let sidecars: Result>>, String> = columns .into_iter() .zip(column_kzg_proofs) .enumerate() - .map(|(index, (col, proofs))| { - Arc::new(DataColumnSidecar { - index: index as u64, - column: DataColumn::::from(col), - kzg_commitments: kzg_commitments.clone(), - kzg_proofs: VariableList::from(proofs), - signed_block_header: signed_block_header.clone(), - kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), - }) - }) + .map( + |(index, (col, proofs))| -> Result>, String> { + Ok(Arc::new(DataColumnSidecar { + index: index as u64, + column: DataColumn::::try_from(col) + .map_err(|e| format!("MaxBlobCommitmentsPerBlock exceeded: {e:?}"))?, + kzg_commitments: kzg_commitments.clone(), + kzg_proofs: VariableList::try_from(proofs) + .map_err(|e| format!("MaxBlobCommitmentsPerBlock exceeded: {e:?}"))?, + signed_block_header: signed_block_header.clone(), + kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), + })) + }, + ) .collect(); - Ok(sidecars) + sidecars } /// Reconstruct blobs from a subset of data column sidecars (requires at least 50%). /// /// If `blob_indices_opt` is `None`, this function attempts to reconstruct all blobs associated /// with the block. +/// This function does NOT use rayon as this is primarily used by a non critical path in HTTP API +/// and it will be slow if the node needs to reconstruct the blobs pub fn reconstruct_blobs( kzg: &Kzg, - data_columns: &[Arc>], + mut data_columns: Vec>>, blob_indices_opt: Option>, signed_block: &SignedBlindedBeaconBlock, spec: &ChainSpec, ) -> Result, String> { - // The data columns are from the database, so we assume their correctness. + // Sort data columns by index to ensure ascending order for KZG operations + data_columns.sort_unstable_by_key(|dc| dc.index); + let first_data_column = data_columns .first() .ok_or("data_columns should have at least one element".to_string())?; @@ -319,11 +329,11 @@ pub fn reconstruct_blobs( }; let blob_sidecars = blob_indices - .into_par_iter() + .into_iter() .map(|row_index| { let mut cells: Vec = vec![]; let mut cell_ids: Vec = vec![]; - for data_column in data_columns { + for data_column in &data_columns { let cell = data_column .column .get(row_index) @@ -336,16 +346,26 @@ pub fn reconstruct_blobs( cell_ids.push(data_column.index); } - let (cells, _kzg_proofs) = kzg - .recover_cells_and_compute_kzg_proofs(&cell_ids, &cells) - .map_err(|e| format!("Failed to recover cells and compute KZG proofs: {e:?}"))?; + let num_cells_original_blob = E::number_of_columns() / 2; + let blob_bytes = if data_columns.len() < E::number_of_columns() { + let (recovered_cells, _kzg_proofs) = kzg + .recover_cells_and_compute_kzg_proofs(&cell_ids, &cells) + .map_err(|e| { + format!("Failed to recover cells and compute KZG proofs: {e:?}") + })?; - let num_cells_original_blob = cells.len() / 2; - let blob_bytes = cells - .into_iter() - .take(num_cells_original_blob) - .flat_map(|cell| cell.into_iter()) - .collect(); + recovered_cells + .into_iter() + .take(num_cells_original_blob) + .flat_map(|cell| cell.into_iter()) + .collect() + } else { + cells + .into_iter() + .take(num_cells_original_blob) + .flat_map(|cell| (*cell).into_iter()) + .collect() + }; let blob = Blob::::new(blob_bytes).map_err(|e| format!("{e:?}"))?; let kzg_proof = KzgProof::empty(); @@ -371,14 +391,18 @@ pub fn reconstruct_blobs( /// Reconstruct all data columns from a subset of data column sidecars (requires at least 50%). pub fn reconstruct_data_columns( kzg: &Kzg, - data_columns: &[Arc>], + mut data_columns: Vec>>, spec: &ChainSpec, ) -> Result, KzgError> { + // Sort data columns by index to ensure ascending order for KZG operations + data_columns.sort_unstable_by_key(|dc| dc.index); + let first_data_column = data_columns .first() .ok_or(KzgError::InconsistentArrayLength( "data_columns should have at least one element".to_string(), ))?; + let num_of_blobs = first_data_column.kzg_commitments.len(); let blob_cells_and_proofs_vec = @@ -387,7 +411,7 @@ pub fn reconstruct_data_columns( .map(|row_index| { let mut cells: Vec = vec![]; let mut cell_ids: Vec = vec![]; - for data_column in data_columns { + for data_column in &data_columns { let cell = data_column.column.get(row_index).ok_or( KzgError::InconsistentArrayLength(format!( "Missing data column at row index {row_index}" @@ -416,15 +440,16 @@ pub fn reconstruct_data_columns( mod test { use crate::kzg_utils::{ blobs_to_data_column_sidecars, reconstruct_blobs, reconstruct_data_columns, - validate_blobs_and_cell_proofs, + validate_data_columns, }; use bls::Signature; use eth2::types::BlobsBundle; use execution_layer::test_utils::generate_blobs; - use kzg::{trusted_setup::get_trusted_setup, Kzg, KzgCommitment, TrustedSetup}; + use kzg::{Kzg, KzgCommitment, trusted_setup::get_trusted_setup}; use types::{ - beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockFulu, BlobsList, ChainSpec, - EmptyBlock, EthSpec, ForkName, FullPayload, KzgProofs, MainnetEthSpec, SignedBeaconBlock, + BeaconBlock, BeaconBlockFulu, BlobsList, ChainSpec, EmptyBlock, EthSpec, ForkName, + FullPayload, KzgProofs, MainnetEthSpec, SignedBeaconBlock, + beacon_block_body::KzgCommitments, }; type E = MainnetEthSpec; @@ -438,22 +463,23 @@ mod test { test_build_data_columns_empty(&kzg, &spec); test_build_data_columns(&kzg, &spec); test_reconstruct_data_columns(&kzg, &spec); + test_reconstruct_data_columns_unordered(&kzg, &spec); test_reconstruct_blobs_from_data_columns(&kzg, &spec); - test_verify_blob_and_cell_proofs(&kzg); + test_reconstruct_blobs_from_data_columns_unordered(&kzg, &spec); + test_validate_data_columns(&kzg, &spec); } #[track_caller] - fn test_verify_blob_and_cell_proofs(kzg: &Kzg) { - let (blobs_bundle, _) = generate_blobs::(3, ForkName::Fulu).unwrap(); - let BlobsBundle { - blobs, - commitments, - proofs, - } = blobs_bundle; - - let result = - validate_blobs_and_cell_proofs::(kzg, blobs.iter().collect(), &proofs, &commitments); + fn test_validate_data_columns(kzg: &Kzg, spec: &ChainSpec) { + let num_of_blobs = 2; + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); + let column_sidecars = + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); + let result = validate_data_columns::(kzg, column_sidecars.iter()); assert!(result.is_ok()); } @@ -471,7 +497,8 @@ mod test { #[track_caller] fn test_build_data_columns(kzg: &Kzg, spec: &ChainSpec) { - let num_of_blobs = 6; + // Using at least 2 blobs to make sure we're arranging the data columns correctly. + let num_of_blobs = 2; let (signed_block, blobs, proofs) = create_test_fulu_block_and_blobs::(num_of_blobs, spec); @@ -492,7 +519,7 @@ mod test { .kzg_commitments_merkle_proof() .unwrap(); - assert_eq!(column_sidecars.len(), spec.number_of_columns as usize); + assert_eq!(column_sidecars.len(), E::number_of_columns()); for (idx, col_sidecar) in column_sidecars.iter().enumerate() { assert_eq!(col_sidecar.index, idx as u64); @@ -511,7 +538,8 @@ mod test { #[track_caller] fn test_reconstruct_data_columns(kzg: &Kzg, spec: &ChainSpec) { - let num_of_blobs = 6; + // Using at least 2 blobs to make sure we're arranging the data columns correctly. + let num_of_blobs = 2; let (signed_block, blobs, proofs) = create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); @@ -522,19 +550,41 @@ mod test { // Now reconstruct let reconstructed_columns = reconstruct_data_columns( kzg, - &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2].to_vec(), spec, ) .unwrap(); - for i in 0..spec.number_of_columns as usize { + for i in 0..E::number_of_columns() { + assert_eq!(reconstructed_columns.get(i), column_sidecars.get(i), "{i}"); + } + } + + #[track_caller] + fn test_reconstruct_data_columns_unordered(kzg: &Kzg, spec: &ChainSpec) { + // Using at least 2 blobs to make sure we're arranging the data columns correctly. + let num_of_blobs = 2; + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); + let column_sidecars = + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); + + // Test reconstruction with columns in reverse order (non-ascending) + let mut subset_columns: Vec<_> = + column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2].to_vec(); + subset_columns.reverse(); // This would fail without proper sorting in reconstruct_data_columns + let reconstructed_columns = reconstruct_data_columns(kzg, subset_columns, spec).unwrap(); + + for i in 0..E::number_of_columns() { assert_eq!(reconstructed_columns.get(i), column_sidecars.get(i), "{i}"); } } #[track_caller] fn test_reconstruct_blobs_from_data_columns(kzg: &Kzg, spec: &ChainSpec) { - let num_of_blobs = 6; + let num_of_blobs = 3; let (signed_block, blobs, proofs) = create_test_fulu_block_and_blobs::(num_of_blobs, spec); let blob_refs = blobs.iter().collect::>(); @@ -544,10 +594,11 @@ mod test { // Now reconstruct let signed_blinded_block = signed_block.into(); - let blob_indices = vec![3, 4, 5]; + // Using at least 2 blobs to make sure we're arranging the data columns correctly. + let blob_indices = vec![1, 2]; let reconstructed_blobs = reconstruct_blobs( kzg, - &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + column_sidecars[0..column_sidecars.len() / 2].to_vec(), Some(blob_indices.clone()), &signed_blinded_block, spec, @@ -565,11 +616,33 @@ mod test { } } + #[track_caller] + fn test_reconstruct_blobs_from_data_columns_unordered(kzg: &Kzg, spec: &ChainSpec) { + let num_of_blobs = 2; + let (signed_block, blobs, proofs) = + create_test_fulu_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); + let column_sidecars = + blobs_to_data_column_sidecars(&blob_refs, proofs.to_vec(), &signed_block, kzg, spec) + .unwrap(); + + // Test reconstruction with columns in reverse order (non-ascending) + let mut subset_columns: Vec<_> = + column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2].to_vec(); + subset_columns.reverse(); // This would fail without proper sorting in reconstruct_blobs + + let signed_blinded_block = signed_block.into(); + let reconstructed_blobs = + reconstruct_blobs(kzg, subset_columns, None, &signed_blinded_block, spec).unwrap(); + + for (i, original_blob) in blobs.iter().enumerate() { + let reconstructed_blob = &reconstructed_blobs.get(i).unwrap().blob; + assert_eq!(reconstructed_blob, original_blob, "{i}"); + } + } + fn get_kzg() -> Kzg { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - Kzg::new_from_trusted_setup_das_enabled(trusted_setup).expect("should create kzg") + Kzg::new_from_trusted_setup(&get_trusted_setup()).expect("should create kzg") } fn create_test_fulu_block_and_blobs( diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index fb47996100..4582d5f198 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -16,25 +16,20 @@ mod block_verification; pub mod block_verification_types; pub mod builder; pub mod canonical_head; -pub mod capella_readiness; pub mod chain_config; +pub mod custody_context; pub mod data_availability_checker; pub mod data_column_verification; -pub mod deneb_readiness; mod early_attester_cache; -pub mod eip7805_readiness; -pub mod electra_readiness; mod errors; -pub mod eth1_chain; -mod eth1_finalization_cache; pub mod events; pub mod execution_payload; pub mod fetch_blobs; pub mod fork_choice_signal; pub mod fork_revert; -pub mod fulu_readiness; pub mod graffiti_calculator; pub mod historical_blocks; +pub mod historical_data_columns; pub mod inclusion_list_verification; pub mod kzg_utils; pub mod light_client_finality_update_verification; @@ -49,7 +44,8 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -mod persisted_beacon_chain; +pub mod persisted_beacon_chain; +pub mod persisted_custody; mod persisted_fork_choice; mod pre_finalization_cache; pub mod proposer_prep_service; @@ -67,26 +63,29 @@ pub mod validator_pubkey_cache; pub use self::beacon_chain::{ AttestationProcessingOutcome, AvailabilityProcessingStatus, BeaconBlockResponse, BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, - ChainSegmentResult, ForkChoiceError, LightClientProducerEvent, OverrideForkchoiceUpdate, + ChainSegmentResult, ForkChoiceError, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, LightClientProducerEvent, OverrideForkchoiceUpdate, ProduceBlockVerification, StateSkipConfig, WhenSlotSkipped, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, }; pub use self::beacon_snapshot::BeaconSnapshot; pub use self::chain_config::ChainConfig; pub use self::errors::{BeaconChainError, BlockProductionError}; pub use self::historical_blocks::HistoricalBlockError; pub use attestation_verification::Error as AttestationError; -pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; +pub use beacon_fork_choice_store::{ + BeaconForkChoiceStore, Error as ForkChoiceStoreError, PersistedForkChoiceStoreV17, + PersistedForkChoiceStoreV28, +}; pub use block_verification::{ - build_blob_data_column_sidecars, get_block_root, BlockError, ExecutionPayloadError, - ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, IntoGossipVerifiedBlock, - InvalidSignature, PayloadVerificationOutcome, PayloadVerificationStatus, + BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, + PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, + get_block_root, }; pub use block_verification_types::AvailabilityPendingExecutedBlock; pub use block_verification_types::ExecutedBlock; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; -pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; +pub use custody_context::CustodyContext; pub use events::ServerSentEventHandler; pub use execution_layer::EngineState; pub use execution_payload::NotifyExecutionLayer; diff --git a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs index 879fa02f7d..2dc4de7d04 100644 --- a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs @@ -1,9 +1,9 @@ use crate::{BeaconChain, BeaconChainTypes}; -use derivative::Derivative; +use educe::Educe; use slot_clock::SlotClock; use std::time::Duration; use strum::AsRefStr; -use types::LightClientFinalityUpdate; +use types::{Hash256, LightClientFinalityUpdate, Slot}; /// Returned when a light client finality update was not successfully verified. It might not have been verified for /// two reasons: @@ -21,17 +21,42 @@ pub enum Error { /// /// Assuming the local clock is correct, the peer has sent an invalid message. TooEarly, - /// Light client finality update message does not match the locally constructed one. - InvalidLightClientFinalityUpdate, + /// Light client finalized update message does not match the locally constructed one, it has a + /// different signature slot. + MismatchedSignatureSlot { local: Slot, observed: Slot }, + /// Light client finalized update message does not match the locally constructed one, it has a + /// different finalized block header for the same signature slot. + MismatchedFinalizedHeader { + local_finalized_header_root: Hash256, + observed_finalized_header_root: Hash256, + signature_slot: Slot, + }, + /// Light client finalized update message does not match the locally constructed one, it has a + /// different attested block header for the same signature slot and finalized header. + MismatchedAttestedHeader { + local_attested_header_root: Hash256, + observed_attested_header_root: Hash256, + finalized_header_root: Hash256, + signature_slot: Slot, + }, + /// Light client finalized update message does not match the locally constructed one, it has a + /// different proof or sync aggregate for the same slot, attested header and finalized header. + MismatchedProofOrSyncAggregate { + attested_header_root: Hash256, + finalized_header_root: Hash256, + signature_slot: Slot, + }, /// Signature slot start time is none. SigSlotStartIsNone, /// Failed to construct a LightClientFinalityUpdate from state. FailedConstructingUpdate, + /// Silently ignore this light client finality update + Ignore, } /// Wraps a `LightClientFinalityUpdate` that has been verified for propagation on the gossip network. -#[derive(Derivative)] -#[derivative(Clone(bound = "T: BeaconChainTypes"))] +#[derive(Educe)] +#[educe(Clone(bound(T: BeaconChainTypes)))] pub struct VerifiedLightClientFinalityUpdate { light_client_finality_update: LightClientFinalityUpdate, seen_timestamp: Duration, @@ -48,7 +73,7 @@ impl VerifiedLightClientFinalityUpdate { // verify that enough time has passed for the block to have been propagated let start_time = chain .slot_clock - .start_of(*rcv_finality_update.signature_slot()) + .start_of(rcv_finality_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() @@ -57,16 +82,82 @@ impl VerifiedLightClientFinalityUpdate { return Err(Error::TooEarly); } + if let Some(latest_broadcasted_finality_update) = chain + .light_client_server_cache + .get_latest_broadcasted_finality_update() + { + // Ignore the incoming finality update if we've already broadcasted it + if latest_broadcasted_finality_update == rcv_finality_update { + return Err(Error::Ignore); + } + + // Ignore the incoming finality update if the latest broadcasted attested header slot + // is greater than the incoming attested header slot. + if latest_broadcasted_finality_update.get_attested_header_slot() + > rcv_finality_update.get_attested_header_slot() + { + return Err(Error::Ignore); + } + } + let latest_finality_update = chain .light_client_server_cache .get_latest_finality_update() .ok_or(Error::FailedConstructingUpdate)?; - // verify that the gossiped finality update is the same as the locally constructed one. - if latest_finality_update != rcv_finality_update { - return Err(Error::InvalidLightClientFinalityUpdate); + // Ignore the incoming finality update if the latest constructed attested header slot + // is greater than the incoming attested header slot. + if latest_finality_update.get_attested_header_slot() + > rcv_finality_update.get_attested_header_slot() + { + return Err(Error::Ignore); } + // Verify that the gossiped finality update is the same as the locally constructed one. + if latest_finality_update != rcv_finality_update { + let signature_slot = latest_finality_update.signature_slot(); + + if signature_slot != rcv_finality_update.signature_slot() { + // The locally constructed finality update is not up to date, probably + // because the node has fallen behind and needs to sync. + if rcv_finality_update.signature_slot() > signature_slot { + return Err(Error::Ignore); + } + return Err(Error::MismatchedSignatureSlot { + local: signature_slot, + observed: rcv_finality_update.signature_slot(), + }); + } + let local_finalized_header_root = latest_finality_update.get_finalized_header_root(); + let observed_finalized_header_root = rcv_finality_update.get_finalized_header_root(); + if local_finalized_header_root != observed_finalized_header_root { + return Err(Error::MismatchedFinalizedHeader { + local_finalized_header_root, + observed_finalized_header_root, + signature_slot, + }); + } + let local_attested_header_root = latest_finality_update.get_attested_header_root(); + let observed_attested_header_root = rcv_finality_update.get_attested_header_root(); + if local_attested_header_root != observed_attested_header_root { + return Err(Error::MismatchedAttestedHeader { + local_attested_header_root, + observed_attested_header_root, + finalized_header_root: local_finalized_header_root, + signature_slot, + }); + } + return Err(Error::MismatchedProofOrSyncAggregate { + attested_header_root: local_attested_header_root, + finalized_header_root: local_finalized_header_root, + signature_slot, + }); + } + + chain + .light_client_server_cache + .set_latest_broadcasted_finality_update(rcv_finality_update.clone()); + Ok(Self { light_client_finality_update: rcv_finality_update, seen_timestamp, diff --git a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs index 5665adc3ed..4079a374f8 100644 --- a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs @@ -1,10 +1,10 @@ use crate::{BeaconChain, BeaconChainTypes}; -use derivative::Derivative; +use educe::Educe; use eth2::types::Hash256; use slot_clock::SlotClock; use std::time::Duration; use strum::AsRefStr; -use types::LightClientOptimisticUpdate; +use types::{LightClientOptimisticUpdate, Slot}; /// Returned when a light client optimistic update was not successfully verified. It might not have been verified for /// two reasons: @@ -22,19 +22,35 @@ pub enum Error { /// /// Assuming the local clock is correct, the peer has sent an invalid message. TooEarly, - /// Light client optimistic update message does not match the locally constructed one. - InvalidLightClientOptimisticUpdate, + /// Light client optimistic update message does not match the locally constructed one, it has a + /// different signature slot. + MismatchedSignatureSlot { local: Slot, observed: Slot }, + /// Light client optimistic update message does not match the locally constructed one, it has a + /// different block header at the same slot. + MismatchedAttestedHeader { + local_attested_header_root: Hash256, + observed_attested_header_root: Hash256, + signature_slot: Slot, + }, + /// Light client optimistic update message does not match the locally constructed one, it has a + /// different sync aggregate for the same slot and attested header. + MismatchedSyncAggregate { + attested_header_root: Hash256, + signature_slot: Slot, + }, /// Signature slot start time is none. SigSlotStartIsNone, /// Failed to construct a LightClientOptimisticUpdate from state. FailedConstructingUpdate, /// Unknown block with parent root. UnknownBlockParentRoot(Hash256), + /// Silently ignore this light client optimistic update + Ignore, } /// Wraps a `LightClientOptimisticUpdate` that has been verified for propagation on the gossip network. -#[derive(Derivative)] -#[derivative(Clone(bound = "T: BeaconChainTypes"))] +#[derive(Educe)] +#[educe(Clone(bound(T: BeaconChainTypes)))] pub struct VerifiedLightClientOptimisticUpdate { light_client_optimistic_update: LightClientOptimisticUpdate, pub parent_root: Hash256, @@ -52,7 +68,7 @@ impl VerifiedLightClientOptimisticUpdate { // verify that enough time has passed for the block to have been propagated let start_time = chain .slot_clock - .start_of(*rcv_optimistic_update.signature_slot()) + .start_of(rcv_optimistic_update.signature_slot()) .ok_or(Error::SigSlotStartIsNone)?; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() @@ -61,6 +77,22 @@ impl VerifiedLightClientOptimisticUpdate { return Err(Error::TooEarly); } + if let Some(latest_broadcasted_optimistic_update) = chain + .light_client_server_cache + .get_latest_broadcasted_optimistic_update() + { + // Ignore the incoming optimistic update if we've already broadcasted it + if latest_broadcasted_optimistic_update == rcv_optimistic_update { + return Err(Error::Ignore); + } + + // Ignore the incoming optimistic update if the latest broadcasted slot + // is greater than the incoming slot. + if latest_broadcasted_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() { + return Err(Error::Ignore); + } + } + let head = chain.canonical_head.cached_head(); let head_block = &head.snapshot.beacon_block; // check if we can process the optimistic update immediately @@ -76,11 +108,45 @@ impl VerifiedLightClientOptimisticUpdate { .get_latest_optimistic_update() .ok_or(Error::FailedConstructingUpdate)?; - // verify that the gossiped optimistic update is the same as the locally constructed one. - if latest_optimistic_update != rcv_optimistic_update { - return Err(Error::InvalidLightClientOptimisticUpdate); + // Ignore the incoming optimistic update if the latest constructed slot + // is greater than the incoming slot. + if latest_optimistic_update.get_slot() > rcv_optimistic_update.get_slot() { + return Err(Error::Ignore); } + // Verify that the gossiped optimistic update is the same as the locally constructed one. + if latest_optimistic_update != rcv_optimistic_update { + let signature_slot = latest_optimistic_update.signature_slot(); + if signature_slot != rcv_optimistic_update.signature_slot() { + // The locally constructed optimistic update is not up to date, probably + // because the node has fallen behind and needs to sync. + if rcv_optimistic_update.signature_slot() > signature_slot { + return Err(Error::Ignore); + } + return Err(Error::MismatchedSignatureSlot { + local: signature_slot, + observed: rcv_optimistic_update.signature_slot(), + }); + } + let local_attested_header_root = latest_optimistic_update.get_canonical_root(); + let observed_attested_header_root = rcv_optimistic_update.get_canonical_root(); + if local_attested_header_root != observed_attested_header_root { + return Err(Error::MismatchedAttestedHeader { + local_attested_header_root, + observed_attested_header_root, + signature_slot, + }); + } + return Err(Error::MismatchedSyncAggregate { + attested_header_root: local_attested_header_root, + signature_slot, + }); + } + + chain + .light_client_server_cache + .set_latest_broadcasted_optimistic_update(rcv_optimistic_update.clone()); + let parent_root = rcv_optimistic_update.get_parent_root(); Ok(Self { light_client_optimistic_update: rcv_optimistic_update, diff --git a/beacon_node/beacon_chain/src/light_client_server_cache.rs b/beacon_node/beacon_chain/src/light_client_server_cache.rs index b7b6d1df18..487ddfd3ec 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -1,5 +1,5 @@ use crate::errors::BeaconChainError; -use crate::{metrics, BeaconChainTypes, BeaconStore}; +use crate::{BeaconChainTypes, BeaconStore, metrics}; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; use ssz::Decode; @@ -40,6 +40,10 @@ pub struct LightClientServerCache { latest_written_current_sync_committee: RwLock>>>, /// Caches state proofs by block root prev_block_cache: Mutex>>, + /// Tracks the latest broadcasted finality update + latest_broadcasted_finality_update: RwLock>>, + /// Tracks the latest broadcasted optimistic update + latest_broadcasted_optimistic_update: RwLock>>, } impl LightClientServerCache { @@ -49,6 +53,8 @@ impl LightClientServerCache { latest_optimistic_update: None.into(), latest_light_client_update: None.into(), latest_written_current_sync_committee: None.into(), + latest_broadcasted_finality_update: None.into(), + latest_broadcasted_optimistic_update: None.into(), prev_block_cache: lru::LruCache::new(PREV_BLOCK_CACHE_SIZE).into(), } } @@ -217,10 +223,9 @@ impl LightClientServerCache { ) -> Result<(), BeaconChainError> { if let Some(latest_sync_committee) = self.latest_written_current_sync_committee.read().clone() + && latest_sync_committee == cached_parts.current_sync_committee { - if latest_sync_committee == cached_parts.current_sync_committee { - return Ok(()); - } + return Ok(()); }; if finalized_period + 1 >= sync_committee_period { @@ -334,10 +339,89 @@ impl LightClientServerCache { Ok(new_value) } + /// Checks if we've already broadcasted the latest finality update. + /// If we haven't, update the `latest_broadcasted_finality_update` cache + /// and return the latest finality update for broadcasting, else return `None`. + pub fn should_broadcast_latest_finality_update( + &self, + ) -> Option> { + if let Some(latest_finality_update) = self.get_latest_finality_update() { + let latest_broadcasted_finality_update = self.get_latest_broadcasted_finality_update(); + match latest_broadcasted_finality_update { + Some(latest_broadcasted_finality_update) => { + if latest_broadcasted_finality_update != latest_finality_update { + self.set_latest_broadcasted_finality_update(latest_finality_update.clone()); + return Some(latest_finality_update); + } + } + None => { + self.set_latest_broadcasted_finality_update(latest_finality_update.clone()); + return Some(latest_finality_update); + } + } + } + + None + } + pub fn get_latest_finality_update(&self) -> Option> { self.latest_finality_update.read().clone() } + pub fn get_latest_broadcasted_optimistic_update( + &self, + ) -> Option> { + self.latest_broadcasted_optimistic_update.read().clone() + } + + pub fn get_latest_broadcasted_finality_update( + &self, + ) -> Option> { + self.latest_broadcasted_finality_update.read().clone() + } + + pub fn set_latest_broadcasted_optimistic_update( + &self, + optimistic_update: LightClientOptimisticUpdate, + ) { + *self.latest_broadcasted_optimistic_update.write() = Some(optimistic_update.clone()); + } + + pub fn set_latest_broadcasted_finality_update( + &self, + finality_update: LightClientFinalityUpdate, + ) { + *self.latest_broadcasted_finality_update.write() = Some(finality_update.clone()); + } + + /// Checks if we've already broadcasted the latest optimistic update. + /// If we haven't, update the `latest_broadcasted_optimistic_update` cache + /// and return the latest optimistic update for broadcasting, else return `None`. + pub fn should_broadcast_latest_optimistic_update( + &self, + ) -> Option> { + if let Some(latest_optimistic_update) = self.get_latest_optimistic_update() { + let latest_broadcasted_optimistic_update = + self.get_latest_broadcasted_optimistic_update(); + match latest_broadcasted_optimistic_update { + Some(latest_broadcasted_optimistic_update) => { + if latest_broadcasted_optimistic_update != latest_optimistic_update { + self.set_latest_broadcasted_optimistic_update( + latest_optimistic_update.clone(), + ); + return Some(latest_optimistic_update); + } + } + None => { + self.set_latest_broadcasted_optimistic_update(latest_optimistic_update.clone()); + return Some(latest_optimistic_update); + } + } + } + + None + } + pub fn get_latest_optimistic_update(&self) -> Option> { self.latest_optimistic_update.read().clone() } @@ -374,15 +458,15 @@ impl LightClientServerCache { let Some(current_sync_committee_branch) = store.get_sync_committee_branch(block_root)? else { return Err(BeaconChainError::LightClientBootstrapError(format!( - "Sync committee branch for block root {:?} not found", + "Sync committee branch for block root {:?} not found. This typically occurs when the block is not a finalized checkpoint. Light client bootstrap is only supported for finalized checkpoint block roots.", block_root ))); }; if sync_committee_period > finalized_period { - return Err(BeaconChainError::LightClientBootstrapError( - format!("The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}"), - )); + return Err(BeaconChainError::LightClientBootstrapError(format!( + "The blocks sync committee period {sync_committee_period} is greater than the current finalized period {finalized_period}" + ))); } let Some(current_sync_committee) = store.get_sync_committee(sync_committee_period)? else { diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index b030cd809c..422c85364e 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -260,7 +260,7 @@ pub static UNAGGREGATED_ATTESTATION_GOSSIP_VERIFICATION_TIMES: LazyLock> LazyLock::new(|| { try_create_histogram( "beacon_attestation_processing_state_skip_seconds", - "Time spent on reading the state during attestation processing", + "Time spent on skipping the state during attestation processing", ) }); pub static ATTESTATION_PROCESSING_SIGNATURE_SETUP_TIMES: LazyLock> = @@ -408,9 +408,9 @@ pub static ATTESTATION_PROCESSING_BATCH_AGG_SIGNATURE_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram( - "beacon_attestation_processing_batch_unagg_signature_setup_times", - "Time spent on setting up for the signature verification of batch unaggregate processing" - ) + "beacon_attestation_processing_batch_unagg_signature_setup_times", + "Time spent on setting up for the signature verification of batch unaggregate processing", + ) }); pub static ATTESTATION_PROCESSING_BATCH_UNAGG_SIGNATURE_TIMES: LazyLock> = LazyLock::new(|| { @@ -458,12 +458,6 @@ pub static BEACON_EARLY_ATTESTER_CACHE_HITS: LazyLock> = Lazy ) }); -pub static BEACON_REQRESP_PRE_IMPORT_CACHE_SIZE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_reqresp_pre_import_cache_size", - "Current count of items of the reqresp pre import cache", - ) -}); pub static BEACON_REQRESP_PRE_IMPORT_CACHE_HITS: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -602,6 +596,14 @@ pub static FORK_CHOICE_READ_LOCK_AQUIRE_TIMES: LazyLock> = Laz exponential_buckets(1e-4, 4.0, 7), ) }); +pub static FORK_CHOICE_UPGRADABLE_READ_LOCK_AQUIRE_TIMES: LazyLock> = + LazyLock::new(|| { + try_create_histogram_with_buckets( + "beacon_fork_choice_upgradable_read_lock_aquire_seconds", + "Time taken to aquire the fork-choice upgradable read lock", + exponential_buckets(1e-4, 4.0, 7), + ) + }); pub static FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram_with_buckets( "beacon_fork_choice_write_lock_aquire_seconds", @@ -609,6 +611,18 @@ pub static FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES: LazyLock> = La exponential_buckets(1e-3, 4.0, 7), ) }); +pub static FORK_CHOICE_ENCODE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "beacon_fork_choice_encode_seconds", + "Time taken to SSZ encode the persisted fork choice data", + ) +}); +pub static FORK_CHOICE_COMPRESS_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "beacon_fork_choice_compress_seconds", + "Time taken to compress the persisted fork choice data", + ) +}); pub static BALANCES_CACHE_HITS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_balances_cache_hits_total", @@ -631,12 +645,6 @@ pub static PERSIST_OP_POOL: LazyLock> = LazyLock::new(|| { "Time taken to persist the operations pool", ) }); -pub static PERSIST_ETH1_CACHE: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "beacon_persist_eth1_cache", - "Time taken to persist the eth1 caches", - ) -}); pub static PERSIST_FORK_CHOICE: LazyLock> = LazyLock::new(|| { try_create_histogram( "beacon_persist_fork_choice", @@ -856,17 +864,17 @@ pub static ATTN_OBSERVATION_PREV_EPOCH_AGGREGATORS: LazyLock> = pub static SYNC_COMM_OBSERVATION_PREV_SLOT_SIGNERS: LazyLock> = LazyLock::new( || { try_create_int_gauge( - "beacon_sync_comm_observation_slot_signers", - "Count of sync committee contributors that have been seen by the beacon chain in the previous slot" - ) + "beacon_sync_comm_observation_slot_signers", + "Count of sync committee contributors that have been seen by the beacon chain in the previous slot", + ) }, ); pub static SYNC_COMM_OBSERVATION_PREV_SLOT_AGGREGATORS: LazyLock> = LazyLock::new( || { try_create_int_gauge( - "beacon_sync_comm_observation_slot_aggregators", - "Count of sync committee aggregators that have been seen by the beacon chain in the previous slot" - ) + "beacon_sync_comm_observation_slot_aggregators", + "Count of sync committee aggregators that have been seen by the beacon chain in the previous slot", + ) }, ); @@ -1027,10 +1035,10 @@ pub static VALIDATOR_MONITOR_PREV_EPOCH_ATTESTATIONS_MIN_DELAY_SECONDS: LazyLock Result, > = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_prev_epoch_attestations_min_delay_seconds", - "The min delay between when the validator should send the attestation and when it was received.", - &["validator"] - ) + "validator_monitor_prev_epoch_attestations_min_delay_seconds", + "The min delay between when the validator should send the attestation and when it was received.", + &["validator"], + ) }); pub static VALIDATOR_MONITOR_PREV_EPOCH_ATTESTATION_AGGREGATE_INCLUSIONS: LazyLock< Result, @@ -1088,10 +1096,10 @@ pub static VALIDATOR_MONITOR_PREV_EPOCH_AGGREGATES_MIN_DELAY_SECONDS: LazyLock< Result, > = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_prev_epoch_aggregates_min_delay_seconds", - "The min delay between when the validator should send the aggregate and when it was received.", - &["validator"] - ) + "validator_monitor_prev_epoch_aggregates_min_delay_seconds", + "The min delay between when the validator should send the aggregate and when it was received.", + &["validator"], + ) }); pub static VALIDATOR_MONITOR_PREV_EPOCH_EXITS_TOTAL: LazyLock> = LazyLock::new(|| { @@ -1130,10 +1138,10 @@ pub static VALIDATOR_MONITOR_PREV_EPOCH_SYNC_COMMITTEE_MESSAGES_MIN_DELAY_SECOND Result, > = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_prev_epoch_sync_committee_messages_min_delay_seconds", - "The min delay between when the validator should send the sync committee message and when it was received.", - &["validator"] - ) + "validator_monitor_prev_epoch_sync_committee_messages_min_delay_seconds", + "The min delay between when the validator should send the sync committee message and when it was received.", + &["validator"], + ) }); pub static VALIDATOR_MONITOR_PREV_EPOCH_SYNC_CONTRIBUTION_INCLUSIONS: LazyLock< Result, @@ -1165,10 +1173,10 @@ pub static VALIDATOR_MONITOR_PREV_EPOCH_SYNC_CONTRIBUTION_MIN_DELAY_SECONDS: Laz Result, > = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_prev_epoch_sync_contribution_min_delay_seconds", - "The min delay between when the validator should send the sync contribution and when it was received.", - &["validator"] - ) + "validator_monitor_prev_epoch_sync_contribution_min_delay_seconds", + "The min delay between when the validator should send the sync contribution and when it was received.", + &["validator"], + ) }); pub static VALIDATOR_MONITOR_VALIDATOR_IN_CURRENT_SYNC_COMMITTEE: LazyLock> = LazyLock::new(|| { @@ -1201,8 +1209,8 @@ pub static VALIDATOR_MONITOR_UNAGGREGATED_ATTESTATION_DELAY_SECONDS: LazyLock< > = LazyLock::new(|| { try_create_histogram_vec( "validator_monitor_unaggregated_attestation_delay_seconds", - "The delay between when the validator should send the attestation and when it was received.", - &["src", "validator"] + "The delay between when the validator sent the attestation and the start of the slot.", + &["src", "validator"], ) }); pub static VALIDATOR_MONITOR_SYNC_COMMITTEE_MESSAGES_TOTAL: LazyLock> = @@ -1216,10 +1224,10 @@ pub static VALIDATOR_MONITOR_SYNC_COMMITTEE_MESSAGES_TOTAL: LazyLock> = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_sync_committee_messages_delay_seconds", - "The delay between when the validator should send the sync committee message and when it was received.", - &["src", "validator"] - ) + "validator_monitor_sync_committee_messages_delay_seconds", + "The delay between when the validator should send the sync committee message and when it was received.", + &["src", "validator"], + ) }); pub static VALIDATOR_MONITOR_SYNC_CONTRIBUTIONS_TOTAL: LazyLock> = LazyLock::new(|| { @@ -1232,10 +1240,10 @@ pub static VALIDATOR_MONITOR_SYNC_CONTRIBUTIONS_TOTAL: LazyLock> = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_sync_contributions_delay_seconds", - "The delay between when the aggregator should send the sync contribution and when it was received.", - &["src", "validator"] - ) + "validator_monitor_sync_contributions_delay_seconds", + "The delay between when the aggregator should send the sync contribution and when it was received.", + &["src", "validator"], + ) }); pub static VALIDATOR_MONITOR_AGGREGATED_ATTESTATION_TOTAL: LazyLock> = LazyLock::new(|| { @@ -1248,10 +1256,10 @@ pub static VALIDATOR_MONITOR_AGGREGATED_ATTESTATION_TOTAL: LazyLock> = LazyLock::new(|| { try_create_histogram_vec( - "validator_monitor_aggregated_attestation_delay_seconds", - "The delay between then the validator should send the aggregate and when it was received.", - &["src", "validator"] - ) + "validator_monitor_aggregated_attestation_delay_seconds", + "The delay between then the validator should send the aggregate and when it was received.", + &["src", "validator"], + ) }); pub static VALIDATOR_MONITOR_ATTESTATION_IN_AGGREGATE_TOTAL: LazyLock> = LazyLock::new(|| { @@ -1299,10 +1307,10 @@ pub static VALIDATOR_MONITOR_SYNC_COMMITTEE_MESSAGE_IN_BLOCK_TOTAL: LazyLock< pub static VALIDATOR_MONITOR_ATTESTATION_IN_BLOCK_DELAY_SLOTS: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( - "validator_monitor_attestation_in_block_delay_slots", - "The excess slots (beyond the minimum delay) between the attestation slot and the block slot.", - &["src", "validator"] - ) + "validator_monitor_attestation_in_block_delay_slots", + "The excess slots (beyond the minimum delay) between the attestation slot and the block slot.", + &["src", "validator"], + ) }); pub static VALIDATOR_MONITOR_BEACON_BLOCK_TOTAL: LazyLock> = LazyLock::new(|| { @@ -1364,13 +1372,14 @@ pub static BEACON_BLOCK_DELAY_OBSERVED_SLOT_START: LazyLock> = ) }); -pub static BEACON_BLOB_DELAY_ALL_OBSERVED_SLOT_START: LazyLock> = - LazyLock::new(|| { +pub static BEACON_BLOB_DELAY_ALL_OBSERVED_SLOT_START: LazyLock> = LazyLock::new( + || { try_create_int_gauge( "beacon_blob_delay_all_observed_slot_start", - "Duration between the start of the block's slot and the time the block was observed.", + "Duration between the start of the block's slot and the time when all blobs have been observed.", ) - }); + }, +); pub static BEACON_BLOCK_DELAY_CONSENSUS_VERIFICATION_TIME: LazyLock> = LazyLock::new(|| { @@ -1409,20 +1418,21 @@ pub static BEACON_BLOCK_DELAY_IMPORTED_TIME: LazyLock> = LazyLo ) }); -pub static BEACON_BLOCK_DELAY_HEAD_IMPORTED_TIME: LazyLock> = - LazyLock::new(|| { +pub static BEACON_BLOCK_DELAY_HEAD_IMPORTED_TIME: LazyLock> = LazyLock::new( + || { try_create_int_gauge( - "beacon_block_delay_head_imported_time", - "Duration between the time that block was imported and the time when it was set as head.", - ) - }); + "beacon_block_delay_head_imported_time", + "Duration between the time that block was imported and the time when it was set as head.", + ) + }, +); pub static BEACON_BLOCK_DELAY_HEAD_SLOT_START_EXCEEDED_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_block_delay_head_slot_start_exceeded_total", - "A counter that is triggered when the duration between the start of the block's slot and the current time \ + "beacon_block_delay_head_slot_start_exceeded_total", + "A counter that is triggered when the duration between the start of the block's slot and the current time \ will result in failed attestations.", - ) + ) }); /* @@ -1702,7 +1712,9 @@ pub static BLOBS_FROM_EL_EXPECTED: LazyLock> = LazyLock::new(| try_create_histogram_with_buckets( "beacon_blobs_from_el_expected", "Number of blobs expected from the execution layer", - Ok(vec![0.0, 3.0, 6.0, 9.0, 12.0, 18.0, 24.0, 30.0]), + Ok(vec![ + 0.0, 3.0, 6.0, 9.0, 12.0, 18.0, 24.0, 30.0, 36.0, 42.0, 48.0, + ]), ) }); @@ -1710,7 +1722,9 @@ pub static BLOBS_FROM_EL_RECEIVED: LazyLock> = LazyLock::new(| try_create_histogram_with_buckets( "beacon_blobs_from_el_received_total", "Number of blobs fetched from the execution layer", - linear_buckets(0.0, 4.0, 20), + Ok(vec![ + 0.0, 3.0, 6.0, 9.0, 12.0, 18.0, 24.0, 30.0, 36.0, 42.0, 48.0, + ]), ) }); @@ -1832,35 +1846,30 @@ pub static KZG_VERIFICATION_BATCH_TIMES: LazyLock> = LazyLock: "Runtime of batched kzg verification", ) }); +/// For reference on how the kzg data column verification buckets were set, here are some numbers for 48 blobs: +/// * 1 column batch: 5.76 ms +/// * 8 columns batch: 34.3 ms +/// * 64 columns batch: 257 ms +/// * 128 columns batch: 508 ms pub static KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: LazyLock> = + // 7 exponential buckets between 0.002 and 0.128 seconds, with more granularity on the lower end. LazyLock::new(|| { - try_create_histogram_with_buckets( - "beacon_kzg_verification_data_column_single_seconds", - "Runtime of single data column kzg verification", - Ok(vec![ - 0.0005, 0.001, 0.0015, 0.002, 0.003, 0.004, 0.005, 0.007, 0.01, 0.02, 0.05, - ]), - ) - }); + try_create_histogram_with_buckets( + "beacon_kzg_verification_data_column_single_seconds", + "Runtime of single data column kzg verification", + exponential_buckets(0.002, 2.0, 7), + ) + }); pub static KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: LazyLock> = + // 10 exponential buckets between 0.002 and 1.024 seconds, with more + // granularity on the lower end. LazyLock::new(|| { - try_create_histogram_with_buckets( - "beacon_kzg_verification_data_column_batch_seconds", - "Runtime of batched data column kzg verification", - Ok(vec![ - 0.002, 0.004, 0.006, 0.008, 0.01, 0.012, 0.015, 0.02, 0.03, 0.05, 0.07, - ]), - ) - }); - -pub static BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES: LazyLock> = LazyLock::new( - || { - try_create_histogram( - "beacon_block_production_blobs_verification_seconds", - "Time taken to verify blobs against commitments and creating BlobSidecar objects in block production" - ) - }, -); + try_create_histogram_with_buckets( + "beacon_kzg_verification_data_column_batch_seconds", + "Runtime of batched data column kzg verification", + exponential_buckets(0.002, 2.0, 10), + ) + }); /* * Data Availability cache metrics @@ -1890,7 +1899,7 @@ pub static DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS: LazyLock> LazyLock::new(|| { try_create_int_counter( "beacon_data_availability_reconstructed_columns_total", - "Total count of reconstructed columns", + "Total count of useful reconstructed columns", ) }); @@ -1974,7 +1983,6 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { } let attestation_stats = beacon_chain.op_pool.attestation_stats(); - let chain_metrics = beacon_chain.metrics(); // Kept duplicated for backwards compatibility set_gauge_by_usize( @@ -1982,11 +1990,6 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { beacon_chain.store.state_cache_len(), ); - set_gauge_by_usize( - &BEACON_REQRESP_PRE_IMPORT_CACHE_SIZE, - chain_metrics.reqresp_pre_import_cache_len, - ); - let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); set_gauge_by_usize( &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 03c468a35e..bd232f2e8a 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -1,13 +1,13 @@ use crate::errors::BeaconChainError; -use crate::summaries_dag::{DAGStateSummaryV22, Error as SummariesDagError, StateSummariesDAG}; +use crate::summaries_dag::{DAGStateSummary, Error as SummariesDagError, StateSummariesDAG}; use parking_lot::Mutex; use std::collections::HashSet; use std::mem; -use std::sync::{mpsc, Arc}; +use std::sync::{Arc, mpsc}; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use store::hot_cold_store::{migrate_database, HotColdDBError}; -use store::{Error, ItemStore, StoreOp}; +use store::hot_cold_store::{HotColdDBError, migrate_database}; +use store::{Error, ItemStore, Split, StoreOp}; pub use store::{HotColdDB, MemoryStore}; use tracing::{debug, error, info, warn}; use types::{BeaconState, BeaconStateHash, Checkpoint, Epoch, EthSpec, Hash256, Slot}; @@ -223,15 +223,14 @@ impl, Cold: ItemStore> BackgroundMigrator { // Schedule another reconstruction batch if required and we have access to the // channel for requeueing. - if let Some(tx) = opt_tx { - if !db.get_anchor_info().all_historic_states_stored() { - if let Err(e) = tx.send(Notification::Reconstruction) { - error!( - error = ?e, - "Unable to requeue reconstruction notification" - ); - } - } + if let Some(tx) = opt_tx + && !db.get_anchor_info().all_historic_states_stored() + && let Err(e) = tx.send(Notification::Reconstruction) + { + error!( + error = ?e, + "Unable to requeue reconstruction notification" + ); } } Err(e) => { @@ -343,18 +342,23 @@ impl, Cold: ItemStore> BackgroundMigrator {} + Ok(split_change) => { + // Migration run, return the split before the migration + split_change.previous + } Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => { debug!( slot = slot.as_u64(), "Database migration postponed, unaligned finalized block" ); + // Migration did not run, return the current split info + db.get_split_info() } Err(e) => { warn!(error = ?e, "Database migration failed"); @@ -367,6 +371,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, new_finalized_checkpoint: Checkpoint, + split_prior_to_migration: Split, ) -> Result { let new_finalized_slot = new_finalized_checkpoint .epoch @@ -519,6 +525,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, BeaconChainError>>()?; - - // De-duplicate block roots to reduce block reads below - let summary_block_roots = HashSet::::from_iter( - state_summaries - .iter() - .map(|(_, summary)| summary.latest_block_root), - ); + .map(|(state_root, summary)| (state_root, summary.into())) + .collect::>(); // Sanity check, there is at least one summary with the new finalized block root - if !summary_block_roots.contains(&new_finalized_checkpoint.root) { + if !state_summaries + .iter() + .any(|(_, s)| s.latest_block_root == new_finalized_checkpoint.root) + { return Err(BeaconChainError::PruningError( PruningError::MissingSummaryForFinalizedCheckpoint( new_finalized_checkpoint.root, @@ -562,16 +550,31 @@ impl, Cold: ItemStore> BackgroundMigrator 1 { + let state_summaries_dag_roots_post_split = state_summaries_dag_roots + .iter() + .filter(|(_, s)| s.slot >= split_prior_to_migration.slot) + .collect::>(); + + // Because of the additional HDiffs kept for the grid prior to finalization the tree_roots + // function will consider them roots. Those are expected. We just want to assert that the + // relevant tree of states (post-split) is well-formed. + // + // This warning could also fire if we have imported a block that doesn't descend from the + // new finalized state, and has had its ancestor state summaries pruned by a previous + // run. See: https://github.com/sigp/lighthouse/issues/7270. + if state_summaries_dag_roots_post_split.len() > 1 { warn!( - state_summaries_dag_roots = ?state_summaries_dag_roots, + location = "pruning", + new_finalized_state_root = ?new_finalized_state_root, + split_prior_to_migration_slot = %split_prior_to_migration.slot, + state_summaries_dag_roots_post_split = ?state_summaries_dag_roots_post_split, error = "summaries dag found more than one root", "Notify the devs your hot DB has some inconsistency. Pruning will fix it but devs want to know about it", ); @@ -626,10 +629,17 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); + let mut kept_summaries_for_hdiff = vec![]; // Consider the following block tree where we finalize block `[0]` at the checkpoint `(f)`. // There's a block `[3]` that descendends from the finalized block but NOT from the @@ -650,6 +660,30 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, i: usize) { match a { - Attestation::Base(ref mut att) => att + Attestation::Base(att) => att .aggregation_bits .set(i, false) .expect("should unset aggregation bit"), - Attestation::Electra(ref mut att) => att + Attestation::Electra(att) => att .aggregation_bits .set(i, false) .expect("should unset aggregation bit"), diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index 20ed36ace7..b2c5cb4b38 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -473,7 +473,8 @@ where #[cfg(not(debug_assertions))] mod tests { use super::*; - use types::{test_utils::test_random_instance, AttestationBase, FixedBytesExtended, Hash256}; + use fixed_bytes::FixedBytesExtended; + use types::{AttestationBase, Hash256, test_utils::test_random_instance}; type E = types::MainnetEthSpec; diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index 5bba8e4d8e..d5433f49d1 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -19,8 +19,9 @@ use bitvec::vec::BitVec; use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; +use typenum::Unsigned; use types::slot_data::SlotData; -use types::{Epoch, EthSpec, Hash256, Slot, Unsigned}; +use types::{Epoch, EthSpec, Hash256, Slot}; /// The maximum capacity of the `AutoPruningEpochContainer`. /// @@ -619,7 +620,7 @@ impl SlotSubcommitteeIndex { #[cfg(test)] mod tests { use super::*; - use types::FixedBytesExtended; + use fixed_bytes::FixedBytesExtended; type E = types::MainnetEthSpec; @@ -633,18 +634,24 @@ mod tests { let value = Hash256::zero(); // Assert there is no entry. - assert!(store - .observation_for_validator(key, validator_index) - .unwrap() - .is_none()); - assert!(!store - .validator_has_been_observed(key, validator_index) - .unwrap()); + assert!( + store + .observation_for_validator(key, validator_index) + .unwrap() + .is_none() + ); + assert!( + !store + .validator_has_been_observed(key, validator_index) + .unwrap() + ); // Add an entry. - assert!(!store - .observe_validator(key, validator_index, value) - .unwrap()); + assert!( + !store + .observe_validator(key, validator_index, value) + .unwrap() + ); // Assert there is a correct entry. assert_eq!( @@ -653,9 +660,11 @@ mod tests { .unwrap(), Some(value) ); - assert!(store - .validator_has_been_observed(key, validator_index) - .unwrap()); + assert!( + store + .validator_has_been_observed(key, validator_index) + .unwrap() + ); let alternate_value = Hash256::from_low_u64_be(1); diff --git a/beacon_node/beacon_chain/src/observed_block_producers.rs b/beacon_node/beacon_chain/src/observed_block_producers.rs index 096c8bff77..b740735ac4 100644 --- a/beacon_node/beacon_chain/src/observed_block_producers.rs +++ b/beacon_node/beacon_chain/src/observed_block_producers.rs @@ -4,7 +4,8 @@ use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{BeaconBlockRef, Epoch, EthSpec, Hash256, Slot, Unsigned}; +use typenum::Unsigned; +use types::{BeaconBlockRef, Epoch, EthSpec, Hash256, Slot}; #[derive(Debug, PartialEq)] pub enum Error { diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index 1ca6c03f00..46a3678f16 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -58,8 +58,8 @@ impl ObservableDataSidecar for DataColumnSidecar { self.index } - fn max_num_of_items(spec: &ChainSpec, _slot: Slot) -> usize { - spec.number_of_columns as usize + fn max_num_of_items(_spec: &ChainSpec, _slot: Slot) -> usize { + E::number_of_columns() } } @@ -124,6 +124,10 @@ impl ObservedDataSidecars { Ok(is_known) } + pub fn known_for_proposal(&self, proposal_key: &ProposalKey) -> Option<&HashSet> { + self.items.get(proposal_key) + } + fn sanitize_data_sidecar(&self, data_sidecar: &T) -> Result<(), Error> { if data_sidecar.index() >= T::max_num_of_items(&self.spec, data_sidecar.slot()) as u64 { return Err(Error::InvalidDataIndex(data_sidecar.index())); @@ -161,6 +165,7 @@ pub trait ObservationStrategy { /// Type for messages that are observed immediately. pub struct Observe; /// Type for messages that have not been observed. +#[derive(Debug)] pub struct DoNotObserve; impl ObservationStrategy for Observe { diff --git a/beacon_node/beacon_chain/src/observed_operations.rs b/beacon_node/beacon_chain/src/observed_operations.rs index 969d03a11b..4ca5371242 100644 --- a/beacon_node/beacon_chain/src/observed_operations.rs +++ b/beacon_node/beacon_chain/src/observed_operations.rs @@ -1,5 +1,5 @@ -use derivative::Derivative; -use smallvec::{smallvec, SmallVec}; +use educe::Educe; +use smallvec::{SmallVec, smallvec}; use state_processing::{SigVerifiedOp, TransformPersist, VerifyOperation, VerifyOperationAt}; use std::collections::HashSet; use std::marker::PhantomData; @@ -14,8 +14,8 @@ pub const SMALL_VEC_SIZE: usize = 8; /// Stateful tracker for exit/slashing operations seen on the network. /// /// Implements the conditions for gossip verification of exits and slashings from the P2P spec. -#[derive(Debug, Derivative)] -#[derivative(Default(bound = "T: ObservableOperation, E: EthSpec"))] +#[derive(Debug, Educe)] +#[educe(Default(bound(T: ObservableOperation, E: EthSpec)))] pub struct ObservedOperations, E: EthSpec> { /// Indices of validators for whom we have already seen an instance of an operation `T`. /// @@ -26,7 +26,7 @@ pub struct ObservedOperations, E: EthSpec> { /// `attestation_1.attester_indices` and `attestation_2.attester_indices`. observed_validator_indices: HashSet, /// The name of the current fork. The default will be overwritten on first use. - #[derivative(Default(value = "ForkName::Base"))] + #[educe(Default(expression = ForkName::Base))] current_fork: ForkName, _phantom: PhantomData<(T, E)>, } diff --git a/beacon_node/beacon_chain/src/observed_slashable.rs b/beacon_node/beacon_chain/src/observed_slashable.rs index 001a0d4a86..704d605436 100644 --- a/beacon_node/beacon_chain/src/observed_slashable.rs +++ b/beacon_node/beacon_chain/src/observed_slashable.rs @@ -5,7 +5,8 @@ use crate::observed_block_producers::Error; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{EthSpec, Hash256, Slot, Unsigned}; +use typenum::Unsigned; +use types::{EthSpec, Hash256, Slot}; #[derive(Eq, Hash, PartialEq, Debug, Default)] pub struct ProposalKey { diff --git a/beacon_node/beacon_chain/src/persisted_custody.rs b/beacon_node/beacon_chain/src/persisted_custody.rs new file mode 100644 index 0000000000..ba221c67b5 --- /dev/null +++ b/beacon_node/beacon_chain/src/persisted_custody.rs @@ -0,0 +1,46 @@ +use crate::custody_context::CustodyContextSsz; +use ssz::{Decode, Encode}; +use std::sync::Arc; +use store::{DBColumn, Error as StoreError, HotColdDB, ItemStore, StoreItem}; +use types::{EthSpec, Hash256}; + +/// 32-byte key for accessing the `CustodyContext`. All zero because `CustodyContext` has its own column. +pub const CUSTODY_DB_KEY: Hash256 = Hash256::ZERO; + +pub struct PersistedCustody(pub CustodyContextSsz); + +pub fn load_custody_context, Cold: ItemStore>( + store: Arc>, +) -> Option { + let res: Result, _> = + store.get_item::(&CUSTODY_DB_KEY); + // Load context from the store + match res { + Ok(Some(c)) => Some(c.0), + _ => None, + } +} + +/// Attempt to persist the custody context object to `self.store`. +pub fn persist_custody_context, Cold: ItemStore>( + store: Arc>, + custody_context: CustodyContextSsz, +) -> Result<(), store::Error> { + store.put_item(&CUSTODY_DB_KEY, &PersistedCustody(custody_context)) +} + +impl StoreItem for PersistedCustody { + fn db_column() -> DBColumn { + DBColumn::CustodyContext + } + + fn as_store_bytes(&self) -> Vec { + self.0.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + let custody_context = CustodyContextSsz::from_ssz_bytes(bytes)?; + + Ok(PersistedCustody(custody_context)) + } +} diff --git a/beacon_node/beacon_chain/src/persisted_fork_choice.rs b/beacon_node/beacon_chain/src/persisted_fork_choice.rs index 8961a74c3d..d8fcc0901b 100644 --- a/beacon_node/beacon_chain/src/persisted_fork_choice.rs +++ b/beacon_node/beacon_chain/src/persisted_fork_choice.rs @@ -1,16 +1,30 @@ -use crate::beacon_fork_choice_store::PersistedForkChoiceStoreV17; +use crate::{ + beacon_fork_choice_store::{PersistedForkChoiceStoreV17, PersistedForkChoiceStoreV28}, + metrics, +}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; -use store::{DBColumn, Error, StoreItem}; +use store::{DBColumn, Error, KeyValueStoreOp, StoreConfig, StoreItem}; use superstruct::superstruct; +use types::Hash256; // If adding a new version you should update this type alias and fix the breakages. -pub type PersistedForkChoice = PersistedForkChoiceV17; +pub type PersistedForkChoice = PersistedForkChoiceV28; -#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)] +#[superstruct( + variants(V17, V28), + variant_attributes(derive(Encode, Decode)), + no_enum +)] pub struct PersistedForkChoice { - pub fork_choice: fork_choice::PersistedForkChoice, - pub fork_choice_store: PersistedForkChoiceStoreV17, + #[superstruct(only(V17))] + pub fork_choice_v17: fork_choice::PersistedForkChoiceV17, + #[superstruct(only(V28))] + pub fork_choice: fork_choice::PersistedForkChoiceV28, + #[superstruct(only(V17))] + pub fork_choice_store_v17: PersistedForkChoiceStoreV17, + #[superstruct(only(V28))] + pub fork_choice_store: PersistedForkChoiceStoreV28, } macro_rules! impl_store_item { @@ -32,3 +46,35 @@ macro_rules! impl_store_item { } impl_store_item!(PersistedForkChoiceV17); + +impl PersistedForkChoiceV28 { + pub fn from_bytes(bytes: &[u8], store_config: &StoreConfig) -> Result { + let decompressed_bytes = store_config + .decompress_bytes(bytes) + .map_err(Error::Compression)?; + Self::from_ssz_bytes(&decompressed_bytes).map_err(Into::into) + } + + pub fn as_bytes(&self, store_config: &StoreConfig) -> Result, Error> { + let encode_timer = metrics::start_timer(&metrics::FORK_CHOICE_ENCODE_TIMES); + let ssz_bytes = self.as_ssz_bytes(); + drop(encode_timer); + + let _compress_timer = metrics::start_timer(&metrics::FORK_CHOICE_COMPRESS_TIMES); + store_config + .compress_bytes(&ssz_bytes) + .map_err(Error::Compression) + } + + pub fn as_kv_store_op( + &self, + key: Hash256, + store_config: &StoreConfig, + ) -> Result { + Ok(KeyValueStoreOp::PutKeyValue( + DBColumn::ForkChoice, + key.as_slice().to_vec(), + self.as_bytes(store_config)?, + )) + } +} diff --git a/beacon_node/beacon_chain/src/pre_finalization_cache.rs b/beacon_node/beacon_chain/src/pre_finalization_cache.rs index 5bd45dc59f..8996d6b874 100644 --- a/beacon_node/beacon_chain/src/pre_finalization_cache.rs +++ b/beacon_node/beacon_chain/src/pre_finalization_cache.rs @@ -5,8 +5,8 @@ use parking_lot::Mutex; use std::num::NonZeroUsize; use std::time::Duration; use tracing::debug; -use types::non_zero_usize::new_non_zero_usize; use types::Hash256; +use types::non_zero_usize::new_non_zero_usize; const BLOCK_ROOT_CACHE_LIMIT: NonZeroUsize = new_non_zero_usize(512); const LOOKUP_LIMIT: NonZeroUsize = new_non_zero_usize(8); diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 49aa116f6c..ddc5978339 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -1,20 +1,20 @@ //! Utilities for managing database schema changes. -mod migration_schema_v20; -mod migration_schema_v21; -mod migration_schema_v22; mod migration_schema_v23; +mod migration_schema_v24; +mod migration_schema_v25; +mod migration_schema_v26; +mod migration_schema_v27; +mod migration_schema_v28; use crate::beacon_chain::BeaconChainTypes; use std::sync::Arc; -use store::hot_cold_store::{HotColdDB, HotColdDBError}; -use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; use store::Error as StoreError; -use types::Hash256; +use store::hot_cold_store::{HotColdDB, HotColdDBError}; +use store::metadata::{CURRENT_SCHEMA_VERSION, SchemaVersion}; /// Migrate the database from one schema version to another, applying all requisite mutations. pub fn migrate_schema( db: Arc>, - genesis_state_root: Option, from: SchemaVersion, to: SchemaVersion, ) -> Result<(), StoreError> { @@ -24,40 +24,19 @@ pub fn migrate_schema( // Upgrade across multiple versions by recursively migrating one step at a time. (_, _) if from.as_u64() + 1 < to.as_u64() => { let next = SchemaVersion(from.as_u64() + 1); - migrate_schema::(db.clone(), genesis_state_root, from, next)?; - migrate_schema::(db, genesis_state_root, next, to) + migrate_schema::(db.clone(), from, next)?; + migrate_schema::(db, next, to) } // Downgrade across multiple versions by recursively migrating one step at a time. (_, _) if to.as_u64() + 1 < from.as_u64() => { let next = SchemaVersion(from.as_u64() - 1); - migrate_schema::(db.clone(), genesis_state_root, from, next)?; - migrate_schema::(db, genesis_state_root, next, to) + migrate_schema::(db.clone(), from, next)?; + migrate_schema::(db, next, to) } // - // Migrations from before SchemaVersion(19) are deprecated. + // Migrations from before SchemaVersion(22) are deprecated. // - (SchemaVersion(19), SchemaVersion(20)) => { - let ops = migration_schema_v20::upgrade_to_v20::(db.clone())?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(20), SchemaVersion(19)) => { - let ops = migration_schema_v20::downgrade_from_v20::(db.clone())?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(20), SchemaVersion(21)) => { - let ops = migration_schema_v21::upgrade_to_v21::(db.clone())?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(21), SchemaVersion(20)) => { - let ops = migration_schema_v21::downgrade_from_v21::(db.clone())?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(21), SchemaVersion(22)) => { - // This migration needs to sync data between hot and cold DBs. The schema version is - // bumped inside the upgrade_to_v22 fn - migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root) - } (SchemaVersion(22), SchemaVersion(23)) => { let ops = migration_schema_v23::upgrade_to_v23::(db.clone())?; db.store_schema_version_atomically(to, ops) @@ -66,6 +45,49 @@ pub fn migrate_schema( let ops = migration_schema_v23::downgrade_from_v23::(db.clone())?; db.store_schema_version_atomically(to, ops) } + (SchemaVersion(23), SchemaVersion(24)) => { + let ops = migration_schema_v24::upgrade_to_v24::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(24), SchemaVersion(23)) => { + let ops = migration_schema_v24::downgrade_from_v24::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(24), SchemaVersion(25)) => { + let ops = migration_schema_v25::upgrade_to_v25()?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(25), SchemaVersion(24)) => { + let ops = migration_schema_v25::downgrade_from_v25()?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(25), SchemaVersion(26)) => { + let ops = migration_schema_v26::upgrade_to_v26::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(26), SchemaVersion(25)) => { + let ops = migration_schema_v26::downgrade_from_v26::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(26), SchemaVersion(27)) => { + // This migration updates the blobs db. The schema version + // is bumped inside upgrade_to_v27. + migration_schema_v27::upgrade_to_v27::(db.clone()) + } + (SchemaVersion(27), SchemaVersion(26)) => { + // Downgrading is essentially a no-op and is only possible + // if peer das isn't scheduled. + migration_schema_v27::downgrade_from_v27::(db.clone())?; + db.store_schema_version_atomically(to, vec![]) + } + (SchemaVersion(27), SchemaVersion(28)) => { + let ops = migration_schema_v28::upgrade_to_v28::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(28), SchemaVersion(27)) => { + let ops = migration_schema_v28::downgrade_from_v28::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs deleted file mode 100644 index 13fde349f5..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY}; -use operation_pool::{ - PersistedOperationPool, PersistedOperationPoolV15, PersistedOperationPoolV20, -}; -use std::sync::Arc; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; -use tracing::{debug, info}; -use types::Attestation; - -pub fn upgrade_to_v20( - db: Arc>, -) -> Result, Error> { - info!("Upgrading from v19 to v20"); - - // Load a V15 op pool and transform it to V20. - let Some(PersistedOperationPoolV15:: { - attestations_v15, - sync_contributions, - attester_slashings_v15, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - capella_bls_change_broadcast_indices, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!("Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - let attestations = attestations_v15 - .into_iter() - .map(|(attestation, indices)| (Attestation::Base(attestation).into(), indices)) - .collect(); - - let attester_slashings = attester_slashings_v15 - .into_iter() - .map(|slashing| slashing.into()) - .collect(); - - let v20 = PersistedOperationPool::V20(PersistedOperationPoolV20 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - capella_bls_change_broadcast_indices, - }); - Ok(vec![v20.as_kv_store_op(OP_POOL_DB_KEY)]) -} - -pub fn downgrade_from_v20( - db: Arc>, -) -> Result, Error> { - info!("Downgrading from v20 to v19"); - - // Load a V20 op pool and transform it to V15. - let Some(PersistedOperationPoolV20:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - capella_bls_change_broadcast_indices, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!("Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - let attestations_v15 = attestations - .into_iter() - .filter_map(|(attestation, indices)| { - if let Attestation::Base(attestation) = attestation.into() { - Some((attestation, indices)) - } else { - info!( - reason = "not a base attestation", - "Dropping attestation during downgrade" - ); - None - } - }) - .collect(); - - let attester_slashings_v15 = attester_slashings - .into_iter() - .filter_map(|slashing| match slashing.try_into() { - Ok(slashing) => Some(slashing), - Err(_) => { - info!( - reason = "not a base attester slashing", - "Dropping attester slashing during downgrade" - ); - None - } - }) - .collect(); - - let v15 = PersistedOperationPool::V15(PersistedOperationPoolV15 { - attestations_v15, - sync_contributions, - attester_slashings_v15, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - capella_bls_change_broadcast_indices, - }); - Ok(vec![v15.as_kv_store_op(OP_POOL_DB_KEY)]) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs deleted file mode 100644 index d73660cf3c..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::beacon_chain::BeaconChainTypes; -use crate::validator_pubkey_cache::DatabasePubkey; -use ssz::{Decode, Encode}; -use std::sync::Arc; -use store::{DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem}; -use tracing::info; -use types::{Hash256, PublicKey}; - -const LOG_EVERY: usize = 200_000; - -pub fn upgrade_to_v21( - db: Arc>, -) -> Result, Error> { - info!("Upgrading from v20 to v21"); - - let mut ops = vec![]; - - // Iterate through all pubkeys and decompress them. - for (i, res) in db - .hot_db - .iter_column::(DBColumn::PubkeyCache) - .enumerate() - { - let (key, value) = res?; - let pubkey = PublicKey::from_ssz_bytes(&value)?; - let decompressed = DatabasePubkey::from_pubkey(&pubkey); - ops.push(decompressed.as_kv_store_op(key)); - - if i > 0 && i % LOG_EVERY == 0 { - info!( - keys_decompressed = i, - "Public key decompression in progress" - ); - } - } - info!("Public key decompression complete"); - - Ok(ops) -} - -pub fn downgrade_from_v21( - db: Arc>, -) -> Result, Error> { - info!("Downgrading from v21 to v20"); - - let mut ops = vec![]; - - // Iterate through all pubkeys and recompress them. - for (i, res) in db - .hot_db - .iter_column::(DBColumn::PubkeyCache) - .enumerate() - { - let (key, value) = res?; - let decompressed = DatabasePubkey::from_ssz_bytes(&value)?; - let (_, pubkey_bytes) = decompressed.as_pubkey().map_err(|e| Error::DBError { - message: format!("{e:?}"), - })?; - - ops.push(KeyValueStoreOp::PutKeyValue( - DBColumn::PubkeyCache, - key.as_slice().to_vec(), - pubkey_bytes.as_ssz_bytes(), - )); - - if i > 0 && i % LOG_EVERY == 0 { - info!(keys_compressed = i, "Public key compression in progress"); - } - } - - info!("Public key compression complete"); - - Ok(ops) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs deleted file mode 100644 index a995f9d6b4..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::beacon_chain::BeaconChainTypes; -use std::sync::Arc; -use store::chunked_iter::ChunkedVectorIter; -use store::{ - chunked_vector::BlockRootsChunked, - metadata::{ - SchemaVersion, ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN, - }, - partial_beacon_state::PartialBeaconState, - AnchorInfo, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, -}; -use tracing::info; -use types::{BeaconState, Hash256, Slot}; - -const LOG_EVERY: usize = 200_000; - -fn load_old_schema_frozen_state( - db: &HotColdDB, - state_root: Hash256, -) -> Result>, Error> { - let Some(partial_state_bytes) = db - .cold_db - .get_bytes(DBColumn::BeaconState, state_root.as_slice())? - else { - return Ok(None); - }; - let mut partial_state: PartialBeaconState = - PartialBeaconState::from_ssz_bytes(&partial_state_bytes, db.get_chain_spec())?; - - // Fill in the fields of the partial state. - partial_state.load_block_roots(&db.cold_db, db.get_chain_spec())?; - partial_state.load_state_roots(&db.cold_db, db.get_chain_spec())?; - partial_state.load_historical_roots(&db.cold_db, db.get_chain_spec())?; - partial_state.load_randao_mixes(&db.cold_db, db.get_chain_spec())?; - partial_state.load_historical_summaries(&db.cold_db, db.get_chain_spec())?; - - partial_state.try_into().map(Some) -} - -pub fn upgrade_to_v22( - db: Arc>, - genesis_state_root: Option, -) -> Result<(), Error> { - info!("Upgrading DB schema from v21 to v22"); - - let old_anchor = db.get_anchor_info(); - - // If the anchor was uninitialized in the old schema (`None`), this represents a full archive - // node. - let effective_anchor = if old_anchor == ANCHOR_UNINITIALIZED { - ANCHOR_FOR_ARCHIVE_NODE - } else { - old_anchor.clone() - }; - - let split_slot = db.get_split_slot(); - let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; - - let mut cold_ops = vec![]; - - // Load the genesis state in the previous chunked format, BEFORE we go deleting or rewriting - // anything. - let mut genesis_state = load_old_schema_frozen_state::(&db, genesis_state_root)? - .ok_or(Error::MissingGenesisState)?; - let genesis_state_root = genesis_state.update_tree_hash_cache()?; - let genesis_block_root = genesis_state.get_latest_block_root(genesis_state_root); - - // Store the genesis state in the new format, prior to updating the schema version on disk. - // In case of a crash no data is lost because we will re-load it in the old format and re-do - // this write. - if split_slot > 0 { - info!( - state_root = ?genesis_state_root, - "Re-storing genesis state" - ); - db.store_cold_state(&genesis_state_root, &genesis_state, &mut cold_ops)?; - } - - // Write the block roots in the new format in a new column. Similar to above, we do this - // separately from deleting the old format block roots so that this is crash safe. - let oldest_block_slot = effective_anchor.oldest_block_slot; - write_new_schema_block_roots::( - &db, - genesis_block_root, - oldest_block_slot, - split_slot, - &mut cold_ops, - )?; - - // Commit this first batch of non-destructive cold database ops. - db.cold_db.do_atomically(cold_ops)?; - - // Now we update the anchor and the schema version atomically in the hot database. - // - // If we crash after commiting this change, then there will be some leftover cruft left in the - // freezer database, but no corruption because all the new-format data has already been written - // above. - let new_anchor = AnchorInfo { - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - ..effective_anchor.clone() - }; - let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, new_anchor)?]; - db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; - - // Finally, clean up the old-format data from the freezer database. - delete_old_schema_freezer_data::(&db)?; - - Ok(()) -} - -pub fn delete_old_schema_freezer_data( - db: &Arc>, -) -> Result<(), Error> { - let mut cold_ops = vec![]; - - let columns = [ - DBColumn::BeaconState, - // Cold state summaries indexed by state root were stored in this column. - DBColumn::BeaconStateSummary, - // Mapping from restore point number to state root was stored in this column. - DBColumn::BeaconRestorePoint, - // Chunked vector values were stored in these columns. - DBColumn::BeaconHistoricalRoots, - DBColumn::BeaconRandaoMixes, - DBColumn::BeaconHistoricalSummaries, - DBColumn::BeaconBlockRootsChunked, - DBColumn::BeaconStateRootsChunked, - ]; - - for column in columns { - for res in db.cold_db.iter_column_keys::>(column) { - let key = res?; - cold_ops.push(KeyValueStoreOp::DeleteKey(column, key)); - } - } - let delete_ops = cold_ops.len(); - - info!(delete_ops, "Deleting historic states"); - db.cold_db.do_atomically(cold_ops)?; - - // In order to reclaim space, we need to compact the freezer DB as well. - db.compact_freezer()?; - - Ok(()) -} - -pub fn write_new_schema_block_roots( - db: &HotColdDB, - genesis_block_root: Hash256, - oldest_block_slot: Slot, - split_slot: Slot, - cold_ops: &mut Vec, -) -> Result<(), Error> { - info!( - %oldest_block_slot, - ?genesis_block_root, - "Starting beacon block root migration" - ); - - // Store the genesis block root if it would otherwise not be stored. - if oldest_block_slot != 0 { - cold_ops.push(KeyValueStoreOp::PutKeyValue( - DBColumn::BeaconBlockRoots, - 0u64.to_be_bytes().to_vec(), - genesis_block_root.as_slice().to_vec(), - )); - } - - // Block roots are available from the `oldest_block_slot` to the `split_slot`. - let start_vindex = oldest_block_slot.as_usize(); - let block_root_iter = ChunkedVectorIter::::new( - db, - start_vindex, - split_slot, - db.get_chain_spec(), - ); - - // OK to hold these in memory (10M slots * 43 bytes per KV ~= 430 MB). - for (i, (slot, block_root)) in block_root_iter.enumerate() { - cold_ops.push(KeyValueStoreOp::PutKeyValue( - DBColumn::BeaconBlockRoots, - slot.to_be_bytes().to_vec(), - block_root.as_slice().to_vec(), - )); - - if i > 0 && i % LOG_EVERY == 0 { - info!( - roots_migrated = i, - "Beacon block root migration in progress" - ); - } - } - - Ok(()) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs index d0f8202679..e238e1efb6 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v23.rs @@ -1,8 +1,8 @@ -use crate::beacon_chain::BeaconChainTypes; -use crate::persisted_fork_choice::PersistedForkChoice; -use crate::schema_change::StoreError; -use crate::test_utils::{PersistedBeaconChain, BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY}; use crate::BeaconForkChoiceStore; +use crate::beacon_chain::BeaconChainTypes; +use crate::persisted_fork_choice::PersistedForkChoiceV17; +use crate::schema_change::StoreError; +use crate::test_utils::{BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY, PersistedBeaconChain}; use fork_choice::{ForkChoice, ResetPayloadStatuses}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -43,21 +43,26 @@ pub fn upgrade_to_v23( let state_root = state_root_result?; debug!( ?state_root, - "Deleting temporary state flag on v23 schema migration" + "Deleting temporary state on v23 schema migration" ); ops.push(KeyValueStoreOp::DeleteKey( DBColumn::BeaconStateTemporary, state_root.as_slice().to_vec(), )); - // Here we SHOULD delete the items for key `state_root` in columns `BeaconState` and - // `BeaconStateSummary`. However, in the event we have dangling temporary states at the time - // of the migration, the first pruning routine will prune them. They will be a tree branch / - // root not part of the finalized tree and trigger a warning log once. - // - // We believe there may be race conditions concerning temporary flags where a necessary - // canonical state is marked as temporary. In current stable, a restart with that DB will - // corrupt the DB. In the unlikely case this happens we choose to leave the states and - // allow pruning to clean them. + + // We also delete the temporary states themselves. Although there are known issue with + // temporary states and this could lead to DB corruption, we will only corrupt the DB in + // cases where the DB would be corrupted by restarting on v7.0.x. We consider these DBs + // "too far gone". Deleting here has the advantage of not generating warnings about + // disjoint state DAGs in the v24 upgrade, or the first pruning after migration. + ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + )); + ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateSummary, + state_root.as_slice().to_vec(), + )); } Ok(ops) @@ -75,7 +80,7 @@ pub fn downgrade_from_v23( }; // Recreate head-tracker from fork choice. - let Some(persisted_fork_choice) = db.get_item::(&FORK_CHOICE_DB_KEY)? + let Some(persisted_fork_choice) = db.get_item::(&FORK_CHOICE_DB_KEY)? else { // Fork choice should exist if the database exists. return Err(Error::MigrationError( @@ -83,19 +88,30 @@ pub fn downgrade_from_v23( )); }; - let fc_store = - BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, db.clone()) - .map_err(|e| { - Error::MigrationError(format!( - "Error loading fork choise store from persisted: {e:?}" - )) - })?; + // We use dummy roots for the justified states because we can source the balances from the v17 + // persited fork choice. The justified state root isn't required to look up the justified state's + // balances (as it would be in V28). This fork choice object with corrupt state roots SHOULD NOT + // be written to disk. + let dummy_justified_state_root = Hash256::repeat_byte(0x66); + let dummy_unrealized_justified_state_root = Hash256::repeat_byte(0x77); + + let fc_store = BeaconForkChoiceStore::from_persisted_v17( + persisted_fork_choice.fork_choice_store_v17, + dummy_justified_state_root, + dummy_unrealized_justified_state_root, + db.clone(), + ) + .map_err(|e| { + Error::MigrationError(format!( + "Error loading fork choice store from persisted: {e:?}" + )) + })?; // Doesn't matter what policy we use for invalid payloads, as our head calculation just // considers descent from finalization. let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload; let fork_choice = ForkChoice::from_persisted( - persisted_fork_choice.fork_choice, + persisted_fork_choice.fork_choice_v17.try_into()?, reset_payload_statuses, fc_store, &db.spec, @@ -106,7 +122,7 @@ pub fn downgrade_from_v23( let heads = fork_choice .proto_array() - .heads_descended_from_finalization::(); + .heads_descended_from_finalization::(fork_choice.finalized_checkpoint()); let head_roots = heads.iter().map(|node| node.root).collect(); let head_slots = heads.iter().map(|node| node.slot).collect(); diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs new file mode 100644 index 0000000000..1e1823a836 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v24.rs @@ -0,0 +1,605 @@ +use crate::{ + beacon_chain::BeaconChainTypes, + summaries_dag::{DAGStateSummary, DAGStateSummaryV22, StateSummariesDAG}, +}; +use ssz::{Decode, DecodeError, Encode}; +use ssz_derive::Encode; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use store::{ + DBColumn, Error, HotColdDB, HotStateSummary, KeyValueStore, KeyValueStoreOp, StoreItem, + hdiff::StorageStrategy, + hot_cold_store::{HotStateSummaryV22, OptionalDiffBaseState}, +}; +use tracing::{debug, info, warn}; +use types::{ + BeaconState, CACHED_EPOCHS, ChainSpec, Checkpoint, CommitteeCache, EthSpec, Hash256, Slot, +}; + +/// We stopped using the pruning checkpoint in schema v23 but never explicitly deleted it. +/// +/// We delete it as part of the v24 migration. +pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3); + +pub fn store_full_state_v22( + state_root: &Hash256, + state: &BeaconState, + ops: &mut Vec, +) -> Result<(), Error> { + let bytes = StorageContainer::new(state).as_ssz_bytes(); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + bytes, + )); + Ok(()) +} + +/// Fetch a V22 state from the database either as a full state or using block replay. +pub fn get_state_v22( + db: &Arc>, + state_root: &Hash256, + spec: &ChainSpec, +) -> Result>, Error> { + let Some(summary) = db.get_item::(state_root)? else { + return Ok(None); + }; + let Some(base_state) = + get_full_state_v22(&db.hot_db, &summary.epoch_boundary_state_root, spec)? + else { + return Ok(None); + }; + // Loading hot states via block replay doesn't care about the schema version, so we can use + // the DB's current method for this. + let update_cache = false; + db.load_hot_state_using_replay( + base_state, + summary.slot, + summary.latest_block_root, + update_cache, + ) + .map(Some) +} + +pub fn get_full_state_v22, E: EthSpec>( + db: &KV, + state_root: &Hash256, + spec: &ChainSpec, +) -> Result>, Error> { + match db.get_bytes(DBColumn::BeaconState, state_root.as_slice())? { + Some(bytes) => { + let container = StorageContainer::from_ssz_bytes(&bytes, spec)?; + Ok(Some(container.try_into()?)) + } + None => Ok(None), + } +} + +/// A container for storing `BeaconState` components. +/// +/// DEPRECATED. +#[derive(Encode)] +pub struct StorageContainer { + state: BeaconState, + committee_caches: Vec>, +} + +impl StorageContainer { + /// Create a new instance for storing a `BeaconState`. + pub fn new(state: &BeaconState) -> Self { + Self { + state: state.clone(), + committee_caches: state.committee_caches().to_vec(), + } + } + + pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { + // We need to use the slot-switching `from_ssz_bytes` of `BeaconState`, which doesn't + // compose with the other SSZ utils, so we duplicate some parts of `ssz_derive` here. + let mut builder = ssz::SszDecoderBuilder::new(bytes); + + builder.register_anonymous_variable_length_item()?; + builder.register_type::>()?; + + let mut decoder = builder.build()?; + + let state = decoder.decode_next_with(|bytes| BeaconState::from_ssz_bytes(bytes, spec))?; + let committee_caches = decoder.decode_next()?; + + Ok(Self { + state, + committee_caches, + }) + } +} + +impl TryInto> for StorageContainer { + type Error = Error; + + fn try_into(mut self) -> Result, Error> { + let mut state = self.state; + + for i in (0..CACHED_EPOCHS).rev() { + if i >= self.committee_caches.len() { + return Err(Error::SszDecodeError(DecodeError::BytesInvalid( + "Insufficient committees for BeaconState".to_string(), + ))); + }; + + state.committee_caches_mut()[i] = self.committee_caches.remove(i); + } + + Ok(state) + } +} + +/// The checkpoint used for pruning the database. +/// +/// Updated whenever pruning is successful. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PruningCheckpoint { + pub checkpoint: Checkpoint, +} + +impl StoreItem for PruningCheckpoint { + fn db_column() -> DBColumn { + DBColumn::BeaconMeta + } + + fn as_store_bytes(&self) -> Vec { + self.checkpoint.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(PruningCheckpoint { + checkpoint: Checkpoint::from_ssz_bytes(bytes)?, + }) + } +} + +pub fn upgrade_to_v24( + db: Arc>, +) -> Result, Error> { + let mut migrate_ops = vec![]; + let split = db.get_split_info(); + let hot_hdiff_start_slot = split.slot; + + // Delete the `PruningCheckpoint` (no longer used). + migrate_ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconMeta, + PRUNING_CHECKPOINT_KEY.as_slice().to_vec(), + )); + + // Sanity check to make sure the HDiff grid is aligned with the epoch start + if hot_hdiff_start_slot % T::EthSpec::slots_per_epoch() != 0 { + return Err(Error::MigrationError(format!( + "hot_hdiff_start_slot is not first slot in epoch {hot_hdiff_start_slot}" + ))); + } + + // After V24 hot tree states, the in-memory `anchor_info.anchor_slot` is the start slot of the + // hot HDiff grid. Before the migration, it's set to the slot of the anchor state in the DB: + // - the genesis state on a genesis sync, or + // - the checkpoint state on a checkpoint sync. + // + // If the node has been running for a while the `anchor_slot` might be less than the finalized + // checkpoint. This upgrade constructs a grid only with unfinalized states, rooted in the + // current finalized state. So we set the `anchor_slot` to `split.slot` to root the grid in the + // current finalized state. Each migration sets the split to + // ``` + // Split { slot: finalized_state.slot(), state_root: finalized_state_root } + // ``` + { + let anchor_info = db.get_anchor_info(); + + // If the node is already an archive node, we can set the anchor slot to 0 and copy + // snapshots and diffs from the freezer DB to the hot DB in order to establish an initial + // hot grid that is aligned/"perfect" (no `start_slot`/`anchor_slot` to worry about). + // + // This only works if all of the following are true: + // + // - We have the previous snapshot for the split state stored in the freezer DB, i.e. + // if `previous_snapshot_slot >= state_upper_limit`. + // - The split state itself will be stored as a diff or snapshot in the new grid. We choose + // not to support a split state that requires block replay, because computing its previous + // state root from the DAG is not straight-forward. + let dummy_start_slot = Slot::new(0); + let closest_layer_points = db + .hierarchy + .closest_layer_points(split.slot, dummy_start_slot); + + let previous_snapshot_slot = + closest_layer_points + .iter() + .copied() + .min() + .ok_or(Error::MigrationError( + "closest_layer_points must not be empty".to_string(), + ))?; + + if previous_snapshot_slot >= anchor_info.state_upper_limit + && db + .hierarchy + .storage_strategy(split.slot, dummy_start_slot) + .is_ok_and(|strategy| !strategy.is_replay_from()) + { + info!( + %previous_snapshot_slot, + split_slot = %split.slot, + "Aligning hot diff grid to freezer" + ); + + // Set anchor slot to 0 in case it was set to something else by a previous checkpoint + // sync. + let mut new_anchor_info = anchor_info.clone(); + new_anchor_info.anchor_slot = Slot::new(0); + + // Update the anchor on disk atomically if migration is successful + migrate_ops.push(db.compare_and_set_anchor_info(anchor_info, new_anchor_info)?); + + // Copy each of the freezer layers to the hot DB in slot ascending order. + for layer_slot in closest_layer_points.into_iter().rev() { + // Do not try to load the split state itself from the freezer, it won't be there. + // It will be migrated in the main loop below. + if layer_slot == split.slot { + continue; + } + + let mut freezer_state = db.load_cold_state_by_slot(layer_slot)?; + + let state_root = freezer_state.canonical_root()?; + + let mut state_ops = vec![]; + db.store_hot_state(&state_root, &freezer_state, &mut state_ops)?; + db.hot_db.do_atomically(state_ops)?; + } + } else { + // Otherwise for non-archive nodes, set the anchor slot for the hot grid to the current + // split slot (the oldest slot available). + let mut new_anchor_info = anchor_info.clone(); + new_anchor_info.anchor_slot = hot_hdiff_start_slot; + + // Update the anchor in disk atomically if migration is successful + migrate_ops.push(db.compare_and_set_anchor_info(anchor_info, new_anchor_info)?); + } + } + + let state_summaries_dag = new_dag::(&db)?; + + // We compute the state summaries DAG outside of a DB migration. Therefore if the DB is properly + // prunned, it should have a single root equal to the split. + let state_summaries_dag_roots = state_summaries_dag.tree_roots(); + if state_summaries_dag_roots.len() == 1 { + let (root_summary_state_root, root_summary) = + state_summaries_dag_roots.first().expect("len == 1"); + if *root_summary_state_root != split.state_root { + warn!( + ?root_summary_state_root, + ?root_summary, + ?split, + "State summaries DAG root is not the split" + ); + } + } else { + warn!( + location = "migration", + state_summaries_dag_roots = ?state_summaries_dag_roots, + "State summaries DAG found more than one root" + ); + } + + // Sort summaries by slot so we have their ancestor diffs already stored when we store them. + // If the summaries are sorted topologically we can insert them into the DB like if they were a + // new state, re-using existing code. As states are likely to be sequential the diff cache + // should kick in making the migration more efficient. If we just iterate the column of + // summaries we may get distance state of each iteration. + let summaries_by_slot = state_summaries_dag.summaries_by_slot_ascending(); + debug!( + summaries_count = state_summaries_dag.summaries_count(), + slots_count = summaries_by_slot.len(), + min_slot = ?summaries_by_slot.first_key_value().map(|(slot, _)| slot), + max_slot = ?summaries_by_slot.last_key_value().map(|(slot, _)| slot), + ?state_summaries_dag_roots, + %hot_hdiff_start_slot, + split_state_root = ?split.state_root, + "Starting hot states migration" + ); + + // Upgrade all hot DB state summaries to the new type: + // - Set all summaries of boundary states to `Snapshot` type + // - Set all others to `Replay` pointing to `epoch_boundary_state_root` + + let mut diffs_written = 0; + let mut summaries_written = 0; + let mut last_log_time = Instant::now(); + + for (slot, old_hot_state_summaries) in summaries_by_slot { + for (state_root, old_summary) in old_hot_state_summaries { + if slot < hot_hdiff_start_slot { + // To reach here, there must be some pruning issue with the DB where we still have + // hot states below the split slot. This states can't be migrated as we can't compute + // a storage strategy for them. After this if else block, the summary and state are + // scheduled for deletion. + debug!( + %slot, + ?state_root, + "Ignoring state summary prior to split slot" + ); + } else { + // 1. Store snapshot or diff at this slot (if required). + let storage_strategy = db.hot_storage_strategy(slot)?; + debug!( + %slot, + ?state_root, + ?storage_strategy, + "Migrating state summary" + ); + + match storage_strategy { + StorageStrategy::DiffFrom(_) | StorageStrategy::Snapshot => { + // Load the state and re-store it as a snapshot or diff. + let state = get_state_v22::(&db, &state_root, &db.spec)? + .ok_or(Error::MissingState(state_root))?; + + // Store immediately so that future diffs can load and diff from it. + let mut ops = vec![]; + // We must commit the hot state summary immediately, otherwise we can't diff + // against it and future writes will fail. That's why we write the new hot + // summaries in a different column to have both new and old data present at + // once. Otherwise if the process crashes during the migration the database will + // be broken. + db.store_hot_state_summary(&state_root, &state, &mut ops)?; + db.store_hot_state_diffs(&state_root, &state, &mut ops)?; + db.hot_db.do_atomically(ops)?; + diffs_written += 1; + } + StorageStrategy::ReplayFrom(diff_base_slot) => { + // Optimization: instead of having to load the state of each summary we load x32 + // less states by manually computing the HotStateSummary roots using the + // computed state dag. + // + // No need to store diffs for states that will be reconstructed by replaying + // blocks. + // + // 2. Convert the summary to the new format. + if state_root == split.state_root { + return Err(Error::MigrationError( + "unreachable: split state should be stored as a snapshot or diff" + .to_string(), + )); + } + let previous_state_root = state_summaries_dag + .previous_state_root(state_root) + .map_err(|e| { + Error::MigrationError(format!( + "error computing previous_state_root {e:?}" + )) + })?; + + let diff_base_state = OptionalDiffBaseState::new( + diff_base_slot, + state_summaries_dag + .ancestor_state_root_at_slot(state_root, diff_base_slot) + .map_err(|e| { + Error::MigrationError(format!( + "error computing ancestor_state_root_at_slot \ + ({state_root:?}, {diff_base_slot}): {e:?}" + )) + })?, + ); + + let new_summary = HotStateSummary { + slot, + latest_block_root: old_summary.latest_block_root, + latest_block_slot: old_summary.latest_block_slot, + previous_state_root, + diff_base_state, + }; + let op = new_summary.as_kv_store_op(state_root); + // It's not necessary to immediately commit the summaries of states that are + // ReplayFrom. However we do so for simplicity. + db.hot_db.do_atomically(vec![op])?; + } + } + } + + // 3. Stage old data for deletion. + if slot % T::EthSpec::slots_per_epoch() == 0 { + migrate_ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + )); + } + + // Delete previous summaries + migrate_ops.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateSummary, + state_root.as_slice().to_vec(), + )); + + summaries_written += 1; + if last_log_time.elapsed() > Duration::from_secs(5) { + last_log_time = Instant::now(); + info!( + diffs_written, + summaries_written, + summaries_count = state_summaries_dag.summaries_count(), + "Hot states migration in progress" + ); + } + } + } + + info!( + diffs_written, + summaries_written, + summaries_count = state_summaries_dag.summaries_count(), + "Hot states migration complete" + ); + + Ok(migrate_ops) +} + +pub fn downgrade_from_v24( + db: Arc>, +) -> Result, Error> { + let state_summaries = db + .load_hot_state_summaries()? + .into_iter() + .map(|(state_root, summary)| (state_root, summary.into())) + .collect::>(); + + info!( + summaries_count = state_summaries.len(), + "DB downgrade of v24 state summaries started" + ); + + let state_summaries_dag = StateSummariesDAG::new(state_summaries) + .map_err(|e| Error::MigrationError(format!("Error on new StateSumariesDAG {e:?}")))?; + + let mut migrate_ops = vec![]; + let mut states_written = 0; + let mut summaries_written = 0; + let mut summaries_skipped = 0; + let mut last_log_time = Instant::now(); + + // Rebuild the PruningCheckpoint from the split. + let split = db.get_split_info(); + let pruning_checkpoint = PruningCheckpoint { + checkpoint: Checkpoint { + epoch: split.slot.epoch(T::EthSpec::slots_per_epoch()), + root: split.block_root, + }, + }; + migrate_ops.push(pruning_checkpoint.as_kv_store_op(PRUNING_CHECKPOINT_KEY)); + + // Convert state summaries back to the old format. + for (state_root, summary) in state_summaries_dag + .summaries_by_slot_ascending() + .into_iter() + .flat_map(|(_, summaries)| summaries) + { + // No need to migrate any states prior to the split. The v22 schema does not need them, and + // they would generate warnings about a disjoint DAG when re-upgrading to V24. + if summary.slot < split.slot { + debug!( + slot = %summary.slot, + ?state_root, + "Skipping migration of pre-split state" + ); + summaries_skipped += 1; + continue; + } + + // If boundary state: persist. + // Do not cache these states as they are unlikely to be relevant later. + let update_cache = false; + if summary.slot % T::EthSpec::slots_per_epoch() == 0 { + let (state, _) = db + .load_hot_state(&state_root, update_cache)? + .ok_or(Error::MissingState(state_root))?; + + // Immediately commit the state, so we don't OOM. It's stored in a different + // column so if the migration crashes we'll just store extra harmless junk in the DB. + let mut state_write_ops = vec![]; + store_full_state_v22(&state_root, &state, &mut state_write_ops)?; + db.hot_db.do_atomically(state_write_ops)?; + states_written += 1; + } + + // Persist old summary. + let epoch_boundary_state_slot = summary.slot - summary.slot % T::EthSpec::slots_per_epoch(); + let old_summary = HotStateSummaryV22 { + slot: summary.slot, + latest_block_root: summary.latest_block_root, + epoch_boundary_state_root: state_summaries_dag + .ancestor_state_root_at_slot(state_root, epoch_boundary_state_slot) + .map_err(|e| { + Error::MigrationError(format!( + "error computing ancestor_state_root_at_slot({state_root:?}, {epoch_boundary_state_slot}) {e:?}" + )) + })?, + }; + migrate_ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconStateSummary, + state_root.as_slice().to_vec(), + old_summary.as_ssz_bytes(), + )); + summaries_written += 1; + + if last_log_time.elapsed() > Duration::from_secs(5) { + last_log_time = Instant::now(); + info!( + states_written, + summaries_written, + summaries_count = state_summaries_dag.summaries_count(), + "DB downgrade of v24 state summaries in progress" + ); + } + } + + // Delete all V24 schema data. We do this outside the loop over summaries to ensure we cover + // every piece of data and to simplify logic around skipping certain summaries that do not get + // migrated. + for db_column in [ + DBColumn::BeaconStateHotSummary, + DBColumn::BeaconStateHotDiff, + DBColumn::BeaconStateHotSnapshot, + ] { + for key in db.hot_db.iter_column_keys::(db_column) { + let state_root = key?; + migrate_ops.push(KeyValueStoreOp::DeleteKey( + db_column, + state_root.as_slice().to_vec(), + )); + } + } + + info!( + states_written, + summaries_written, + summaries_skipped, + summaries_count = state_summaries_dag.summaries_count(), + "DB downgrade of v24 state summaries completed" + ); + + Ok(migrate_ops) +} + +fn new_dag( + db: &HotColdDB, +) -> Result { + // Collect all sumaries for unfinalized states + let state_summaries_v22 = db + .hot_db + // Collect summaries from the legacy V22 column BeaconStateSummary + .iter_column::(DBColumn::BeaconStateSummary) + .map(|res| { + let (key, value) = res?; + let state_root: Hash256 = key; + let summary = HotStateSummaryV22::from_ssz_bytes(&value)?; + let block_root = summary.latest_block_root; + // Read blocks to get the block slot and parent root. In Holesky forced finalization it + // took 5100 ms to read 15072 state summaries, so it's not really necessary to + // de-duplicate block reads. + let block = db + .get_blinded_block(&block_root)? + .ok_or(Error::MissingBlock(block_root))?; + + Ok(( + state_root, + DAGStateSummaryV22 { + slot: summary.slot, + latest_block_root: summary.latest_block_root, + block_slot: block.slot(), + block_parent_root: block.parent_root(), + }, + )) + }) + .collect::, Error>>()?; + + StateSummariesDAG::new_from_v22(state_summaries_v22) + .map_err(|e| Error::MigrationError(format!("error computing states summaries dag {e:?}"))) +} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v25.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v25.rs new file mode 100644 index 0000000000..44e8894d6f --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v25.rs @@ -0,0 +1,20 @@ +use store::{DBColumn, Error, KeyValueStoreOp}; +use tracing::info; +use types::Hash256; + +pub const ETH1_CACHE_DB_KEY: Hash256 = Hash256::ZERO; + +/// Delete the on-disk eth1 data. +pub fn upgrade_to_v25() -> Result, Error> { + info!("Deleting eth1 data from disk for v25 DB upgrade"); + Ok(vec![KeyValueStoreOp::DeleteKey( + DBColumn::Eth1Cache, + ETH1_CACHE_DB_KEY.as_slice().to_vec(), + )]) +} + +/// No-op: we don't need to recreate on-disk eth1 data, as previous versions gracefully handle +/// data missing from disk. +pub fn downgrade_from_v25() -> Result, Error> { + Ok(vec![]) +} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs new file mode 100644 index 0000000000..38714ea060 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs @@ -0,0 +1,91 @@ +use crate::BeaconChainTypes; +use crate::custody_context::CustodyContextSsz; +use crate::persisted_custody::{CUSTODY_DB_KEY, PersistedCustody}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::sync::Arc; +use store::{DBColumn, Error, HotColdDB, KeyValueStoreOp, StoreItem}; +use tracing::info; + +#[derive(Debug, Encode, Decode, Clone)] +pub(crate) struct CustodyContextSszV24 { + pub(crate) validator_custody_at_head: u64, + pub(crate) persisted_is_supernode: bool, +} + +pub(crate) struct PersistedCustodyV24(CustodyContextSszV24); + +impl StoreItem for PersistedCustodyV24 { + fn db_column() -> DBColumn { + DBColumn::CustodyContext + } + + fn as_store_bytes(&self) -> Vec { + self.0.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + let custody_context = CustodyContextSszV24::from_ssz_bytes(bytes)?; + Ok(PersistedCustodyV24(custody_context)) + } +} + +/// Upgrade the `CustodyContext` entry to v26. +pub fn upgrade_to_v26( + db: Arc>, +) -> Result, Error> { + let ops = if db.spec.is_peer_das_scheduled() { + match db.get_item::(&CUSTODY_DB_KEY) { + Ok(Some(PersistedCustodyV24(ssz_v24))) => { + info!("Migrating `CustodyContext` to v26 schema"); + let custody_context_v2 = CustodyContextSsz { + validator_custody_at_head: ssz_v24.validator_custody_at_head, + persisted_is_supernode: ssz_v24.persisted_is_supernode, + epoch_validator_custody_requirements: vec![], + }; + vec![KeyValueStoreOp::PutKeyValue( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + PersistedCustody(custody_context_v2).as_store_bytes(), + )] + } + _ => { + vec![] + } + } + } else { + // Delete it from db if PeerDAS hasn't been scheduled + vec![KeyValueStoreOp::DeleteKey( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + )] + }; + + Ok(ops) +} + +pub fn downgrade_from_v26( + db: Arc>, +) -> Result, Error> { + let res = db.get_item::(&CUSTODY_DB_KEY); + let ops = match res { + Ok(Some(PersistedCustody(ssz_v26))) => { + info!("Migrating `CustodyContext` back from v26 schema"); + let custody_context_v24 = CustodyContextSszV24 { + validator_custody_at_head: ssz_v26.validator_custody_at_head, + persisted_is_supernode: ssz_v26.persisted_is_supernode, + }; + vec![KeyValueStoreOp::PutKeyValue( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + PersistedCustodyV24(custody_context_v24).as_store_bytes(), + )] + } + _ => { + // no op if it's not on the db, as previous versions gracefully handle data missing from disk. + vec![] + } + }; + + Ok(ops) +} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v27.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v27.rs new file mode 100644 index 0000000000..fbe865ee27 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v27.rs @@ -0,0 +1,26 @@ +use crate::BeaconChainTypes; +use std::sync::Arc; +use store::{Error, HotColdDB, metadata::SchemaVersion}; + +/// Add `DataColumnCustodyInfo` entry to v27. +pub fn upgrade_to_v27( + db: Arc>, +) -> Result<(), Error> { + if db.spec.is_peer_das_scheduled() { + db.put_data_column_custody_info(None)?; + db.store_schema_version_atomically(SchemaVersion(27), vec![])?; + } + + Ok(()) +} + +pub fn downgrade_from_v27( + db: Arc>, +) -> Result<(), Error> { + if db.spec.is_peer_das_scheduled() { + return Err(Error::MigrationError( + "Cannot downgrade from v27 if peerDAS is scheduled".to_string(), + )); + } + Ok(()) +} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs new file mode 100644 index 0000000000..5885eaabc0 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs @@ -0,0 +1,152 @@ +use crate::{ + BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, PersistedForkChoiceStoreV17, + beacon_chain::FORK_CHOICE_DB_KEY, + persisted_fork_choice::{PersistedForkChoiceV17, PersistedForkChoiceV28}, + summaries_dag::{DAGStateSummary, StateSummariesDAG}, +}; +use fork_choice::{ForkChoice, ForkChoiceStore, ResetPayloadStatuses}; +use std::sync::Arc; +use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; +use tracing::{info, warn}; +use types::{EthSpec, Hash256}; + +/// Upgrade `PersistedForkChoice` from V17 to V28. +pub fn upgrade_to_v28( + db: Arc>, +) -> Result, Error> { + let Some(persisted_fork_choice_v17) = + db.get_item::(&FORK_CHOICE_DB_KEY)? + else { + warn!("No fork choice found to upgrade to v28"); + return Ok(vec![]); + }; + + // Load state DAG in order to compute justified checkpoint roots. + let state_summaries_dag = { + let state_summaries = db + .load_hot_state_summaries()? + .into_iter() + .map(|(state_root, summary)| (state_root, summary.into())) + .collect::>(); + + StateSummariesDAG::new(state_summaries).map_err(|e| { + Error::MigrationError(format!("Error loading state summaries DAG: {e:?}")) + })? + }; + + // Determine the justified state roots. + let justified_checkpoint = persisted_fork_choice_v17 + .fork_choice_store_v17 + .justified_checkpoint; + let justified_block_root = justified_checkpoint.root; + let justified_slot = justified_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + let justified_state_root = state_summaries_dag + .state_root_at_slot(justified_block_root, justified_slot) + .ok_or_else(|| { + Error::MigrationError(format!( + "Missing state root for justified slot {justified_slot} with latest_block_root \ + {justified_block_root:?}" + )) + })?; + + let unrealized_justified_checkpoint = persisted_fork_choice_v17 + .fork_choice_store_v17 + .unrealized_justified_checkpoint; + let unrealized_justified_block_root = unrealized_justified_checkpoint.root; + let unrealized_justified_slot = unrealized_justified_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + let unrealized_justified_state_root = state_summaries_dag + .state_root_at_slot(unrealized_justified_block_root, unrealized_justified_slot) + .ok_or_else(|| { + Error::MigrationError(format!( + "Missing state root for unrealized justified slot {unrealized_justified_slot} \ + with latest_block_root {unrealized_justified_block_root:?}" + )) + })?; + + let fc_store = BeaconForkChoiceStore::from_persisted_v17( + persisted_fork_choice_v17.fork_choice_store_v17, + justified_state_root, + unrealized_justified_state_root, + db.clone(), + ) + .map_err(|e| { + Error::MigrationError(format!( + "Error loading fork choice store from persisted: {e:?}" + )) + })?; + + info!( + ?justified_state_root, + %justified_slot, + "Added justified state root to fork choice" + ); + + // Construct top-level ForkChoice struct using the patched fork choice store, and the converted + // proto array. + let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload; + let fork_choice = ForkChoice::from_persisted( + persisted_fork_choice_v17.fork_choice_v17.try_into()?, + reset_payload_statuses, + fc_store, + db.get_chain_spec(), + ) + .map_err(|e| Error::MigrationError(format!("Unable to build ForkChoice: {e:?}")))?; + + let ops = vec![BeaconChain::::persist_fork_choice_in_batch_standalone( + &fork_choice, + db.get_config(), + )?]; + + info!("Upgraded fork choice for DB schema v28"); + + Ok(ops) +} + +pub fn downgrade_from_v28( + db: Arc>, +) -> Result, Error> { + let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload; + let Some(fork_choice) = + BeaconChain::::load_fork_choice(db.clone(), reset_payload_statuses, db.get_chain_spec()) + .map_err(|e| Error::MigrationError(format!("Unable to load fork choice: {e:?}")))? + else { + warn!("No fork choice to downgrade"); + return Ok(vec![]); + }; + + // Recreate V28 persisted fork choice, then convert each field back to its V17 version. + let persisted_fork_choice = PersistedForkChoiceV28 { + fork_choice: fork_choice.to_persisted(), + fork_choice_store: fork_choice.fc_store().to_persisted(), + }; + + let justified_balances = fork_choice.fc_store().justified_balances(); + + // 1. Create `proto_array::PersistedForkChoiceV17`. + let fork_choice_v17: fork_choice::PersistedForkChoiceV17 = ( + persisted_fork_choice.fork_choice, + justified_balances.clone(), + ) + .into(); + + let fork_choice_store_v17: PersistedForkChoiceStoreV17 = ( + persisted_fork_choice.fork_choice_store, + justified_balances.clone(), + ) + .into(); + + let persisted_fork_choice_v17 = PersistedForkChoiceV17 { + fork_choice_v17, + fork_choice_store_v17, + }; + + let ops = vec![persisted_fork_choice_v17.as_kv_store_op(FORK_CHOICE_DB_KEY)]; + + info!("Downgraded fork choice for DB schema v28"); + + Ok(ops) +} diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 1aa23c28fc..618d459754 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -2,14 +2,14 @@ use std::collections::HashMap; use std::sync::Arc; use itertools::Itertools; -use oneshot_broadcast::{oneshot, Receiver, Sender}; +use oneshot_broadcast::{Receiver, Sender, oneshot}; use tracing::debug; use types::{ - beacon_state::CommitteeCache, AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, - RelativeEpoch, + AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, RelativeEpoch, + beacon_state::CommitteeCache, }; -use crate::{metrics, BeaconChainError}; +use crate::{BeaconChainError, metrics}; /// The size of the cache that stores committee caches for quicker verification. /// @@ -290,6 +290,7 @@ impl BlockShufflingIds { #[cfg(not(debug_assertions))] #[cfg(test)] mod test { + use fixed_bytes::FixedBytesExtended; use types::*; use crate::test_utils::EphemeralHarnessType; diff --git a/beacon_node/beacon_chain/src/single_attestation.rs b/beacon_node/beacon_chain/src/single_attestation.rs index fa4f98bb07..955eb98e92 100644 --- a/beacon_node/beacon_chain/src/single_attestation.rs +++ b/beacon_node/beacon_chain/src/single_attestation.rs @@ -1,9 +1,13 @@ use crate::attestation_verification::Error; -use types::{Attestation, AttestationElectra, BitList, BitVector, EthSpec, SingleAttestation}; +use ssz_types::{BitList, BitVector}; +use types::{ + Attestation, AttestationBase, AttestationElectra, EthSpec, ForkName, SingleAttestation, +}; pub fn single_attestation_to_attestation( single_attestation: &SingleAttestation, committee: &[usize], + fork_name: ForkName, ) -> Result, Error> { let attester_index = single_attestation.attester_index; let committee_index = single_attestation.committee_index; @@ -24,23 +28,33 @@ pub fn single_attestation_to_attestation( slot, })?; - let mut committee_bits: BitVector = BitVector::default(); - committee_bits - .set(committee_index as usize, true) - .map_err(|e| Error::Invalid(e.into()))?; + if fork_name.electra_enabled() { + let mut committee_bits: BitVector = BitVector::default(); + committee_bits + .set(committee_index as usize, true) + .map_err(|e| Error::Invalid(e.into()))?; - let mut aggregation_bits = - BitList::with_capacity(committee.len()).map_err(|e| Error::Invalid(e.into()))?; - aggregation_bits - .set(aggregation_bit, true) - .map_err(|e| Error::Invalid(e.into()))?; - - // TODO(electra): consider eventually allowing conversion to non-Electra attestations as well - // to maintain invertability (`Attestation` -> `SingleAttestation` -> `Attestation`). - Ok(Attestation::Electra(AttestationElectra { - aggregation_bits, - committee_bits, - data: single_attestation.data.clone(), - signature: single_attestation.signature.clone(), - })) + let mut aggregation_bits = + BitList::with_capacity(committee.len()).map_err(|e| Error::Invalid(e.into()))?; + aggregation_bits + .set(aggregation_bit, true) + .map_err(|e| Error::Invalid(e.into()))?; + Ok(Attestation::Electra(AttestationElectra { + aggregation_bits, + committee_bits, + data: single_attestation.data.clone(), + signature: single_attestation.signature.clone(), + })) + } else { + let mut aggregation_bits = + BitList::with_capacity(committee.len()).map_err(|e| Error::Invalid(e.into()))?; + aggregation_bits + .set(aggregation_bit, true) + .map_err(|e| Error::Invalid(e.into()))?; + Ok(Attestation::Base(AttestationBase { + aggregation_bits, + data: single_attestation.data.clone(), + signature: single_attestation.signature.clone(), + })) + } } diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index f206405f67..a070dc350b 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -15,17 +15,17 @@ //! 2. There's a possibility that the head block is never built upon, causing wasted CPU cycles. use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::{ - chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, BeaconChain, BeaconChainError, BeaconChainTypes, + BeaconChain, BeaconChainError, BeaconChainTypes, chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, }; use slot_clock::SlotClock; use state_processing::per_slot_processing; use std::sync::{ - atomic::{AtomicBool, Ordering}, Arc, + atomic::{AtomicBool, Ordering}, }; use task_executor::TaskExecutor; -use tokio::time::{sleep, sleep_until, Instant}; -use tracing::{debug, error, warn}; +use tokio::time::{Instant, sleep, sleep_until}; +use tracing::{Instrument, debug, debug_span, error, instrument, warn}; use types::{AttestationShufflingId, BeaconStateError, EthSpec, Hash256, RelativeEpoch, Slot}; /// If the head slot is more than `MAX_ADVANCE_DISTANCE` from the current slot, then don't perform @@ -33,7 +33,7 @@ use types::{AttestationShufflingId, BeaconStateError, EthSpec, Hash256, Relative /// /// This avoids doing unnecessary work whilst the node is syncing or has perhaps been put to sleep /// for some period of time. -const MAX_ADVANCE_DISTANCE: u64 = 4; +const MAX_ADVANCE_DISTANCE: u64 = 256; /// Similarly for fork choice: avoid the fork choice lookahead during sync. /// @@ -49,17 +49,7 @@ enum Error { HeadMissingFromSnapshotCache(#[allow(dead_code)] Hash256), BeaconState(#[allow(dead_code)] BeaconStateError), Store(#[allow(dead_code)] store::Error), - MaxDistanceExceeded { - current_slot: Slot, - head_slot: Slot, - }, - StateAlreadyAdvanced { - block_root: Hash256, - }, - BadStateSlot { - _state_slot: Slot, - _block_slot: Slot, - }, + MaxDistanceExceeded { current_slot: Slot, head_slot: Slot }, } impl From for Error { @@ -180,9 +170,6 @@ async fn state_advance_timer( error = ?e, "Failed to advance head state" ), - Err(Error::StateAlreadyAdvanced { block_root }) => { - debug!(?block_root, "State already advanced on slot") - } Err(Error::MaxDistanceExceeded { current_slot, head_slot, @@ -241,19 +228,20 @@ async fn state_advance_timer( beacon_chain.task_executor.clone().spawn_blocking( move || { // Signal block proposal for the next slot (if it happens to be waiting). - if let Some(tx) = &beacon_chain.fork_choice_signal_tx { - if let Err(e) = tx.notify_fork_choice_complete(next_slot) { - warn!( - error = ?e, - slot = %next_slot, - "Error signalling fork choice waiter" - ); - } + if let Some(tx) = &beacon_chain.fork_choice_signal_tx + && let Err(e) = tx.notify_fork_choice_complete(next_slot) + { + warn!( + error = ?e, + slot = %next_slot, + "Error signalling fork choice waiter" + ); } }, "fork_choice_advance_signal_tx", ); - }, + } + .instrument(debug_span!("fork_choice_advance")), "fork_choice_advance", ); } @@ -264,6 +252,7 @@ async fn state_advance_timer( /// slot then placed in the `state_cache` to be used for block verification. /// /// See the module-level documentation for rationale. +#[instrument(skip_all)] fn advance_head(beacon_chain: &Arc>) -> Result<(), Error> { let current_slot = beacon_chain.slot()?; @@ -293,25 +282,6 @@ fn advance_head(beacon_chain: &Arc>) -> Resu .get_advanced_hot_state(head_block_root, current_slot, head_block_state_root)? .ok_or(Error::HeadMissingFromSnapshotCache(head_block_root))?; - // Protect against advancing a state more than a single slot. - // - // Advancing more than one slot without storing the intermediate state would corrupt the - // database. Future works might store intermediate states inside this function. - match state.slot().cmp(&state.latest_block_header().slot) { - std::cmp::Ordering::Equal => (), - std::cmp::Ordering::Greater => { - return Err(Error::StateAlreadyAdvanced { - block_root: head_block_root, - }); - } - std::cmp::Ordering::Less => { - return Err(Error::BadStateSlot { - _block_slot: state.latest_block_header().slot, - _state_slot: state.slot(), - }); - } - } - let initial_slot = state.slot(); let initial_epoch = state.current_epoch(); @@ -363,25 +333,60 @@ fn advance_head(beacon_chain: &Arc>) -> Resu .build_committee_cache(RelativeEpoch::Next, &beacon_chain.spec) .map_err(BeaconChainError::from)?; - // If the `pre_state` is in a later epoch than `state`, pre-emptively add the proposer shuffling - // for the state's current epoch and the committee cache for the state's next epoch. + // The state root is required to prime the proposer cache AND for writing it to disk. + let advanced_state_root = state.update_tree_hash_cache()?; + + // If the `pre_state` is in a later epoch than `state`, pre-emptively update the proposer + // shuffling and attester shuffling caches. if initial_epoch < state.current_epoch() { - // Update the proposer cache. - // - // We supply the `head_block_root` as the decision block since the prior `if` statement guarantees - // the head root is the latest block from the prior epoch. - beacon_chain - .beacon_proposer_cache - .lock() - .insert( - state.current_epoch(), + // Include the proposer shuffling from the current epoch, which is likely to be useful + // pre-Fulu, and probably redundant post-Fulu (it should already have been in the cache). + let current_epoch_decision_root = state.proposer_shuffling_decision_root_at_epoch( + state.current_epoch(), + head_block_root, + &beacon_chain.spec, + )?; + beacon_chain.with_proposer_cache( + current_epoch_decision_root, + state.current_epoch(), + |_| Ok(()), + || { + debug!( + shuffling_decision_root = ?current_epoch_decision_root, + epoch = %state.current_epoch(), + "Computing current epoch proposer shuffling in state advance" + ); + Ok::<_, Error>((advanced_state_root, state.clone())) + }, + )?; + + // For epochs *greater than* the Fulu fork epoch, we have also determined the proposer + // shuffling for the next epoch. + let next_epoch = state.next_epoch()?; + let next_epoch_decision_slot = beacon_chain + .spec + .proposer_shuffling_decision_slot::(next_epoch); + + if state.slot() > next_epoch_decision_slot { + let next_epoch_decision_root = state.proposer_shuffling_decision_root_at_epoch( + next_epoch, head_block_root, - state - .get_beacon_proposer_indices(&beacon_chain.spec) - .map_err(BeaconChainError::from)?, - state.fork(), - ) - .map_err(BeaconChainError::from)?; + &beacon_chain.spec, + )?; + beacon_chain.with_proposer_cache( + next_epoch_decision_root, + next_epoch, + |_| Ok(()), + || { + debug!( + shuffling_decision_root = ?next_epoch_decision_root, + epoch = %next_epoch, + "Computing next epoch proposer shuffling in state advance" + ); + Ok::<_, Error>((advanced_state_root, state.clone())) + }, + )?; + } // Update the attester cache. let shuffling_id = @@ -436,7 +441,6 @@ fn advance_head(beacon_chain: &Arc>) -> Resu // even if we race with the deletion of this state by the finalization pruning code, the worst // case is we end up with a finalized state stored, that will get pruned the next time pruning // runs. - let advanced_state_root = state.update_tree_hash_cache()?; beacon_chain.store.put_state(&advanced_state_root, &state)?; debug!( diff --git a/beacon_node/beacon_chain/src/summaries_dag.rs b/beacon_node/beacon_chain/src/summaries_dag.rs index 8dff2ac7be..4ddcdaab5a 100644 --- a/beacon_node/beacon_chain/src/summaries_dag.rs +++ b/beacon_node/beacon_chain/src/summaries_dag.rs @@ -1,8 +1,9 @@ use itertools::Itertools; use std::{ cmp::Ordering, - collections::{btree_map::Entry, BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, btree_map::Entry}, }; +use store::HotStateSummary; use types::{Hash256, Slot}; #[derive(Debug, Clone, Copy)] @@ -57,6 +58,12 @@ pub enum Error { root_state_root: Hash256, root_state_slot: Slot, }, + CircularAncestorChain { + state_root: Hash256, + previous_state_root: Hash256, + slot: Slot, + last_slot: Slot, + }, } impl StateSummariesDAG { @@ -81,7 +88,7 @@ impl StateSummariesDAG { block_root: summary.latest_block_root, existing_state_summary: (summary.slot, state_root).into(), new_state_summary: (*existing.key(), existing.get().0), - }) + }); } } @@ -129,7 +136,7 @@ impl StateSummariesDAG { block_root: summary.latest_block_root, existing_state_summary: (summary.slot, *state_root).into(), new_state_summary: (*existing.key(), *existing.get().0), - }) + }); } } } @@ -281,7 +288,7 @@ impl StateSummariesDAG { ancestor_slot, state_root, state_slot: summary.slot, - }) + }); } Ordering::Equal => { return Ok(state_root); @@ -311,10 +318,24 @@ impl StateSummariesDAG { } let mut ancestors = vec![]; + let mut last_slot = None; loop { if let Some(summary) = self.state_summaries_by_state_root.get(&state_root) { + // Detect cycles, including the case where `previous_state_root == state_root`. + if let Some(last_slot) = last_slot + && summary.slot >= last_slot + { + return Err(Error::CircularAncestorChain { + state_root, + previous_state_root: summary.previous_state_root, + slot: summary.slot, + last_slot, + }); + } + ancestors.push((state_root, summary.slot)); - state_root = summary.previous_state_root + last_slot = Some(summary.slot); + state_root = summary.previous_state_root; } else { return Ok(ancestors); } @@ -334,6 +355,29 @@ impl StateSummariesDAG { } Ok(descendants) } + + /// Returns the root of the state at `slot` with `latest_block_root`, if it exists. + /// + /// The `slot` must be the slot of the `latest_block_root` or a skipped slot following it. This + /// function will not return the `state_root` of a state with a different `latest_block_root` + /// even if it lies on the same chain. + pub fn state_root_at_slot(&self, latest_block_root: Hash256, slot: Slot) -> Option { + self.state_summaries_by_block_root + .get(&latest_block_root)? + .get(&slot) + .map(|(state_root, _)| *state_root) + } +} + +impl From for DAGStateSummary { + fn from(value: HotStateSummary) -> Self { + Self { + slot: value.slot, + latest_block_root: value.latest_block_root, + latest_block_slot: value.latest_block_slot, + previous_state_root: value.previous_state_root, + } + } } #[cfg(test)] diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index 768c971f94..e74e284e58 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -28,10 +28,11 @@ use crate::observed_attesters::SlotSubcommitteeIndex; use crate::{ - metrics, observed_aggregates::ObserveOutcome, BeaconChain, BeaconChainError, BeaconChainTypes, + BeaconChain, BeaconChainError, BeaconChainTypes, metrics, observed_aggregates::ObserveOutcome, }; -use bls::{verify_signature_sets, PublicKeyBytes}; -use derivative::Derivative; +use bls::AggregateSignature; +use bls::{PublicKeyBytes, verify_signature_sets}; +use educe::Educe; use safe_arith::ArithError; use slot_clock::SlotClock; use ssz_derive::{Decode, Encode}; @@ -46,14 +47,14 @@ use std::collections::HashMap; use strum::AsRefStr; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use types::ChainSpec; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::slot_data::SlotData; -use types::sync_committee::Error as SyncCommitteeError; -use types::ChainSpec; +use types::sync_committee::SyncCommitteeError; use types::{ - sync_committee_contribution::Error as ContributionError, AggregateSignature, BeaconStateError, - EthSpec, Hash256, SignedContributionAndProof, Slot, SyncCommitteeContribution, - SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + BeaconStateError, EthSpec, Hash256, SignedContributionAndProof, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + sync_committee_contribution::Error as ContributionError, }; /// Returned when a sync committee contribution was not successfully verified. It might not have been verified for @@ -261,8 +262,8 @@ impl From for Error { } /// Wraps a `SignedContributionAndProof` that has been verified for propagation on the gossip network.\ -#[derive(Derivative)] -#[derivative(Clone(bound = "T: BeaconChainTypes"))] +#[derive(Educe)] +#[educe(Clone(bound(T: BeaconChainTypes)))] pub struct VerifiedSyncContribution { signed_aggregate: SignedContributionAndProof, participant_pubkeys: Vec, @@ -505,15 +506,14 @@ impl VerifiedSyncCommitteeMessage { validator_index as usize, ) .map_err(BeaconChainError::from)? + && !should_override_prev(&prev_root, &new_root) { - if !should_override_prev(&prev_root, &new_root) { - return Err(Error::PriorSyncCommitteeMessageKnown { - validator_index, - slot: sync_message.slot, - prev_root, - new_root, - }); - } + return Err(Error::PriorSyncCommitteeMessageKnown { + validator_index, + slot: sync_message.slot, + prev_root, + new_root, + }); } // The aggregate signature of the sync committee message is valid. @@ -629,7 +629,7 @@ pub fn verify_signed_aggregate_signatures( (signed_aggregate.message.contribution.slot + 1).epoch(T::EthSpec::slots_per_epoch()); let fork = chain.spec.fork_at_epoch(next_slot_epoch); - let signature_sets = vec![ + let signature_sets = [ signed_sync_aggregate_selection_proof_signature_set( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), signed_aggregate, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4bea055416..fe268d4161 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,50 +1,57 @@ use crate::blob_verification::GossipVerifiedBlob; use crate::block_verification_types::{AsBlock, RpcBlock}; +use crate::custody_context::NodeCustodyType; use crate::data_column_verification::CustodyDataColumn; +use crate::graffiti_calculator::GraffitiSettings; use crate::kzg_utils::build_data_column_sidecars; use crate::observed_operations::ObservationOutcome; pub use crate::persisted_beacon_chain::PersistedBeaconChain; +use crate::{BeaconBlockResponseWrapper, get_block_root}; +use crate::{ + BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler, + StateSkipConfig, + builder::{BeaconChainBuilder, Witness}, +}; pub use crate::{ - beacon_chain::{BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}, + BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification, + beacon_chain::{BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}, migrate::MigratorConfig, single_attestation::single_attestation_to_attestation, sync_committee_verification::Error as SyncCommitteeError, validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}, - BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification, }; -use crate::{ - builder::{BeaconChainBuilder, Witness}, - eth1_chain::CachingEth1Backend, - BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler, - StateSkipConfig, -}; -use crate::{get_block_root, BeaconBlockResponseWrapper}; use bls::get_withdrawal_credentials; -use eth2::types::SignedBlockContentsTuple; +use bls::{ + AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, +}; +use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple}; use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ + ExecutionLayer, auth::JwtKey, test_utils::{ - ExecutionBlockGenerator, MockBuilder, MockExecutionLayer, DEFAULT_JWT_SECRET, - DEFAULT_TERMINAL_BLOCK, + DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, ExecutionBlockGenerator, MockBuilder, + MockExecutionLayer, }, - ExecutionLayer, }; +use fixed_bytes::FixedBytesExtended; use futures::channel::mpsc::Receiver; -pub use genesis::{InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH}; +pub use genesis::{DEFAULT_ETH1_BLOCK_HASH, InteropGenesisBuilder}; use int_to_bytes::int_to_bytes32; +use kzg::Kzg; use kzg::trusted_setup::get_trusted_setup; -use kzg::{Kzg, TrustedSetup}; use logging::create_test_tracing_subscriber; use merkle_proof::MerkleTree; use operation_pool::ReceivedPreCapella; use parking_lot::{Mutex, RwLockWriteGuard}; -use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; +use rand::rngs::StdRng; +use rand::seq::SliceRandom; use rayon::prelude::*; use sensitive_url::SensitiveUrl; use slot_clock::{SlotClock, TestingSlotClock}; +use ssz_types::{RuntimeVariableList, VariableList}; use state_processing::per_block_processing::compute_timestamp_at_slot; use state_processing::state_advance::complete_state_advance; use std::borrow::Cow; @@ -55,26 +62,26 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock}; use std::time::Duration; use store::database::interface::BeaconNodeBackend; -use store::{config::StoreConfig, HotColdDB, ItemStore, MemoryStore}; +use store::{HotColdDB, ItemStore, MemoryStore, config::StoreConfig}; use task_executor::TaskExecutor; -use task_executor::{test_utils::TestRuntime, ShutdownReason}; +use task_executor::{ShutdownReason, test_utils::TestRuntime}; use tree_hash::TreeHash; +use typenum::U4294967296; +use types::data_column_custody_group::CustodyIndex; use types::indexed_attestation::IndexedAttestationBase; use types::payload::BlockProductionVersion; -pub use types::test_utils::generate_deterministic_keypairs; use types::test_utils::TestRandom; -use types::{typenum::U4294967296, *}; +pub use types::test_utils::generate_deterministic_keypairs; +use types::*; // 4th September 2019 pub const HARNESS_GENESIS_TIME: u64 = 1_567_552_690; // Environment variable to read if `fork_from_env` feature is enabled. pub const FORK_NAME_ENV_VAR: &str = "FORK_NAME"; -// Environment variable to read if `ci_logger` feature is enabled. -pub const CI_LOGGER_DIR_ENV_VAR: &str = "CI_LOGGER_DIR"; // Pre-computed data column sidecar using a single static blob from: // `beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle.ssz` -const TEST_DATA_COLUMN_SIDECARS_SSZ: &[u8] = +pub const TEST_DATA_COLUMN_SIDECARS_SSZ: &[u8] = include_bytes!("test_utils/fixtures/test_data_column_sidecars.ssz"); // Default target aggregators to set during testing, this ensures an aggregator at each slot. @@ -83,34 +90,23 @@ const TEST_DATA_COLUMN_SIDECARS_SSZ: &[u8] = // a different value. pub const DEFAULT_TARGET_AGGREGATORS: u64 = u64::MAX; -static KZG: LazyLock> = LazyLock::new(|| { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); - Arc::new(kzg) -}); +// Minimum and maximum number of blobs to generate in each slot when using the `NumBlobs::Random` option (default). +const DEFAULT_MIN_BLOBS: usize = 1; +const DEFAULT_MAX_BLOBS: usize = 2; -static KZG_PEERDAS: LazyLock> = LazyLock::new(|| { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - let kzg = Kzg::new_from_trusted_setup_das_enabled(trusted_setup).expect("should create kzg"); +static KZG: LazyLock> = LazyLock::new(|| { + let kzg = Kzg::new_from_trusted_setup(&get_trusted_setup()).expect("should create kzg"); Arc::new(kzg) }); static KZG_NO_PRECOMP: LazyLock> = LazyLock::new(|| { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - let kzg = Kzg::new_from_trusted_setup_no_precomp(trusted_setup).expect("should create kzg"); + let kzg = + Kzg::new_from_trusted_setup_no_precomp(&get_trusted_setup()).expect("should create kzg"); Arc::new(kzg) }); pub fn get_kzg(spec: &ChainSpec) -> Arc { if spec.fulu_fork_epoch.is_some() { - KZG_PEERDAS.clone() - } else if spec.deneb_fork_epoch.is_some() { KZG.clone() } else { KZG_NO_PRECOMP.clone() @@ -118,7 +114,7 @@ pub fn get_kzg(spec: &ChainSpec) -> Arc { } pub type BaseHarnessType = - Witness, E, THotStore, TColdStore>; + Witness; pub type DiskHarnessType = BaseHarnessType, BeaconNodeBackend>; pub type EphemeralHarnessType = BaseHarnessType, MemoryStore>; @@ -189,23 +185,28 @@ fn make_rng() -> Mutex { Mutex::new(StdRng::seed_from_u64(0x0DDB1A5E5BAD5EEDu64)) } -/// Return a `ChainSpec` suitable for test usage. -/// -/// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment -/// variable. Otherwise use the default spec. -pub fn test_spec() -> ChainSpec { - let mut spec = if cfg!(feature = "fork_from_env") { +pub fn fork_name_from_env() -> Option { + if cfg!(feature = "fork_from_env") { let fork_name = std::env::var(FORK_NAME_ENV_VAR).unwrap_or_else(|e| { panic!( "{} env var must be defined when using fork_from_env: {:?}", FORK_NAME_ENV_VAR, e ) }); - let fork = ForkName::from_str(fork_name.as_str()).unwrap(); - fork.make_genesis_spec(E::default_spec()) + Some(ForkName::from_str(fork_name.as_str()).unwrap()) } else { - E::default_spec() - }; + None + } +} + +/// Return a `ChainSpec` suitable for test usage. +/// +/// If the `fork_from_env` feature is enabled, read the fork to use from the FORK_NAME environment +/// variable. Otherwise use the default spec. +pub fn test_spec() -> ChainSpec { + let mut spec = fork_name_from_env() + .map(|fork| fork.make_genesis_spec(E::default_spec())) + .unwrap_or_else(|| E::default_spec()); // Set target aggregators to a high value by default. spec.target_aggregators_per_committee = DEFAULT_TARGET_AGGREGATORS; @@ -228,7 +229,7 @@ pub struct Builder { testing_slot_clock: Option, validator_monitor_config: Option, genesis_state_builder: Option>, - import_all_data_columns: bool, + node_custody_type: NodeCustodyType, runtime: TestRuntime, } @@ -374,7 +375,7 @@ where testing_slot_clock: None, validator_monitor_config: None, genesis_state_builder: None, - import_all_data_columns: false, + node_custody_type: NodeCustodyType::Fullnode, runtime, } } @@ -460,8 +461,8 @@ where self } - pub fn import_all_data_columns(mut self, import_all_data_columns: bool) -> Self { - self.import_all_data_columns = import_all_data_columns; + pub fn node_custody_type(mut self, node_custody_type: NodeCustodyType) -> Self { + self.node_custody_type = node_custody_type; self } @@ -518,15 +519,15 @@ where mock.server.execution_block_generator().osaka_time = spec.fulu_fork_epoch.map(|epoch| { genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); + mock.server.execution_block_generator().amsterdam_time = + spec.gloas_fork_epoch.map(|epoch| { + genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); self } - pub fn mock_execution_layer(self) -> Self { - self.mock_execution_layer_with_config() - } - - pub fn mock_execution_layer_with_config(mut self) -> Self { + pub fn mock_execution_layer(mut self) -> Self { let mock = mock_execution_layer_from_parts::( self.spec.clone().expect("cannot build without spec"), self.runtime.task_executor.clone(), @@ -585,11 +586,10 @@ where ) .task_executor(self.runtime.task_executor.clone()) .execution_layer(self.execution_layer) - .dummy_eth1_backend() - .expect("should build dummy backend") .shutdown_sender(shutdown_tx) .chain_config(chain_config) - .import_all_data_columns(self.import_all_data_columns) + .node_custody_type(self.node_custody_type) + .ordered_custody_column_indices(generate_data_column_indices_rand_order::()) .event_handler(Some(ServerSentEventHandler::new_with_capacity(5))) .validator_monitor_config(validator_monitor_config) .rng(Box::new(StdRng::seed_from_u64(42))); @@ -619,12 +619,6 @@ where let chain = builder.build().expect("should build"); - let sampling_column_count = if self.import_all_data_columns { - chain.spec.number_of_custody_groups as usize - } else { - chain.spec.custody_requirement as usize - }; - BeaconChainHarness { spec: chain.spec.clone(), chain: Arc::new(chain), @@ -635,7 +629,6 @@ where mock_execution_layer: self.mock_execution_layer, mock_builder: None, rng: make_rng(), - sampling_column_count, } } } @@ -659,6 +652,9 @@ pub fn mock_execution_layer_from_parts( let osaka_time = spec.fulu_fork_epoch.map(|epoch| { HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); + let amsterdam_time = spec.gloas_fork_epoch.map(|epoch| { + HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); let kzg = get_kzg(&spec); @@ -670,6 +666,7 @@ pub fn mock_execution_layer_from_parts( prague_time, eip7805_time, osaka_time, + amsterdam_time, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec, Some(kzg), @@ -696,7 +693,6 @@ pub struct BeaconChainHarness { pub mock_execution_layer: Option>, pub mock_builder: Option>>, - pub sampling_column_count: usize, pub rng: Mutex, } @@ -738,7 +734,10 @@ where pub fn set_mock_builder( &mut self, beacon_url: SensitiveUrl, - ) -> impl futures::Future { + strict_registrations: bool, + apply_operations: bool, + broadcast_to_bn: bool, + ) -> impl futures::Future + use { let mock_el = self .mock_execution_layer .as_ref() @@ -750,6 +749,9 @@ where let (mock_builder, (addr, mock_builder_server)) = MockBuilder::new_for_testing( mock_el_url, beacon_url, + strict_registrations, + apply_operations, + broadcast_to_bn, self.spec.clone(), self.runtime.task_executor.clone(), ); @@ -798,10 +800,6 @@ where (0..self.validator_keypairs.len()).collect() } - pub fn get_sampling_column_count(&self) -> usize { - self.sampling_column_count - } - pub fn slots_per_epoch(&self) -> u64 { E::slots_per_epoch() } @@ -914,7 +912,9 @@ where let fork_choice = self.chain.canonical_head.fork_choice_read_lock(); if heads.is_empty() { let nodes = &fork_choice.proto_array().core_proto_array().nodes; - panic!("Expected to know head block root {head_block_root:?}, but heads is empty. Nodes: {nodes:#?}"); + panic!( + "Expected to know head block root {head_block_root:?}, but heads is empty. Nodes: {nodes:#?}" + ); } else { panic!( "Expected to know head block root {head_block_root:?}, known heads {heads:#?}" @@ -928,8 +928,67 @@ where state: BeaconState, slot: Slot, ) -> (SignedBlindedBeaconBlock, BeaconState) { - let (unblinded, new_state) = self.make_block(state, slot).await; - ((*unblinded.0).clone().into(), new_state) + self.make_blinded_block_with_modifier(state, slot, |_| {}) + .await + } + + pub async fn make_blinded_block_with_modifier( + &self, + mut state: BeaconState, + slot: Slot, + block_modifier: impl FnOnce(&mut BlindedBeaconBlock), + ) -> (SignedBlindedBeaconBlock, BeaconState) { + assert_ne!(slot, 0, "can't produce a block at slot 0"); + assert!(slot >= state.slot()); + + complete_state_advance(&mut state, None, slot, &self.spec) + .expect("should be able to advance state to slot"); + + state.build_caches(&self.spec).expect("should build caches"); + + let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); + + // If we produce two blocks for the same slot, they hash up to the same value and + // BeaconChain errors out with `DuplicateFullyImported`. Vary the graffiti so that we produce + // different blocks each time. + let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>()); + let graffiti_settings = + GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti)); + + let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); + + // Always use the builder, so that we produce a "real" blinded payload. + let builder_boost_factor = Some(u64::MAX); + + let BeaconBlockResponseWrapper::Blinded(block_response) = self + .chain + .produce_block_on_state( + state, + None, + slot, + randao_reveal, + graffiti_settings, + ProduceBlockVerification::VerifyRandao, + builder_boost_factor, + BlockProductionVersion::V3, + ) + .await + .unwrap() + else { + panic!("Should always be a blinded payload response"); + }; + + let mut block = block_response.block; + block_modifier(&mut block); + + let signed_block = block.sign( + &self.validator_keypairs[proposer_index].sk, + &block_response.state.fork(), + block_response.state.genesis_validators_root(), + &self.spec, + ); + + (signed_block, block_response.state) } /// Returns a newly created block, signed by the proposer for the given slot. @@ -951,7 +1010,9 @@ where // If we produce two blocks for the same slot, they hash up to the same value and // BeaconChain errors out with `DuplicateFullyImported`. Vary the graffiti so that we produce // different blocks each time. - let graffiti = Graffiti::from(self.rng.lock().gen::<[u8; 32]>()); + let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>()); + let graffiti_settings = + GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti)); let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); @@ -962,7 +1023,7 @@ where None, slot, randao_reveal, - Some(graffiti), + graffiti_settings, ProduceBlockVerification::VerifyRandao, None, BlockProductionVersion::FullV2, @@ -1010,7 +1071,9 @@ where // If we produce two blocks for the same slot, they hash up to the same value and // BeaconChain errors out with `DuplicateFullyImported`. Vary the graffiti so that we produce // different blocks each time. - let graffiti = Graffiti::from(self.rng.lock().gen::<[u8; 32]>()); + let graffiti = Graffiti::from(self.rng.lock().random::<[u8; 32]>()); + let graffiti_settings = + GraffitiSettings::new(Some(graffiti), Some(GraffitiPolicy::PreserveUserGraffiti)); let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); @@ -1023,7 +1086,7 @@ where None, slot, randao_reveal, - Some(graffiti), + graffiti_settings, ProduceBlockVerification::VerifyRandao, None, BlockProductionVersion::FullV2, @@ -1137,9 +1200,14 @@ where attn.aggregation_bits .set(aggregation_bit_index, true) .unwrap(); - attn + Attestation::Electra(attn) + } + Attestation::Base(mut attn) => { + attn.aggregation_bits + .set(aggregation_bit_index, true) + .unwrap(); + Attestation::Base(attn) } - Attestation::Base(_) => panic!("Must be an Electra attestation"), }; let aggregation_bits = attestation.get_aggregation_bits(); @@ -1167,8 +1235,10 @@ where let single_attestation = attestation.to_single_attestation_with_attester_index(attester_index as u64)?; + let fork_name = self.spec.fork_name_at_slot::(attestation.data().slot); let attestation: Attestation = - single_attestation_to_attestation(&single_attestation, committee.committee).unwrap(); + single_attestation_to_attestation(&single_attestation, committee.committee, fork_name) + .unwrap(); assert_eq!( single_attestation.committee_index, @@ -2344,7 +2414,7 @@ where .collect::>(); // Building a VarList from leaves - let deposit_data_list = VariableList::<_, U4294967296>::from(leaves.clone()); + let deposit_data_list = VariableList::<_, U4294967296>::try_from(leaves.clone()).unwrap(); // Setting the deposit_root to be the tree_hash_root of the VarList state.eth1_data_mut().deposit_root = deposit_data_list.tree_hash_root(); @@ -2368,7 +2438,7 @@ where let deposits = datas .into_par_iter() .zip(proofs.into_par_iter()) - .map(|(data, proof)| (data, proof.into())) + .map(|(data, proof)| (data, proof.try_into().unwrap())) .map(|(data, proof)| Deposit { proof, data }) .collect::>(); @@ -2440,7 +2510,7 @@ where .blob_kzg_commitments() .is_ok_and(|c| !c.is_empty()); if !has_blobs { - return RpcBlock::new_without_blobs(Some(block_root), block, 0); + return RpcBlock::new_without_blobs(Some(block_root), block); } // Blobs are stored as data columns from Fulu (PeerDAS) @@ -2450,14 +2520,7 @@ where .into_iter() .map(CustodyDataColumn::from_asserted_custody) .collect::>(); - RpcBlock::new_with_custody_columns( - Some(block_root), - block, - custody_columns, - self.get_sampling_column_count(), - &self.spec, - ) - .unwrap() + RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns).unwrap() } else { let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); RpcBlock::new(Some(block_root), block, blobs).unwrap() @@ -2465,14 +2528,15 @@ where } /// Builds an `RpcBlock` from a `SignedBeaconBlock` and `BlobsList`. - fn build_rpc_block_from_blobs( + pub fn build_rpc_block_from_blobs( &self, block_root: Hash256, block: Arc>>, blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { - let sampling_column_count = self.get_sampling_column_count(); + let epoch = block.slot().epoch(E::slots_per_epoch()); + let sampling_columns = self.chain.sampling_columns_for_epoch(epoch); if blob_items.is_some_and(|(_, blobs)| !blobs.is_empty()) { // Note: this method ignores the actual custody columns and just take the first @@ -2480,18 +2544,12 @@ where // currently have any knowledge of the columns being custodied. let columns = generate_data_column_sidecars_from_block(&block, &self.spec) .into_iter() - .take(sampling_column_count) + .filter(|d| sampling_columns.contains(&d.index)) .map(CustodyDataColumn::from_asserted_custody) .collect::>(); - RpcBlock::new_with_custody_columns( - Some(block_root), - block, - columns, - sampling_column_count, - &self.spec, - )? + RpcBlock::new_with_custody_columns(Some(block_root), block, columns)? } else { - RpcBlock::new_without_blobs(Some(block_root), block, 0) + RpcBlock::new_without_blobs(Some(block_root), block) } } else { let blobs = blob_items @@ -2504,7 +2562,11 @@ where }) } - pub fn process_attestations(&self, attestations: HarnessAttestations) { + pub fn process_attestations( + &self, + attestations: HarnessAttestations, + state: &BeaconState, + ) { let num_validators = self.validator_keypairs.len(); let mut unaggregated = Vec::with_capacity(num_validators); // This is an over-allocation, but it should be fine. It won't be *that* memory hungry and @@ -2513,7 +2575,35 @@ where for (unaggregated_attestations, maybe_signed_aggregate) in attestations.iter() { for (attn, subnet) in unaggregated_attestations { - unaggregated.push((attn, Some(*subnet))); + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + + let single_attestation = attn + .to_single_attestation_with_attester_index(attester_index as u64) + .unwrap(); + + unaggregated.push((single_attestation, Some(*subnet))); } if let Some(a) = maybe_signed_aggregate { @@ -2523,7 +2613,9 @@ where for result in self .chain - .batch_verify_unaggregated_attestations_for_gossip(unaggregated.into_iter()) + .batch_verify_unaggregated_attestations_for_gossip( + unaggregated.iter().map(|(attn, subnet)| (attn, *subnet)), + ) .unwrap() { let verified = result.unwrap(); @@ -2590,7 +2682,7 @@ where ) { let attestations = self.make_attestations(validators, state, state_root, block_hash, block.slot()); - self.process_attestations(attestations); + self.process_attestations(attestations, state); } pub fn sync_committee_sign_block( @@ -2745,10 +2837,7 @@ where mut latest_block_hash: Option, sync_committee_strategy: SyncCommitteeStrategy, ) -> AddBlocksResult { - assert!( - slots.windows(2).all(|w| w[0] <= w[1]), - "Slots have to be sorted" - ); // slice.is_sorted() isn't stabilized at the moment of writing this + assert!(slots.is_sorted(), "Slots have to be in ascending order"); let mut block_hash_from_slot: HashMap = HashMap::new(); let mut state_hash_from_slot: HashMap = HashMap::new(); for slot in slots { @@ -2788,10 +2877,7 @@ where mut latest_block_hash: Option, sync_committee_strategy: SyncCommitteeStrategy, ) -> AddBlocksResult { - assert!( - slots.windows(2).all(|w| w[0] <= w[1]), - "Slots have to be sorted" - ); // slice.is_sorted() isn't stabilized at the moment of writing this + assert!(slots.is_sorted(), "Slots have to be in ascending order"); let mut block_hash_from_slot: HashMap = HashMap::new(); let mut state_hash_from_slot: HashMap = HashMap::new(); for slot in slots { @@ -2918,7 +3004,6 @@ where let chain_dump = self.chain.chain_dump().unwrap(); chain_dump .iter() - .cloned() .map(|checkpoint| checkpoint.beacon_state.finalized_checkpoint().root) .filter(|block_hash| *block_hash != Hash256::zero()) .map(|hash| hash.into()) @@ -3190,17 +3275,22 @@ where let is_peerdas_enabled = self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); if is_peerdas_enabled { let custody_columns = custody_columns_opt.unwrap_or_else(|| { - let sampling_column_count = self.get_sampling_column_count() as u64; - (0..sampling_column_count).collect() + let epoch = block.slot().epoch(E::slots_per_epoch()); + self.chain + .sampling_columns_for_epoch(epoch) + .iter() + .copied() + .collect() }); let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) .into_iter() .filter(|c| custody_columns.contains(&c.index)) .map(|sidecar| { - let column_index = sidecar.index; + let subnet_id = + DataColumnSubnetId::from_column_index(sidecar.index, &self.spec); self.chain - .verify_data_column_sidecar_for_gossip(sidecar, column_index) + .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) }) .collect::, _>>() .unwrap(); @@ -3246,96 +3336,50 @@ pub enum NumBlobs { None, } +macro_rules! add_blob_transactions { + ($message:expr, $payload_type:ty, $num_blobs:expr, $rng:expr, $fork_name:expr) => {{ + let num_blobs = match $num_blobs { + NumBlobs::Random => $rng.random_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs, $fork_name).unwrap(); + + let payload: &mut $payload_type = &mut $message.body.execution_payload; + payload.execution_payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.execution_payload.transactions.push(tx).unwrap(); + } + $message.body.blob_kzg_commitments = bundle.commitments.clone(); + bundle + }}; +} + pub fn generate_rand_block_and_blobs( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, - spec: &ChainSpec, ) -> (SignedBeaconBlock>, Vec>) { let inner = map_fork_name!(fork_name, BeaconBlock, <_>::random_for_test(rng)); - let mut block = SignedBeaconBlock::from_block(inner, types::Signature::random_for_test(rng)); - let max_blobs = spec.max_blobs_per_block(block.epoch()) as usize; + let mut block = SignedBeaconBlock::from_block(inner, Signature::random_for_test(rng)); let mut blob_sidecars = vec![]; let bundle = match block { SignedBeaconBlock::Deneb(SignedBeaconBlockDeneb { ref mut message, .. - }) => { - // Get either zero blobs or a random number of blobs between 1 and Max Blobs. - let payload: &mut FullPayloadDeneb = &mut message.body.execution_payload; - let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=max_blobs), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); - - payload.execution_payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.execution_payload.transactions.push(tx).unwrap(); - } - message.body.blob_kzg_commitments = bundle.commitments.clone(); - bundle - } + }) => add_blob_transactions!(message, FullPayloadDeneb, num_blobs, rng, fork_name), SignedBeaconBlock::Electra(SignedBeaconBlockElectra { ref mut message, .. - }) => { - // Get either zero blobs or a random number of blobs between 1 and Max Blobs. - let payload: &mut FullPayloadElectra = &mut message.body.execution_payload; - let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=max_blobs), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); - payload.execution_payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.execution_payload.transactions.push(tx).unwrap(); - } - message.body.blob_kzg_commitments = bundle.commitments.clone(); - bundle - } - SignedBeaconBlock::Eip7805(SignedBeaconBlockEip7805 { - ref mut message, .. - }) => { - // Get either zero blobs or a random number of blobs between 1 and Max Blobs. - let payload: &mut FullPayloadEip7805 = &mut message.body.execution_payload; - let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=max_blobs), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); - payload.execution_payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.execution_payload.transactions.push(tx).unwrap(); - } - message.body.blob_kzg_commitments = bundle.commitments.clone(); - bundle - } + }) => add_blob_transactions!(message, FullPayloadElectra, num_blobs, rng, fork_name), SignedBeaconBlock::Fulu(SignedBeaconBlockFulu { ref mut message, .. - }) => { - // Get either zero blobs or a random number of blobs between 1 and Max Blobs. - let payload: &mut FullPayloadFulu = &mut message.body.execution_payload; - let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=max_blobs), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); - payload.execution_payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.execution_payload.transactions.push(tx).unwrap(); - } - message.body.blob_kzg_commitments = bundle.commitments.clone(); - bundle - } + }) => add_blob_transactions!(message, FullPayloadFulu, num_blobs, rng, fork_name), + SignedBeaconBlock::Eip7805(SignedBeaconBlockEip7805 { + ref mut message, .. + }) => add_blob_transactions!(message, FullPayloadEip7805, num_blobs, rng, fork_name), + // TODO(EIP-7732) Add `SignedBeaconBlock::Gloas` variant _ => return (block, blob_sidecars), }; @@ -3376,13 +3420,13 @@ pub fn generate_rand_block_and_data_columns( SignedBeaconBlock>, DataColumnSidecarList, ) { - let (block, _blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng, spec); + let (block, _blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); let data_columns = generate_data_column_sidecars_from_block(&block, spec); (block, data_columns) } /// Generate data column sidecars from pre-computed cells and proofs. -fn generate_data_column_sidecars_from_block( +pub fn generate_data_column_sidecars_from_block( block: &SignedBeaconBlock, spec: &ChainSpec, ) -> DataColumnSidecarList { @@ -3401,7 +3445,7 @@ fn generate_data_column_sidecars_from_block( // load the precomputed column sidecar to avoid computing them for every block in the tests. let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( TEST_DATA_COLUMN_SIDECARS_SSZ, - spec.number_of_columns as usize, + E::number_of_columns(), ) .unwrap(); @@ -3432,3 +3476,9 @@ fn generate_data_column_sidecars_from_block( ) .unwrap() } + +pub fn generate_data_column_indices_rand_order() -> Vec { + let mut indices = (0..E::number_of_columns() as u64).collect::>(); + indices.shuffle(&mut StdRng::seed_from_u64(42)); + indices +} diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index e82689be02..fd8e690837 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -4,6 +4,7 @@ use crate::beacon_proposer_cache::{BeaconProposerCache, TYPICAL_SLOTS_PER_EPOCH}; use crate::metrics; +use bls::PublicKeyBytes; use itertools::Itertools; use logging::crit; use parking_lot::{Mutex, RwLock}; @@ -12,7 +13,7 @@ use slot_clock::SlotClock; use smallvec::SmallVec; use state_processing::common::get_attestation_participation_flag_indices; use state_processing::per_epoch_processing::{ - errors::EpochProcessingError, EpochProcessingSummary, + EpochProcessingSummary, errors::EpochProcessingError, }; use std::collections::{HashMap, HashSet}; use std::io; @@ -21,16 +22,17 @@ use std::str::Utf8Error; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::AbstractExecPayload; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, info, warn}; use types::consts::altair::{ TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }; use types::{ Attestation, AttestationData, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, - IndexedAttestationRef, ProposerSlashing, PublicKeyBytes, SignedAggregateAndProof, - SignedContributionAndProof, SignedInclusionList, Slot, SyncCommitteeMessage, VoluntaryExit, + IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, SignedContributionAndProof, + SignedInclusionList, Slot, SyncCommitteeMessage, VoluntaryExit, }; + /// Used for Prometheus labels. /// /// We've used `total` for this value to align with Nimbus, as per: @@ -163,7 +165,7 @@ impl EpochSummary { /// - It is `None`. /// - `new` is greater than its current value. fn update_if_lt(current: &mut Option, new: T) { - if let Some(ref mut current) = current { + if let Some(current) = current { if new < *current { *current = new } @@ -342,7 +344,7 @@ impl MonitoredValidator { // Prune while summaries.len() > HISTORIC_EPOCHS { - if let Some(key) = summaries.iter().map(|(epoch, _)| *epoch).min() { + if let Some(key) = summaries.keys().copied().min() { summaries.remove(&key); } } @@ -405,11 +407,6 @@ pub struct ValidatorMonitor { } impl ValidatorMonitor { - #[instrument(parent = None, - level = "info", - name = "validator_monitor", - skip_all - )] pub fn new( config: ValidatorMonitorConfig, beacon_proposer_cache: Arc>, @@ -439,23 +436,11 @@ impl ValidatorMonitor { /// Returns `true` when the validator count is sufficiently low enough to /// emit metrics and logs on a per-validator basis (rather than just an /// aggregated basis). - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn individual_tracking(&self) -> bool { self.validators.len() <= self.individual_tracking_threshold } /// Add some validators to `self` for additional monitoring. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn add_validator_pubkey(&mut self, pubkey: PublicKeyBytes) { let index_opt = self .indices @@ -473,43 +458,26 @@ impl ValidatorMonitor { } /// Add an unaggregated attestation - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn set_unaggregated_attestation(&mut self, attestation: Attestation) { let unaggregated_attestations = &mut self.unaggregated_attestations; // Pruning, this removes the oldest key/pair of the hashmap if it's greater than MAX_UNAGGREGATED_ATTESTATION_HASHMAP_LENGTH - if unaggregated_attestations.len() >= MAX_UNAGGREGATED_ATTESTATION_HASHMAP_LENGTH { - if let Some(oldest_slot) = unaggregated_attestations.keys().min().copied() { - unaggregated_attestations.remove(&oldest_slot); - } + if unaggregated_attestations.len() >= MAX_UNAGGREGATED_ATTESTATION_HASHMAP_LENGTH + && let Some(oldest_slot) = unaggregated_attestations.keys().min().copied() + { + unaggregated_attestations.remove(&oldest_slot); } + let slot = attestation.data().slot; self.unaggregated_attestations.insert(slot, attestation); } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn get_unaggregated_attestation(&self, slot: Slot) -> Option<&Attestation> { self.unaggregated_attestations.get(&slot) } /// Reads information from the given `state`. The `state` *must* be valid (i.e, able to be /// imported). - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn process_valid_state( &mut self, current_epoch: Epoch, @@ -531,7 +499,7 @@ impl ValidatorMonitor { }); // Add missed non-finalized blocks for the monitored validators - self.add_validators_missed_blocks(state); + self.add_validators_missed_blocks(state, spec); self.process_unaggregated_attestations(state, spec); // Update metrics for individual validators. @@ -622,13 +590,7 @@ impl ValidatorMonitor { } /// Add missed non-finalized blocks for the monitored validators - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] - fn add_validators_missed_blocks(&mut self, state: &BeaconState) { + fn add_validators_missed_blocks(&mut self, state: &BeaconState, spec: &ChainSpec) { // Define range variables let current_slot = state.slot(); let current_epoch = current_slot.epoch(E::slots_per_epoch()); @@ -656,8 +618,8 @@ impl ValidatorMonitor { if block_root == prev_block_root { let slot_epoch = slot.epoch(E::slots_per_epoch()); - if let Ok(shuffling_decision_block) = - state.proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root) + if let Ok(shuffling_decision_block) = state + .proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root, spec) { // Update the cache if it has not yet been initialised, or if it is // initialised for a prior epoch. This is an optimisation to avoid bouncing @@ -724,12 +686,6 @@ impl ValidatorMonitor { } } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn get_proposers_by_epoch_from_cache( &mut self, epoch: Epoch, @@ -743,12 +699,6 @@ impl ValidatorMonitor { /// Process the unaggregated attestations generated by the service `attestation_simulator_service` /// and check if the attestation qualifies for a reward matching the flags source/target/head - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn process_unaggregated_attestations(&mut self, state: &BeaconState, spec: &ChainSpec) { let current_slot = state.slot(); @@ -821,12 +771,6 @@ impl ValidatorMonitor { /// /// We allow disabling tracking metrics on an individual validator basis /// since it can result in untenable cardinality with high validator counts. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn aggregatable_metric(&self, individual_id: &str, func: F) { func(TOTAL_LABEL); @@ -835,12 +779,6 @@ impl ValidatorMonitor { } } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn process_validator_statuses( &self, epoch: Epoch, @@ -1118,12 +1056,6 @@ impl ValidatorMonitor { Ok(()) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn get_validator(&self, validator_index: u64) -> Option<&MonitoredValidator> { self.indices .get(&validator_index) @@ -1131,33 +1063,15 @@ impl ValidatorMonitor { } /// Returns the number of validators monitored by `self`. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn num_validators(&self) -> usize { self.validators.len() } /// Return the `id`'s of all monitored validators. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn get_all_monitored_validators(&self) -> Vec { self.validators.values().map(|val| val.id.clone()).collect() } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn get_monitored_validator(&self, index: u64) -> Option<&MonitoredValidator> { if let Some(pubkey) = self.indices.get(&index) { self.validators.get(pubkey) @@ -1166,12 +1080,6 @@ impl ValidatorMonitor { } } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn get_monitored_validator_missed_block_count(&self, validator_index: u64) -> u64 { self.missed_blocks .iter() @@ -1179,52 +1087,34 @@ impl ValidatorMonitor { .count() as u64 } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn get_beacon_proposer_cache(&self) -> Arc> { self.beacon_proposer_cache.clone() } /// If `self.auto_register == true`, add the `validator_index` to `self.monitored_validators`. /// Otherwise, do nothing. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn auto_register_local_validator(&mut self, validator_index: u64) { if !self.auto_register { return; } - if let Some(pubkey) = self.indices.get(&validator_index) { - if !self.validators.contains_key(pubkey) { - info!( - %pubkey, - validator = %validator_index, - "Started monitoring validator" - ); + if let Some(pubkey) = self.indices.get(&validator_index) + && !self.validators.contains_key(pubkey) + { + info!( + %pubkey, + validator = %validator_index, + "Started monitoring validator" + ); - self.validators.insert( - *pubkey, - MonitoredValidator::new(*pubkey, Some(validator_index)), - ); - } + self.validators.insert( + *pubkey, + MonitoredValidator::new(*pubkey, Some(validator_index)), + ); } } /// Process a block received on gossip. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_block( &self, seen_timestamp: Duration, @@ -1236,12 +1126,6 @@ impl ValidatorMonitor { } /// Process a block received on the HTTP API from a local validator. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_block( &self, seen_timestamp: Duration, @@ -1252,12 +1136,6 @@ impl ValidatorMonitor { self.register_beacon_block("api", seen_timestamp, block, block_root, slot_clock) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_beacon_block( &self, src: &str, @@ -1297,12 +1175,6 @@ impl ValidatorMonitor { } /// Register an attestation seen on the gossip network. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_unaggregated_attestation( &self, seen_timestamp: Duration, @@ -1318,12 +1190,6 @@ impl ValidatorMonitor { } /// Register an attestation seen on the HTTP API. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_unaggregated_attestation( &self, seen_timestamp: Duration, @@ -1338,12 +1204,6 @@ impl ValidatorMonitor { ) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_unaggregated_attestation( &self, src: &str, @@ -1356,7 +1216,7 @@ impl ValidatorMonitor { let delay = get_message_delay_ms( seen_timestamp, data.slot, - slot_clock.unagg_attestation_production_delay(), + Duration::from_secs(0), slot_clock, ); @@ -1430,12 +1290,6 @@ impl ValidatorMonitor { ) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_aggregated_attestation( &self, src: &str, @@ -1554,11 +1408,6 @@ impl ValidatorMonitor { /// We use the parent slot instead of block slot to ignore skip slots when calculating inclusion distance. /// /// Note: Blocks that get orphaned will skew the inclusion distance calculation. - #[instrument(parent = None, - level = "info", - name = "validator_monitor", - skip_all - )] pub fn register_attestation_in_block( &self, indexed_attestation: IndexedAttestationRef<'_, E>, @@ -1634,12 +1483,6 @@ impl ValidatorMonitor { } /// Register a sync committee message received over gossip. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_sync_committee_message( &self, seen_timestamp: Duration, @@ -1655,12 +1498,6 @@ impl ValidatorMonitor { } /// Register a sync committee message received over the http api. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_sync_committee_message( &self, seen_timestamp: Duration, @@ -1676,12 +1513,6 @@ impl ValidatorMonitor { } /// Register a sync committee message. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_sync_committee_message( &self, src: &str, @@ -1731,12 +1562,6 @@ impl ValidatorMonitor { } /// Register a sync committee contribution received over gossip. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_sync_committee_contribution( &self, seen_timestamp: Duration, @@ -1754,12 +1579,6 @@ impl ValidatorMonitor { } /// Register a sync committee contribution received over the http api. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_sync_committee_contribution( &self, seen_timestamp: Duration, @@ -1777,12 +1596,6 @@ impl ValidatorMonitor { } /// Register a sync committee contribution. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_sync_committee_contribution( &self, src: &str, @@ -1865,12 +1678,6 @@ impl ValidatorMonitor { } /// Register that the `sync_aggregate` was included in a *valid* `BeaconBlock`. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_sync_aggregate_in_block( &self, slot: Slot, @@ -1908,44 +1715,20 @@ impl ValidatorMonitor { } /// Register an exit from the gossip network. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("gossip", exit) } /// Register an exit from the HTTP API. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("api", exit) } /// Register an exit included in a *valid* beacon block. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_block_voluntary_exit(&self, exit: &VoluntaryExit) { self.register_voluntary_exit("block", exit) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_voluntary_exit(&self, src: &str, exit: &VoluntaryExit) { if let Some(validator) = self.get_validator(exit.validator_index) { let id = &validator.id; @@ -1969,44 +1752,20 @@ impl ValidatorMonitor { } /// Register a proposer slashing from the gossip network. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("gossip", slashing) } /// Register a proposer slashing from the HTTP API. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("api", slashing) } /// Register a proposer slashing included in a *valid* `BeaconBlock`. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_block_proposer_slashing(&self, slashing: &ProposerSlashing) { self.register_proposer_slashing("block", slashing) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_proposer_slashing(&self, src: &str, slashing: &ProposerSlashing) { let proposer = slashing.signed_header_1.message.proposer_index; let slot = slashing.signed_header_1.message.slot; @@ -2040,44 +1799,20 @@ impl ValidatorMonitor { } /// Register an attester slashing from the gossip network. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_gossip_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("gossip", slashing) } /// Register an attester slashing from the HTTP API. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_api_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("api", slashing) } /// Register an attester slashing included in a *valid* `BeaconBlock`. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn register_block_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("block", slashing) } - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] fn register_attester_slashing(&self, src: &str, slashing: AttesterSlashingRef<'_, E>) { let data = slashing.attestation_1().data(); let attestation_1_indices: HashSet = slashing @@ -2135,12 +1870,6 @@ impl ValidatorMonitor { /// Scrape `self` for metrics. /// /// Should be called whenever Prometheus is scraping Lighthouse. - #[instrument(parent = None, - level = "info", - fields(service = "validator_monitor"), - name = "validator_monitor", - skip_all - )] pub fn scrape_metrics(&self, slot_clock: &S, spec: &ChainSpec) { metrics::set_gauge( &metrics::VALIDATOR_MONITOR_VALIDATORS_TOTAL, diff --git a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs index 39d2c2c2d7..26ac02d91b 100644 --- a/beacon_node/beacon_chain/src/validator_pubkey_cache.rs +++ b/beacon_node/beacon_chain/src/validator_pubkey_cache.rs @@ -1,13 +1,17 @@ use crate::errors::BeaconChainError; use crate::{BeaconChainTypes, BeaconStore}; use bls::PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN; +use bls::{PublicKey, PublicKeyBytes}; +use fixed_bytes::FixedBytesExtended; +use rayon::prelude::*; use smallvec::SmallVec; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use std::marker::PhantomData; use store::{DBColumn, Error as StoreError, StoreItem, StoreOp}; -use types::{BeaconState, FixedBytesExtended, Hash256, PublicKey, PublicKeyBytes}; +use tracing::instrument; +use types::{BeaconState, Hash256}; /// Provides a mapping of `validator_index -> validator_publickey`. /// @@ -28,6 +32,7 @@ impl ValidatorPubkeyCache { /// Create a new public key cache using the keys in `state.validators`. /// /// The new cache will be updated with the keys from `state` and immediately written to disk. + #[instrument(name = "validator_pubkey_cache_new", skip_all)] pub fn new( state: &BeaconState, store: BeaconStore, @@ -46,6 +51,7 @@ impl ValidatorPubkeyCache { } /// Load the pubkey cache from the given on-disk database. + #[instrument(name = "validator_pubkey_cache_load_from_store", skip_all)] pub fn load_from_store(store: BeaconStore) -> Result { let mut pubkeys = vec![]; let mut indices = HashMap::new(); @@ -77,6 +83,7 @@ impl ValidatorPubkeyCache { /// Does not delete any keys from `self` if they don't appear in `state`. /// /// NOTE: The caller *must* commit the returned I/O batch as part of the block import process. + #[instrument(skip_all)] pub fn import_new_pubkeys( &mut self, state: &BeaconState, @@ -106,29 +113,58 @@ impl ValidatorPubkeyCache { self.indices.reserve(validator_keys.len()); let mut store_ops = Vec::with_capacity(validator_keys.len()); - for pubkey_bytes in validator_keys { - let i = self.pubkeys.len(); - if self.indices.contains_key(&pubkey_bytes) { - return Err(BeaconChainError::DuplicateValidatorPublicKey); + let is_initial_import = self.pubkeys.is_empty(); + + // Helper to insert a decompressed key + let mut insert_key = + |pubkey_bytes: PublicKeyBytes, pubkey: PublicKey| -> Result<(), BeaconChainError> { + let i = self.pubkeys.len(); + + if self.indices.contains_key(&pubkey_bytes) { + return Err(BeaconChainError::DuplicateValidatorPublicKey); + } + + // Stage the new validator key for writing to disk. + // It will be committed atomically when the block that introduced it is written to disk. + // Notably it is NOT written while the write lock on the cache is held. + // See: https://github.com/sigp/lighthouse/issues/2327 + store_ops.push(StoreOp::KeyValueOp( + DatabasePubkey::from_pubkey(&pubkey) + .as_kv_store_op(DatabasePubkey::key_for_index(i)), + )); + + self.pubkeys.push(pubkey); + self.pubkey_bytes.push(pubkey_bytes); + self.indices.insert(pubkey_bytes, i); + Ok(()) + }; + + if is_initial_import { + // On first startup, decompress keys in parallel for better performance + let validator_keys_vec: Vec = validator_keys.collect(); + + let decompressed: Vec<(PublicKeyBytes, PublicKey)> = validator_keys_vec + .into_par_iter() + .map(|pubkey_bytes| { + let pubkey = (&pubkey_bytes) + .try_into() + .map_err(BeaconChainError::InvalidValidatorPubkeyBytes)?; + Ok((pubkey_bytes, pubkey)) + }) + .collect::, BeaconChainError>>()?; + + for (pubkey_bytes, pubkey) in decompressed { + insert_key(pubkey_bytes, pubkey)?; + } + } else { + // Sequential path for incremental updates + for pubkey_bytes in validator_keys { + let pubkey = (&pubkey_bytes) + .try_into() + .map_err(BeaconChainError::InvalidValidatorPubkeyBytes)?; + insert_key(pubkey_bytes, pubkey)?; } - - let pubkey = (&pubkey_bytes) - .try_into() - .map_err(BeaconChainError::InvalidValidatorPubkeyBytes)?; - - // Stage the new validator key for writing to disk. - // It will be committed atomically when the block that introduced it is written to disk. - // Notably it is NOT written while the write lock on the cache is held. - // See: https://github.com/sigp/lighthouse/issues/2327 - store_ops.push(StoreOp::KeyValueOp( - DatabasePubkey::from_pubkey(&pubkey) - .as_kv_store_op(DatabasePubkey::key_for_index(i)), - )); - - self.pubkeys.push(pubkey); - self.pubkey_bytes.push(pubkey_bytes); - self.indices.insert(pubkey_bytes, i); } Ok(store_ops) @@ -210,10 +246,11 @@ impl DatabasePubkey { mod test { use super::*; use crate::test_utils::{BeaconChainHarness, EphemeralHarnessType}; + use bls::Keypair; use logging::create_test_tracing_subscriber; use std::sync::Arc; use store::HotColdDB; - use types::{EthSpec, Keypair, MainnetEthSpec}; + use types::{EthSpec, MainnetEthSpec}; type E = MainnetEthSpec; type T = EphemeralHarnessType; @@ -324,4 +361,39 @@ mod test { let cache = ValidatorPubkeyCache::load_from_store(store).expect("should open cache"); check_cache_get(&cache, &keypairs[..]); } + + #[test] + fn parallel_import_maintains_order() { + // Test that parallel decompression on first startup maintains correct order and indices + let (state, keypairs) = get_state(100); + let store = get_store(); + + // Create cache from empty state (triggers parallel path) + let cache: ValidatorPubkeyCache = + ValidatorPubkeyCache::new(&state, store).expect("should create cache"); + + check_cache_get(&cache, &keypairs[..]); + } + + #[test] + fn incremental_import_maintains_order() { + // Test that incremental imports maintain correct order (triggers sequential path) + let store = get_store(); + + // Start with 50 validators + let (state1, keypairs1) = get_state(50); + let mut cache = + ValidatorPubkeyCache::new(&state1, store.clone()).expect("should create cache"); + check_cache_get(&cache, &keypairs1[..]); + + // Add 50 more validators + let (state2, keypairs2) = get_state(100); + let ops = cache + .import_new_pubkeys(&state2) + .expect("should import pubkeys"); + store.do_atomically_with_block_and_blobs_cache(ops).unwrap(); + + // Verify all 100 validators are correctly indexed + check_cache_get(&cache, &keypairs2[..]); + } } diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index d89a8530e1..017c249d10 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -3,12 +3,11 @@ use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; -use beacon_chain::{metrics, StateSkipConfig, WhenSlotSkipped}; +use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; +use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{ - AggregateSignature, Attestation, EthSpec, Keypair, MainnetEthSpec, RelativeEpoch, Slot, -}; +use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 16; diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 30eec539fc..7984ea4708 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1,31 +1,31 @@ #![cfg(not(debug_assertions))] use beacon_chain::attestation_verification::{ - batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, Error, + Error, batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, }; use beacon_chain::observed_aggregates::ObservedAttestationKey; -use beacon_chain::test_utils::{MakeAttestationOptions, HARNESS_GENESIS_TIME}; +use beacon_chain::test_utils::{HARNESS_GENESIS_TIME, MakeAttestationOptions}; use beacon_chain::{ + BeaconChain, BeaconChainError, BeaconChainTypes, ChainConfig, WhenSlotSkipped, attestation_verification::Error as AttnError, test_utils::{ - test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + single_attestation_to_attestation, test_spec, }, - BeaconChain, BeaconChainError, BeaconChainTypes, ChainConfig, WhenSlotSkipped, }; -use genesis::{interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; +use bls::{AggregateSignature, Keypair, SecretKey}; +use fixed_bytes::FixedBytesExtended; +use genesis::{DEFAULT_ETH1_BLOCK_HASH, interop_genesis_state}; use int_to_bytes::int_to_bytes32; -use ssz_types::BitVector; -use state_processing::{ - per_block_processing::errors::AttestationValidationError, per_slot_processing, -}; +use state_processing::per_slot_processing; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; +use typenum::Unsigned; use types::{ + Address, Attestation, AttestationRef, ChainSpec, Epoch, EthSpec, ForkName, Hash256, + MainnetEthSpec, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, signed_aggregate_and_proof::SignedAggregateAndProofRefMut, - test_utils::generate_deterministic_keypair, Address, AggregateSignature, Attestation, - AttestationRef, AttestationRefMut, BeaconStateError, BitList, ChainSpec, Epoch, EthSpec, - FixedBytesExtended, ForkName, Hash256, Keypair, MainnetEthSpec, SecretKey, SelectionProof, - SignedAggregateAndProof, Slot, SubnetId, Unsigned, + test_utils::generate_deterministic_keypair, }; pub type E = MainnetEthSpec; @@ -122,7 +122,7 @@ fn get_harness_capella_spec( /// Also returns some info about who created it. fn get_valid_unaggregated_attestation( chain: &BeaconChain, -) -> (Attestation, usize, usize, SecretKey, SubnetId) { +) -> (SingleAttestation, SecretKey, SubnetId) { let head = chain.head_snapshot(); let current_slot = chain.slot().expect("should get slot"); @@ -156,8 +156,15 @@ fn get_valid_unaggregated_attestation( ) .expect("should sign attestation"); - let subnet_id = SubnetId::compute_subnet_for_attestation::( - valid_attestation.to_ref(), + let single_attestation = SingleAttestation { + committee_index: valid_attestation.committee_index().unwrap(), + attester_index: validator_index as u64, + data: valid_attestation.data().clone(), + signature: valid_attestation.signature().clone(), + }; + + let subnet_id = SubnetId::compute_subnet_for_single_attestation::( + &single_attestation, head.beacon_state .get_committee_count_at_slot(current_slot) .expect("should get committee count"), @@ -165,13 +172,7 @@ fn get_valid_unaggregated_attestation( ) .expect("should get subnet_id"); - ( - valid_attestation, - validator_index, - validator_committee_index, - validator_sk, - subnet_id, - ) + (single_attestation, validator_sk, subnet_id) } fn get_valid_aggregated_attestation( @@ -275,15 +276,13 @@ struct GossipTester { /* * Valid unaggregated attestation */ - valid_attestation: Attestation, - attester_validator_index: usize, - attester_committee_index: usize, + valid_attestation: SingleAttestation, attester_sk: SecretKey, attestation_subnet_id: SubnetId, /* * Valid unaggregated attestation for batch testing */ - invalid_attestation: Attestation, + invalid_attestation: SingleAttestation, /* * Valid aggregate */ @@ -312,22 +311,33 @@ impl GossipTester { // Advance into a slot where there have not been blocks or attestations produced. harness.advance_slot(); - let ( - valid_attestation, - attester_validator_index, - attester_committee_index, - attester_sk, - attestation_subnet_id, - ) = get_valid_unaggregated_attestation(&harness.chain); + let (valid_attestation, attester_sk, attestation_subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + let head = harness.chain.head_snapshot(); + let state = &head.beacon_state; + let committee = state + .get_beacon_committee( + valid_attestation.data.slot, + valid_attestation.committee_index, + ) + .unwrap(); + let fork_name = harness + .chain + .spec + .fork_name_at_slot::(valid_attestation.data.slot); + let valid_aggregate_attestation = + single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name) + .unwrap(); let (valid_aggregate, aggregator_validator_index, aggregator_sk) = - get_valid_aggregated_attestation(&harness.chain, valid_attestation.clone()); + get_valid_aggregated_attestation(&harness.chain, valid_aggregate_attestation.clone()); let mut invalid_attestation = valid_attestation.clone(); - invalid_attestation.data_mut().beacon_block_root = Hash256::repeat_byte(13); + invalid_attestation.data.beacon_block_root = Hash256::repeat_byte(13); let (mut invalid_aggregate, _, _) = - get_valid_aggregated_attestation(&harness.chain, invalid_attestation.clone()); + get_valid_aggregated_attestation(&harness.chain, valid_aggregate_attestation.clone()); match invalid_aggregate.to_mut() { SignedAggregateAndProofRefMut::Base(att) => { @@ -341,8 +351,6 @@ impl GossipTester { Self { harness, valid_attestation, - attester_validator_index, - attester_committee_index, attester_sk, attestation_subnet_id, invalid_attestation, @@ -467,12 +475,12 @@ impl GossipTester { pub fn inspect_unaggregate_err(self, desc: &str, get_attn: G, inspect_err: I) -> Self where - G: Fn(&Self, &mut Attestation, &mut SubnetId), + G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec), I: Fn(&Self, AttnError), { let mut attn = self.valid_attestation.clone(); let mut subnet_id = self.attestation_subnet_id; - get_attn(&self, &mut attn, &mut subnet_id); + get_attn(&self, &mut attn, &mut subnet_id, &self.harness.spec); /* * Individual verification @@ -912,32 +920,20 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with invalid committee index", - |tester, a, _| { - match a.to_mut() { - AttestationRefMut::Base(attn) => { - attn.data.index = tester - .harness - .chain - .head_snapshot() - .beacon_state - .get_committee_count_at_slot(attn.data.slot) - .unwrap(); - } - AttestationRefMut::Electra(attn) => { - let committee_index = tester - .harness - .chain - .head_snapshot() - .beacon_state - .get_committee_count_at_slot(attn.data.slot) - .unwrap(); - // overwrite the existing committee bits before setting - attn.committee_bits = BitVector::default(); - attn.committee_bits.set(committee_index as usize, true).unwrap(); - } - } + |tester, a, _, _| { + let committee_index = tester + .harness + .chain + .head_snapshot() + .beacon_state + .get_committee_count_at_slot(a.data.slot) + .unwrap(); + + a.committee_index = committee_index; + }, + |_, err| { + assert!(matches!(err, AttnError::NoCommitteeForSlotAndIndex { .. })) }, - |_, err| assert!(matches!(err, AttnError::NoCommitteeForSlotAndIndex { .. })), ) /* * The following test ensures: @@ -946,8 +942,8 @@ async fn unaggregated_gossip_verification() { * attestation.data.slot, attestation.data.index) == subnet_id). */ .inspect_unaggregate_err( - "attestation with invalid committee index", - |_, _, subnet_id| *subnet_id = SubnetId::new(42), + "attestation with invalid subnet_id", + |_, _, subnet_id, _| *subnet_id = SubnetId::new(42), |tester, err| { assert!(matches!( err, @@ -969,7 +965,7 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation from future slot", - |tester, a, _| a.data_mut().slot = tester.slot() + 1, + |tester, a, _, _| a.data.slot = tester.slot() + 1, |tester, err| { assert!(matches!( err, @@ -983,10 +979,10 @@ async fn unaggregated_gossip_verification() { ) .inspect_unaggregate_err( "attestation from past slot", - |tester, a, _| { + |tester, a, _, _| { let too_early_slot = tester.earliest_valid_attestation_slot() - 1; - a.data_mut().slot = too_early_slot; - a.data_mut().target.epoch = too_early_slot.epoch(E::slots_per_epoch()); + a.data.slot = too_early_slot; + a.data.target.epoch = too_early_slot.epoch(E::slots_per_epoch()); }, |tester, err| { let valid_early_slot = tester.earliest_valid_attestation_slot(); @@ -1010,7 +1006,7 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with invalid target epoch", - |_, a, _| a.data_mut().target.epoch += 1, + |_, a, _, _| a.data.target.epoch += 1, |_, err| { assert!(matches!( err, @@ -1018,104 +1014,6 @@ async fn unaggregated_gossip_verification() { )) }, ) - /* - * The following two tests ensure: - * - * The attestation is unaggregated -- that is, it has exactly one participating validator - * (len([bit for bit in attestation.aggregation_bits if bit == 0b1]) == 1). - */ - .inspect_unaggregate_err( - "attestation without any aggregation bits set", - |tester, mut a, _| { - match &mut a { - Attestation::Base(ref mut att) => { - att.aggregation_bits - .set(tester.attester_committee_index, false) - .expect("should unset aggregation bit"); - assert_eq!( - att.aggregation_bits.num_set_bits(), - 0, - "test requires no set bits" - ); - } - Attestation::Electra(ref mut att) => { - att.aggregation_bits - .set(tester.attester_committee_index, false) - .expect("should unset aggregation bit"); - assert_eq!( - att.aggregation_bits.num_set_bits(), - 0, - "test requires no set bits" - ); - } - } - }, - |_, err| { - assert!(matches!( - err, - AttnError::NotExactlyOneAggregationBitSet(0) - )) - }, - ) - .inspect_unaggregate_err( - "attestation with two aggregation bits set", - |tester, mut a, _| { - match &mut a { - Attestation::Base(ref mut att) => { - att.aggregation_bits - .set(tester.attester_committee_index + 1, true) - .expect("should set second aggregation bit"); - } - Attestation::Electra(ref mut att) => { - att.aggregation_bits - .set(tester.attester_committee_index + 1, true) - .expect("should set second aggregation bit"); - } - } - }, - |_, err| { - assert!(matches!( - err, - AttnError::NotExactlyOneAggregationBitSet(2) - )) - }, - ) - /* - * The following test ensures: - * - * The number of aggregation bits matches the committee size -- i.e. - * `len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, - * data.index))`. - */ - .inspect_unaggregate_err( - "attestation with invalid bitfield", - |_, mut a, _| { - match &mut a { - Attestation::Base(ref mut att) => { - let bits = att.aggregation_bits.iter().collect::>(); - att.aggregation_bits = BitList::with_capacity(bits.len() + 1).unwrap(); - for (i, bit) in bits.into_iter().enumerate() { - att.aggregation_bits.set(i, bit).unwrap(); - } - } - Attestation::Electra(ref mut att) => { - let bits = att.aggregation_bits.iter().collect::>(); - att.aggregation_bits = BitList::with_capacity(bits.len() + 1).unwrap(); - for (i, bit) in bits.into_iter().enumerate() { - att.aggregation_bits.set(i, bit).unwrap(); - } - } - } - }, - |_, err| { - assert!(matches!( - err, - AttnError::Invalid(AttestationValidationError::BeaconStateError( - BeaconStateError::InvalidBitfield - )) - )) - }, - ) /* * The following test ensures that: * @@ -1123,8 +1021,8 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with unknown head block", - |_, a, _| { - a.data_mut().beacon_block_root = Hash256::repeat_byte(42); + |_, a, _, _| { + a.data.beacon_block_root = Hash256::repeat_byte(42); }, |_, err| { assert!(matches!( @@ -1145,8 +1043,8 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with invalid target root", - |_, a, _| { - a.data_mut().target.root = Hash256::repeat_byte(42); + |_, a, _, _| { + a.data.target.root = Hash256::repeat_byte(42); }, |_, err| { assert!(matches!( @@ -1162,10 +1060,10 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with bad signature", - |tester, a, _| { + |tester, a, _, _| { let mut agg_sig = AggregateSignature::infinity(); agg_sig.add_assign(&tester.attester_sk.sign(Hash256::repeat_byte(42))); - *a.signature_mut() = agg_sig; + a.signature = agg_sig; }, |_, err| { assert!(matches!( @@ -1186,7 +1084,7 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation that has already been seen", - |_, _, _| {}, + |_, _, _, _| {}, |tester, err| { assert!(matches!( err, @@ -1194,7 +1092,7 @@ async fn unaggregated_gossip_verification() { validator_index, epoch, } - if validator_index == tester.attester_validator_index as u64 && epoch == tester.epoch() + if validator_index == tester.valid_attestation.attester_index && epoch == tester.epoch() )) }, ); @@ -1243,7 +1141,7 @@ async fn attestation_that_skips_epochs() { let state_root = state.update_tree_hash_cache().unwrap(); let (attestation, subnet_id) = harness - .get_unaggregated_attestations( + .get_single_attestations( &AttestationStrategy::AllValidators, &state, state_root, @@ -1256,7 +1154,7 @@ async fn attestation_that_skips_epochs() { .cloned() .expect("should have at least one attestation in committee"); - let block_root = attestation.data().beacon_block_root; + let block_root = attestation.data.beacon_block_root; let block_slot = harness .chain .store @@ -1267,7 +1165,7 @@ async fn attestation_that_skips_epochs() { .slot(); assert!( - attestation.data().slot - block_slot > E::slots_per_epoch() * 2, + attestation.data.slot - block_slot > E::slots_per_epoch() * 2, "the attestation must skip more than two epochs" ); @@ -1357,7 +1255,7 @@ async fn attestation_validator_receive_proposer_reward_and_withdrawals() { // Verifying the attestation triggers an inconsistent state replay. let remaining_attesters = (two_thirds..VALIDATOR_COUNT).collect(); let (attestation, subnet_id) = harness - .get_unaggregated_attestations( + .get_single_attestations( &AttestationStrategy::SomeValidators(remaining_attesters), &state, state_root, @@ -1426,7 +1324,7 @@ async fn attestation_to_finalized_block() { let state_root = state.update_tree_hash_cache().unwrap(); let (attestation, subnet_id) = harness - .get_unaggregated_attestations( + .get_single_attestations( &AttestationStrategy::AllValidators, &state, state_root, @@ -1438,7 +1336,7 @@ async fn attestation_to_finalized_block() { .first() .cloned() .expect("should have at least one attestation in committee"); - assert_eq!(attestation.data().beacon_block_root, earlier_block_root); + assert_eq!(attestation.data.beacon_block_root, earlier_block_root); // Attestation should be rejected for attesting to a pre-finalization block. let res = harness @@ -1451,10 +1349,12 @@ async fn attestation_to_finalized_block() { ); // Pre-finalization block cache should contain the block root. - assert!(harness - .chain - .pre_finalization_block_cache - .contains(earlier_block_root)); + assert!( + harness + .chain + .pre_finalization_block_cache + .contains(earlier_block_root) + ); } #[tokio::test] @@ -1481,8 +1381,23 @@ async fn verify_aggregate_for_gossip_doppelganger_detection() { "the test requires a new epoch to avoid already-seen errors" ); - let (valid_attestation, _attester_index, _attester_committee_index, _, _) = - get_valid_unaggregated_attestation(&harness.chain); + let (valid_attestation, _, _) = get_valid_unaggregated_attestation(&harness.chain); + + let head = harness.chain.head_snapshot(); + let state = &head.beacon_state; + let committee = state + .get_beacon_committee( + valid_attestation.data.slot, + valid_attestation.committee_index, + ) + .unwrap(); + let fork_name = harness + .chain + .spec + .fork_name_at_slot::(valid_attestation.data.slot); + let valid_attestation = + single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name) + .unwrap(); let (valid_aggregate, _, _) = get_valid_aggregated_attestation(&harness.chain, valid_attestation); @@ -1496,24 +1411,30 @@ async fn verify_aggregate_for_gossip_doppelganger_detection() { assert!(harness.chain.validator_seen_at_epoch(index, epoch)); // Check the correct beacon cache is populated - assert!(!harness - .chain - .observed_block_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if block attester was observed")); - assert!(!harness - .chain - .observed_gossip_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip attester was observed")); - assert!(harness - .chain - .observed_aggregators - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip aggregator was observed")); + assert!( + !harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed") + ); + assert!( + !harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed") + ); + assert!( + harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed") + ); } #[tokio::test] @@ -1540,36 +1461,43 @@ async fn verify_attestation_for_gossip_doppelganger_detection() { "the test requires a new epoch to avoid already-seen errors" ); - let (valid_attestation, index, _attester_committee_index, _, subnet_id) = - get_valid_unaggregated_attestation(&harness.chain); + let (valid_attestation, _, subnet_id) = get_valid_unaggregated_attestation(&harness.chain); + + let index = valid_attestation.attester_index as usize; harness .chain .verify_unaggregated_attestation_for_gossip(&valid_attestation, Some(subnet_id)) .expect("should verify attestation"); - let epoch = valid_attestation.data().target.epoch; + let epoch = valid_attestation.data.target.epoch; assert!(harness.chain.validator_seen_at_epoch(index, epoch)); // Check the correct beacon cache is populated - assert!(!harness - .chain - .observed_block_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if block attester was observed")); - assert!(harness - .chain - .observed_gossip_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip attester was observed")); - assert!(!harness - .chain - .observed_aggregators - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip aggregator was observed")); + assert!( + !harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed") + ); + assert!( + harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed") + ); + assert!( + !harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed") + ); } #[tokio::test] @@ -1612,7 +1540,7 @@ async fn attestation_verification_use_head_state_fork() { let attesters = (0..VALIDATOR_COUNT / 2).collect::>(); let capella_fork = spec.fork_for_name(ForkName::Capella).unwrap(); let committee_attestations = harness - .make_unaggregated_attestations_with_opts( + .make_single_attestations_with_opts( attesters.as_slice(), &state, state_root, @@ -1632,7 +1560,8 @@ async fn attestation_verification_use_head_state_fork() { .map(|(attestation, subnet_id)| (attestation, Some(*subnet_id))); assert!( - batch_verify_unaggregated_attestations(attestations_and_subnets, &harness.chain).is_ok(), + batch_verify_unaggregated_attestations(attestations_and_subnets, &harness.chain) + .is_ok(), "should accept attestations with `data.slot` >= first capella slot signed using the Capella fork" ); } @@ -1642,7 +1571,7 @@ async fn attestation_verification_use_head_state_fork() { let attesters = (VALIDATOR_COUNT / 2..VALIDATOR_COUNT).collect::>(); let bellatrix_fork = spec.fork_for_name(ForkName::Bellatrix).unwrap(); let committee_attestations = harness - .make_unaggregated_attestations_with_opts( + .make_single_attestations_with_opts( attesters.as_slice(), &state, state_root, diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs index 3a424e73ba..5d466dd1d3 100644 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ b/beacon_node/beacon_chain/tests/bellatrix.rs @@ -1,7 +1,7 @@ #![cfg(not(debug_assertions))] // Tests run too slow in debug. use beacon_chain::test_utils::BeaconChainHarness; -use execution_layer::test_utils::{generate_pow_block, Block, DEFAULT_TERMINAL_BLOCK}; +use execution_layer::test_utils::{Block, DEFAULT_TERMINAL_BLOCK, generate_pow_block}; use types::*; const VALIDATOR_COUNT: usize = 32; diff --git a/beacon_node/beacon_chain/tests/blob_verification.rs b/beacon_node/beacon_chain/tests/blob_verification.rs new file mode 100644 index 0000000000..d1a0d87adf --- /dev/null +++ b/beacon_node/beacon_chain/tests/blob_verification.rs @@ -0,0 +1,121 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, +}; +use beacon_chain::{ + AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, + block_verification_types::AsBlock, +}; +use bls::{Keypair, Signature}; +use logging::create_test_tracing_subscriber; +use std::sync::{Arc, LazyLock}; +use types::{blob_sidecar::FixedBlobSidecarList, *}; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; + +/// A cached set of keys. +static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); + +fn get_harness( + validator_count: usize, + spec: Arc, +) -> BeaconChainHarness> { + create_test_tracing_subscriber(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +// Regression test for https://github.com/sigp/lighthouse/issues/7650 +#[tokio::test] +async fn rpc_blobs_with_invalid_header_signature() { + let spec = Arc::new(test_spec::()); + + // Only run this test if blobs are enabled and columns are disabled. + if spec.deneb_fork_epoch.is_none() || spec.is_fulu_scheduled() { + return; + } + + let harness = get_harness(VALIDATOR_COUNT, spec); + + let num_blocks = E::slots_per_epoch() as usize; + + // Add some chain depth. + harness + .extend_chain( + num_blocks, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block with blobs. + harness.execution_block_generator().set_min_blob_count(1); + let head_state = harness.get_current_state(); + let slot = head_state.slot() + 1; + let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await; + let (kzg_proofs, blobs) = opt_blobs.unwrap(); + assert!(!blobs.is_empty()); + let block_root = signed_block.canonical_root(); + + // Process the block without blobs so that it doesn't become available. + harness.advance_slot(); + let rpc_block = harness + .build_rpc_block_from_blobs(block_root, signed_block.clone(), None) + .unwrap(); + let availability = harness + .chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) + .await + .unwrap(); + assert_eq!( + availability, + AvailabilityProcessingStatus::MissingComponents(slot, block_root) + ); + + // Build blob sidecars with invalid signatures in the block header. + let mut corrupt_block = (*signed_block).clone(); + *corrupt_block.signature_mut() = Signature::infinity().unwrap(); + + let max_len = harness + .chain + .spec + .max_blobs_per_block(slot.epoch(E::slots_per_epoch())) as usize; + let mut blob_sidecars = FixedBlobSidecarList::new(vec![None; max_len]); + for (i, (kzg_proof, blob)) in kzg_proofs.into_iter().zip(blobs).enumerate() { + let blob_sidecar = BlobSidecar::new(i, blob, &corrupt_block, kzg_proof).unwrap(); + blob_sidecars[i] = Some(Arc::new(blob_sidecar)); + } + + let err = harness + .chain + .process_rpc_blobs(slot, block_root, blob_sidecars) + .await + .unwrap_err(); + assert!(matches!( + err, + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) + )); +} diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 1303978053..eae443d889 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3,21 +3,25 @@ use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::{ - test_utils::{ - test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, - }, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock, + custody_context::NodeCustodyType, + test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, + }, }; use beacon_chain::{ BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, InvalidSignature, NotifyExecutionLayer, }; +use bls::{AggregateSignature, Keypair, Signature}; +use fixed_bytes::FixedBytesExtended; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ + BlockProcessingError, ConsensusContext, VerifyBlockRoot, common::{attesting_indices_base, attesting_indices_electra}, - per_block_processing::{per_block_processing, BlockSignatureStrategy}, - per_slot_processing, BlockProcessingError, ConsensusContext, VerifyBlockRoot, + per_block_processing::{BlockSignatureStrategy, per_block_processing}, + per_slot_processing, }; use std::marker::PhantomData; use std::sync::{Arc, LazyLock}; @@ -30,8 +34,6 @@ type E = MainnetEthSpec; const VALIDATOR_COUNT: usize = 24; const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; -// Default custody group count for tests -const CGC: usize = 8; /// A cached set of keys. static KEYPAIRS: LazyLock> = @@ -43,7 +45,10 @@ enum DataSidecars { } async fn get_chain_segment() -> (Vec>, Vec>>) { - let harness = get_harness(VALIDATOR_COUNT); + // The assumption that you can re-import a block based on what you have in your DB + // is no longer true, as fullnodes stores less than what they sample. + // We use a supernode here to build a chain segment. + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Supernode); harness .extend_chain( @@ -102,7 +107,10 @@ async fn get_chain_segment() -> (Vec>, Vec BeaconChainHarness> { +fn get_harness( + validator_count: usize, + node_custody_type: NodeCustodyType, +) -> BeaconChainHarness> { let harness = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() .chain_config(ChainConfig { @@ -110,6 +118,7 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness BeaconChainHarness], chain_segment_sidecars: &[Option>], - spec: &ChainSpec, ) -> Vec> { chain_segment .iter() .zip(chain_segment_sidecars.iter()) .map(|(snapshot, data_sidecars)| { let block = snapshot.beacon_block.clone(); - build_rpc_block(block, data_sidecars, spec) + build_rpc_block(block, data_sidecars) }) .collect() } @@ -137,17 +145,15 @@ fn chain_segment_blocks( fn build_rpc_block( block: Arc>, data_sidecars: &Option>, - spec: &ChainSpec, ) -> RpcBlock { match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { RpcBlock::new(None, block, Some(blobs.clone())).unwrap() } Some(DataSidecars::DataColumns(columns)) => { - RpcBlock::new_with_custody_columns(None, block, columns.clone(), columns.len(), spec) - .unwrap() + RpcBlock::new_with_custody_columns(None, block, columns.clone()).unwrap() } - None => RpcBlock::new_without_blobs(None, block, 0), + None => RpcBlock::new_without_blobs(None, block), } } @@ -256,12 +262,11 @@ fn update_data_column_signed_header( #[tokio::test] async fn chain_segment_full_segment() { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; - let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); harness .chain @@ -294,20 +299,20 @@ async fn chain_segment_full_segment() { #[tokio::test] async fn chain_segment_varying_chunk_size() { - for chunk_size in &[1, 2, 3, 5, 31, 32, 33, 42] { - let harness = get_harness(VALIDATOR_COUNT); - let (chain_segment, chain_segment_blobs) = get_chain_segment().await; - let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let (chain_segment, chain_segment_blobs) = get_chain_segment().await; + let blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); + + for chunk_size in &[1, 2, 31, 32, 33] { + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); harness .chain .slot_clock .set_slot(blocks.last().unwrap().slot().as_u64()); - for chunk in blocks.chunks(*chunk_size) { + for chunk in blocks.clone().chunks(*chunk_size) { harness .chain .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) @@ -328,7 +333,7 @@ async fn chain_segment_varying_chunk_size() { #[tokio::test] async fn chain_segment_non_linear_parent_roots() { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; harness @@ -339,10 +344,9 @@ async fn chain_segment_non_linear_parent_roots() { /* * Test with a block removed. */ - let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let mut blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); blocks.remove(2); assert!( @@ -360,17 +364,15 @@ async fn chain_segment_non_linear_parent_roots() { /* * Test with a modified parent root. */ - let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let mut blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.parent_root_mut() = Hash256::zero(); blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), - harness.sampling_column_count, ); assert!( @@ -388,7 +390,7 @@ async fn chain_segment_non_linear_parent_roots() { #[tokio::test] async fn chain_segment_non_linear_slots() { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; harness .chain @@ -399,16 +401,14 @@ async fn chain_segment_non_linear_slots() { * Test where a child is lower than the parent. */ - let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let mut blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = Slot::new(0); blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), - harness.sampling_column_count, ); assert!( @@ -427,16 +427,14 @@ async fn chain_segment_non_linear_slots() { * Test where a child is equal to the parent. */ - let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, &harness.spec) - .into_iter() - .collect(); + let mut blocks: Vec> = chain_segment_blocks(&chain_segment, &chain_segment_blobs) + .into_iter() + .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = blocks[2].slot(); blocks[3] = RpcBlock::new_without_blobs( None, Arc::new(SignedBeaconBlock::from_block(block, signature)), - harness.sampling_column_count, ); assert!( @@ -463,9 +461,7 @@ async fn assert_invalid_signature( let blocks: Vec> = snapshots .iter() .zip(chain_segment_blobs.iter()) - .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, &harness.spec) - }) + .map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs)) .collect(); // Ensure the block will be rejected if imported in a chain segment. @@ -490,9 +486,7 @@ async fn assert_invalid_signature( .iter() .take(block_index) .zip(chain_segment_blobs.iter()) - .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, &harness.spec) - }) + .map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs)) .collect(); // We don't care if this fails, we just call this to ensure that all prior blocks have been // imported prior to this test. @@ -509,7 +503,6 @@ async fn assert_invalid_signature( build_rpc_block( snapshots[block_index].beacon_block.clone(), &chain_segment_blobs[block_index], - &harness.spec, ), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, @@ -539,7 +532,7 @@ async fn assert_invalid_signature( async fn get_invalid_sigs_harness( chain_segment: &[BeaconSnapshot], ) -> BeaconChainHarness> { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); harness .chain .slot_clock @@ -567,9 +560,7 @@ async fn invalid_signature_gossip_block() { .iter() .take(block_index) .zip(chain_segment_blobs.iter()) - .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, &harness.spec) - }) + .map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs)) .collect(); harness .chain @@ -578,11 +569,7 @@ async fn invalid_signature_gossip_block() { .into_block_error() .expect("should import all blocks prior to the one being tested"); let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); - let rpc_block = RpcBlock::new_without_blobs( - None, - Arc::new(signed_block), - harness.sampling_column_count, - ); + let rpc_block = RpcBlock::new_without_blobs(None, Arc::new(signed_block)); let process_res = harness .chain .process_block( @@ -624,9 +611,7 @@ async fn invalid_signature_block_proposal() { let blocks: Vec> = snapshots .iter() .zip(chain_segment_blobs.iter()) - .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, &harness.spec) - }) + .map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs)) .collect::>(); // Ensure the block will be rejected if imported in a chain segment. let process_res = harness @@ -725,7 +710,7 @@ async fn invalid_signature_attester_slashing() { let attester_slashing = if fork_name.electra_enabled() { let indexed_attestation = IndexedAttestationElectra { - attesting_indices: vec![0].into(), + attesting_indices: vec![0].try_into().unwrap(), data: AttestationData { slot: Slot::new(0), index: 0, @@ -749,7 +734,7 @@ async fn invalid_signature_attester_slashing() { AttesterSlashing::Electra(attester_slashing) } else { let indexed_attestation = IndexedAttestationBase { - attesting_indices: vec![0].into(), + attesting_indices: vec![0].try_into().unwrap(), data: AttestationData { slot: Slot::new(0), index: 0, @@ -779,42 +764,47 @@ async fn invalid_signature_attester_slashing() { .clone() .deconstruct(); match &mut block.body_mut() { - BeaconBlockBodyRefMut::Base(ref mut blk) => { + BeaconBlockBodyRefMut::Base(blk) => { blk.attester_slashings .push(attester_slashing.as_base().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Altair(ref mut blk) => { + BeaconBlockBodyRefMut::Altair(blk) => { blk.attester_slashings .push(attester_slashing.as_base().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Bellatrix(ref mut blk) => { + BeaconBlockBodyRefMut::Bellatrix(blk) => { blk.attester_slashings .push(attester_slashing.as_base().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Capella(ref mut blk) => { + BeaconBlockBodyRefMut::Capella(blk) => { blk.attester_slashings .push(attester_slashing.as_base().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Deneb(ref mut blk) => { + BeaconBlockBodyRefMut::Deneb(blk) => { blk.attester_slashings .push(attester_slashing.as_base().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Electra(ref mut blk) => { + BeaconBlockBodyRefMut::Electra(blk) => { blk.attester_slashings .push(attester_slashing.as_electra().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Eip7805(ref mut blk) => { + BeaconBlockBodyRefMut::Fulu(blk) => { blk.attester_slashings .push(attester_slashing.as_electra().unwrap().clone()) .expect("should update attester slashing"); } - BeaconBlockBodyRefMut::Fulu(ref mut blk) => { + BeaconBlockBodyRefMut::Eip7805(blk) => { + blk.attester_slashings + .push(attester_slashing.as_electra().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Gloas(blk) => { blk.attester_slashings .push(attester_slashing.as_electra().unwrap().clone()) .expect("should update attester slashing"); @@ -850,35 +840,39 @@ async fn invalid_signature_attestation() { .clone() .deconstruct(); match &mut block.body_mut() { - BeaconBlockBodyRefMut::Base(ref mut blk) => blk + BeaconBlockBodyRefMut::Base(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Altair(ref mut blk) => blk + BeaconBlockBodyRefMut::Altair(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Bellatrix(ref mut blk) => blk + BeaconBlockBodyRefMut::Bellatrix(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Capella(ref mut blk) => blk + BeaconBlockBodyRefMut::Capella(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Deneb(ref mut blk) => blk + BeaconBlockBodyRefMut::Deneb(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Electra(ref mut blk) => blk + BeaconBlockBodyRefMut::Electra(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Eip7805(ref mut blk) => blk + BeaconBlockBodyRefMut::Fulu(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), - BeaconBlockBodyRefMut::Fulu(ref mut blk) => blk + BeaconBlockBodyRefMut::Eip7805(blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Gloas(blk) => blk .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), @@ -916,7 +910,9 @@ async fn invalid_signature_deposit() { let harness = get_invalid_sigs_harness(&chain_segment).await; let mut snapshots = chain_segment.clone(); let deposit = Deposit { - proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1].into(), + proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1] + .try_into() + .unwrap(), data: DepositData { pubkey: Keypair::random().pk.into(), withdrawal_credentials: Hash256::zero(), @@ -941,9 +937,7 @@ async fn invalid_signature_deposit() { let blocks: Vec> = snapshots .iter() .zip(chain_segment_blobs.iter()) - .map(|(snapshot, blobs)| { - build_rpc_block(snapshot.beacon_block.clone(), blobs, &harness.spec) - }) + .map(|(snapshot, blobs)| build_rpc_block(snapshot.beacon_block.clone(), blobs)) .collect(); assert!( !matches!( @@ -1007,11 +1001,10 @@ fn unwrap_err(result: Result) -> U { #[tokio::test] async fn block_gossip_verification() { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let block_index = CHAIN_SEGMENT_LENGTH - 2; - let cgc = harness.chain.spec.custody_requirement as usize; harness .chain @@ -1025,7 +1018,7 @@ async fn block_gossip_verification() { { let gossip_verified = harness .chain - .verify_block_for_gossip(snapshot.beacon_block.clone(), get_cgc(&blobs_opt)) + .verify_block_for_gossip(snapshot.beacon_block.clone()) .await .expect("should obtain gossip verified block"); @@ -1067,7 +1060,7 @@ async fn block_gossip_verification() { *block.slot_mut() = expected_block_slot; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), BlockError::FutureSlot { present_slot, block_slot, @@ -1101,7 +1094,7 @@ async fn block_gossip_verification() { *block.slot_mut() = expected_finalized_slot; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), BlockError::WouldRevertFinalizedSlot { block_slot, finalized_slot, @@ -1131,10 +1124,10 @@ async fn block_gossip_verification() { unwrap_err( harness .chain - .verify_block_for_gossip( - Arc::new(SignedBeaconBlock::from_block(block, junk_signature())), - cgc - ) + .verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block( + block, + junk_signature() + )),) .await ), BlockError::InvalidSignature(InvalidSignature::ProposerSignature) @@ -1159,7 +1152,7 @@ async fn block_gossip_verification() { *block.parent_root_mut() = parent_root; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), BlockError::ParentUnknown {parent_root: p} if p == parent_root ), @@ -1185,7 +1178,7 @@ async fn block_gossip_verification() { *block.parent_root_mut() = parent_root; assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature)), cgc).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), BlockError::NotFinalizedDescendant { block_parent_root } if block_parent_root == parent_root ), @@ -1222,7 +1215,7 @@ async fn block_gossip_verification() { ); assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()), cgc).await), + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), BlockError::IncorrectBlockProposer { block, local_shuffling, @@ -1234,7 +1227,12 @@ async fn block_gossip_verification() { // Check to ensure that we registered this is a valid block from this proposer. assert!( matches!( - unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone()), cgc).await), + unwrap_err( + harness + .chain + .verify_block_for_gossip(Arc::new(block.clone())) + .await + ), BlockError::DuplicateImportStatusUnknown(_), ), "should register any valid signature against the proposer, even if the block failed later verification" @@ -1242,11 +1240,7 @@ async fn block_gossip_verification() { let block = chain_segment[block_index].beacon_block.clone(); assert!( - harness - .chain - .verify_block_for_gossip(block, cgc) - .await - .is_ok(), + harness.chain.verify_block_for_gossip(block).await.is_ok(), "the valid block should be processed" ); @@ -1264,13 +1258,47 @@ async fn block_gossip_verification() { matches!( harness .chain - .verify_block_for_gossip(block.clone(), cgc) + .verify_block_for_gossip(block.clone()) .await .expect_err("should error when processing known block"), BlockError::DuplicateImportStatusUnknown(_) ), "the second proposal by this validator should be rejected" ); + + /* + * This test ensures that: + * + * We do not accept blocks with blob_kzg_commitments length larger than the max_blobs for that epoch. + */ + let (mut block, signature) = chain_segment[block_index] + .beacon_block + .as_ref() + .clone() + .deconstruct(); + + let kzg_commitments_len = harness + .chain + .spec + .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) + as usize; + + if let Ok(kzg_commitments) = block.body_mut().blob_kzg_commitments_mut() { + *kzg_commitments = vec![KzgCommitment::empty_for_testing(); kzg_commitments_len + 1] + .try_into() + .unwrap(); + assert!( + matches!( + unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), + BlockError::InvalidBlobCount { + max_blobs_at_epoch, + block, + } + if max_blobs_at_epoch == kzg_commitments_len && block == kzg_commitments_len + 1 + ), + "should not import a block with higher blob_kzg_commitment length than the max_blobs at epoch" + ); + } } async fn verify_and_process_gossip_data_sidecars( @@ -1303,7 +1331,7 @@ async fn verify_and_process_gossip_data_sidecars( ); harness.chain.verify_data_column_sidecar_for_gossip( column_sidecar.into_inner(), - *subnet_id, + subnet_id, ) }) .collect::, _>>() @@ -1340,17 +1368,8 @@ async fn verify_block_for_gossip_slashing_detection() { let state = harness.get_current_state(); let ((block1, blobs1), _) = harness.make_block(state.clone(), Slot::new(1)).await; let ((block2, _blobs2), _) = harness.make_block(state, Slot::new(1)).await; - let cgc = if block1.fork_name_unchecked().fulu_enabled() { - harness.get_sampling_column_count() - } else { - 0 - }; - let verified_block = harness - .chain - .verify_block_for_gossip(block1, cgc) - .await - .unwrap(); + let verified_block = harness.chain.verify_block_for_gossip(block1).await.unwrap(); if let Some((kzg_proofs, blobs)) = blobs1 { harness @@ -1373,7 +1392,7 @@ async fn verify_block_for_gossip_slashing_detection() { ) .await .unwrap(); - unwrap_err(harness.chain.verify_block_for_gossip(block2, CGC).await); + unwrap_err(harness.chain.verify_block_for_gossip(block2).await); // Slasher should have been handed the two conflicting blocks and crafted a slashing. slasher.process_queued(Epoch::new(0)).unwrap(); @@ -1387,7 +1406,7 @@ async fn verify_block_for_gossip_slashing_detection() { #[tokio::test] async fn verify_block_for_gossip_doppelganger_detection() { - let harness = get_harness(VALIDATOR_COUNT); + let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let state = harness.get_current_state(); let ((block, _), _) = harness.make_block(state.clone(), Slot::new(1)).await; @@ -1397,11 +1416,7 @@ async fn verify_block_for_gossip_doppelganger_detection() { .attestations() .map(|att| att.clone_as_attestation()) .collect::>(); - let verified_block = harness - .chain - .verify_block_for_gossip(block, CGC) - .await - .unwrap(); + let verified_block = harness.chain.verify_block_for_gossip(block).await.unwrap(); harness .chain .process_block( @@ -1437,24 +1452,30 @@ async fn verify_block_for_gossip_doppelganger_detection() { assert!(harness.chain.validator_seen_at_epoch(index, epoch)); // Check the correct beacon cache is populated - assert!(harness - .chain - .observed_block_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if block attester was observed")); - assert!(!harness - .chain - .observed_gossip_attesters - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip attester was observed")); - assert!(!harness - .chain - .observed_aggregators - .read() - .validator_has_been_observed(epoch, index) - .expect("should check if gossip aggregator was observed")); + assert!( + harness + .chain + .observed_block_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if block attester was observed") + ); + assert!( + !harness + .chain + .observed_gossip_attesters + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip attester was observed") + ); + assert!( + !harness + .chain + .observed_aggregators + .read() + .validator_has_been_observed(epoch, index) + .expect("should check if gossip aggregator was observed") + ); } } } @@ -1548,7 +1569,7 @@ async fn add_base_block_to_altair_chain() { assert!(matches!( harness .chain - .verify_block_for_gossip(Arc::new(base_block.clone()), CGC) + .verify_block_for_gossip(Arc::new(base_block.clone())) .await .expect_err("should error when processing base block"), BlockError::InconsistentFork(InconsistentFork { @@ -1558,7 +1579,7 @@ async fn add_base_block_to_altair_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. - let base_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(base_block.clone()), 0); + let base_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(base_block.clone())); assert!(matches!( harness .chain @@ -1582,7 +1603,7 @@ async fn add_base_block_to_altair_chain() { harness .chain .process_chain_segment( - vec![RpcBlock::new_without_blobs(None, Arc::new(base_block), 0)], + vec![RpcBlock::new_without_blobs(None, Arc::new(base_block))], NotifyExecutionLayer::Yes, ) .await, @@ -1685,7 +1706,7 @@ async fn add_altair_block_to_base_chain() { assert!(matches!( harness .chain - .verify_block_for_gossip(Arc::new(altair_block.clone()), CGC) + .verify_block_for_gossip(Arc::new(altair_block.clone())) .await .expect_err("should error when processing altair block"), BlockError::InconsistentFork(InconsistentFork { @@ -1695,7 +1716,7 @@ async fn add_altair_block_to_base_chain() { )); // Ensure that it would be impossible to import via `BeaconChain::process_block`. - let altair_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(altair_block.clone()), 0); + let altair_rpc_block = RpcBlock::new_without_blobs(None, Arc::new(altair_block.clone())); assert!(matches!( harness .chain @@ -1719,7 +1740,7 @@ async fn add_altair_block_to_base_chain() { harness .chain .process_chain_segment( - vec![RpcBlock::new_without_blobs(None, Arc::new(altair_block), 0)], + vec![RpcBlock::new_without_blobs(None, Arc::new(altair_block))], NotifyExecutionLayer::Yes ) .await, @@ -1733,6 +1754,8 @@ async fn add_altair_block_to_base_chain() { )); } +// This is a regression test for this bug: +// https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] async fn import_duplicate_block_unrealized_justification() { let spec = MainnetEthSpec::default_spec(); @@ -1780,11 +1803,7 @@ async fn import_duplicate_block_unrealized_justification() { // Create two verified variants of the block, representing the same block being processed in // parallel. let notify_execution_layer = NotifyExecutionLayer::Yes; - let rpc_block = RpcBlock::new_without_blobs( - Some(block_root), - block.clone(), - harness.sampling_column_count, - ); + let rpc_block = RpcBlock::new_without_blobs(Some(block_root), block.clone()); let verified_block1 = rpc_block .clone() .into_execution_pending_block(block_root, chain, notify_execution_layer) @@ -1798,7 +1817,7 @@ async fn import_duplicate_block_unrealized_justification() { .await .unwrap(); - // Unrealized justification should NOT have updated. + // The store's global unrealized justification should update immediately and match the block. let unrealized_justification = { let fc = chain.canonical_head.fork_choice_read_lock(); assert_eq!(fc.justified_checkpoint().epoch, 0); @@ -1815,9 +1834,12 @@ async fn import_duplicate_block_unrealized_justification() { }; // Import the second verified block, simulating a block processed via RPC. - import_execution_pending_block(chain.clone(), verified_block2) - .await - .unwrap(); + assert_eq!( + import_execution_pending_block(chain.clone(), verified_block2) + .await + .unwrap_err(), + format!("DuplicateFullyImported({block_root})") + ); // Unrealized justification should still be updated. let fc3 = chain.canonical_head.fork_choice_read_lock(); @@ -1855,14 +1877,3 @@ async fn import_execution_pending_block( } } } - -fn get_cgc(blobs_opt: &Option>) -> usize { - if let Some(data_sidecars) = blobs_opt.as_ref() { - match data_sidecars { - DataSidecars::Blobs(_) => 0, - DataSidecars::DataColumns(d) => d.len(), - } - } else { - 0 - } -} diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs new file mode 100644 index 0000000000..be9b3b2fa1 --- /dev/null +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -0,0 +1,118 @@ +#![cfg(not(debug_assertions))] + +use beacon_chain::custody_context::NodeCustodyType; +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + generate_data_column_sidecars_from_block, test_spec, +}; +use beacon_chain::{ + AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, + block_verification_types::AsBlock, +}; +use bls::{Keypair, Signature}; +use logging::create_test_tracing_subscriber; +use std::sync::{Arc, LazyLock}; +use types::*; + +type E = MainnetEthSpec; + +// Should ideally be divisible by 3. +const VALIDATOR_COUNT: usize = 24; + +/// A cached set of keys. +static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); + +fn get_harness( + validator_count: usize, + spec: Arc, + node_custody_type: NodeCustodyType, +) -> BeaconChainHarness> { + create_test_tracing_subscriber(); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .node_custody_type(node_custody_type) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + harness +} + +// Regression test for https://github.com/sigp/lighthouse/issues/7650 +#[tokio::test] +async fn rpc_columns_with_invalid_header_signature() { + let spec = Arc::new(test_spec::()); + + // Only run this test if columns are enabled. + if !spec.is_fulu_scheduled() { + return; + } + + let harness = get_harness(VALIDATOR_COUNT, spec, NodeCustodyType::Supernode); + + let num_blocks = E::slots_per_epoch() as usize; + + // Add some chain depth. + harness + .extend_chain( + num_blocks, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block with blobs. + harness.execution_block_generator().set_min_blob_count(1); + let head_state = harness.get_current_state(); + let slot = head_state.slot() + 1; + let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await; + let (_, blobs) = opt_blobs.unwrap(); + assert!(!blobs.is_empty()); + let block_root = signed_block.canonical_root(); + + // Process the block without blobs so that it doesn't become available. + harness.advance_slot(); + let rpc_block = harness + .build_rpc_block_from_blobs(block_root, signed_block.clone(), None) + .unwrap(); + let availability = harness + .chain + .process_block( + block_root, + rpc_block, + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) + .await + .unwrap(); + assert_eq!( + availability, + AvailabilityProcessingStatus::MissingComponents(slot, block_root) + ); + + // Build blob sidecars with invalid signatures in the block header. + let mut corrupt_block = (*signed_block).clone(); + *corrupt_block.signature_mut() = Signature::infinity().unwrap(); + + let data_column_sidecars = + generate_data_column_sidecars_from_block(&corrupt_block, &harness.chain.spec); + + let err = harness + .chain + .process_rpc_custody_columns(data_column_sidecars) + .await + .unwrap_err(); + assert!(matches!( + err, + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) + )); +} diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index c9bd55e062..86bdb03daf 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,18 +1,26 @@ use beacon_chain::blob_verification::GossipVerifiedBlob; -use beacon_chain::test_utils::BeaconChainHarness; -use eth2::types::{EventKind, SseBlobSidecar}; -use rand::rngs::StdRng; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; +use beacon_chain::test_utils::{ + BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, +}; +use eth2::types::{EventKind, SseBlobSidecar, SseDataColumnSidecar}; use rand::SeedableRng; +use rand::rngs::StdRng; use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; -use types::{BlobSidecar, EthSpec, ForkName, MinimalEthSpec}; +use types::test_utils::TestRandom; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, MinimalEthSpec, Slot}; type E = MinimalEthSpec; /// Verifies that a blob event is emitted when a gossip verified blob is received via gossip or the publish block API. #[tokio::test] async fn blob_sidecar_event_on_process_gossip_blob() { - let spec = Arc::new(ForkName::Deneb.make_genesis_spec(E::default_spec())); + if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { + return; + }; + + let spec = Arc::new(test_spec::()); let harness = BeaconChainHarness::builder(E::default()) .spec(spec) .deterministic_keypairs(8) @@ -43,10 +51,63 @@ async fn blob_sidecar_event_on_process_gossip_blob() { assert_eq!(sidecar_event, EventKind::BlobSidecar(expected_sse_blobs)); } +/// Verifies that a data column event is emitted when a gossip verified data column is received via gossip or the publish block API. +#[tokio::test] +async fn data_column_sidecar_event_on_process_gossip_data_column() { + if fork_name_from_env().is_some_and(|f| !f.fulu_enabled()) { + return; + }; + + let spec = Arc::new(test_spec::()); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // subscribe to blob sidecar events + let event_handler = harness.chain.event_handler.as_ref().unwrap(); + let mut data_column_event_receiver = event_handler.subscribe_data_column_sidecar(); + + // build and process a gossip verified data column + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let sidecar = { + // DA checker only accepts sampling columns, so we need to create one with a sampling index. + let mut random_sidecar = DataColumnSidecar::random_for_test(&mut rng); + let slot = Slot::new(10); + let epoch = slot.epoch(E::slots_per_epoch()); + random_sidecar.signed_block_header.message.slot = slot; + random_sidecar.index = harness.chain.sampling_columns_for_epoch(epoch)[0]; + random_sidecar + }; + let gossip_verified_data_column = + GossipVerifiedDataColumn::__new_for_testing(Arc::new(sidecar)); + let expected_sse_data_column = SseDataColumnSidecar::from_data_column_sidecar( + gossip_verified_data_column.as_data_column(), + ); + + let _ = harness + .chain + .process_gossip_data_columns(vec![gossip_verified_data_column], || Ok(())) + .await + .unwrap(); + + let sidecar_event = data_column_event_receiver.try_recv().unwrap(); + assert_eq!( + sidecar_event, + EventKind::DataColumnSidecar(expected_sse_data_column) + ); +} + /// Verifies that a blob event is emitted when blobs are received via RPC. #[tokio::test] async fn blob_sidecar_event_on_process_rpc_blobs() { - let spec = Arc::new(ForkName::Deneb.make_genesis_spec(E::default_spec())); + if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { + return; + }; + + let spec = Arc::new(test_spec::()); let harness = BeaconChainHarness::builder(E::default()) .spec(spec) .deterministic_keypairs(8) @@ -59,19 +120,18 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { let mut blob_event_receiver = event_handler.subscribe_blob_sidecar(); // build and process multiple rpc blobs - let kzg = harness.chain.kzg.as_ref(); - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + harness.execution_block_generator().set_min_blob_count(2); - let mut blob_1 = BlobSidecar::random_valid(&mut rng, kzg).unwrap(); - let mut blob_2 = BlobSidecar { - index: 1, - ..BlobSidecar::random_valid(&mut rng, kzg).unwrap() - }; - let parent_root = harness.chain.head().head_block_root(); - blob_1.signed_block_header.message.parent_root = parent_root; - blob_2.signed_block_header.message.parent_root = parent_root; - let blob_1 = Arc::new(blob_1); - let blob_2 = Arc::new(blob_2); + let head_state = harness.get_current_state(); + let slot = head_state.slot() + 1; + let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await; + let (kzg_proofs, blobs) = opt_blobs.unwrap(); + assert_eq!(blobs.len(), 2); + + let blob_1 = + Arc::new(BlobSidecar::new(0, blobs[0].clone(), &signed_block, kzg_proofs[0]).unwrap()); + let blob_2 = + Arc::new(BlobSidecar::new(1, blobs[1].clone(), &signed_block, kzg_proofs[1]).unwrap()); let blobs = FixedBlobSidecarList::new(vec![Some(blob_1.clone()), Some(blob_2.clone())]); let expected_sse_blobs = vec![ @@ -81,7 +141,7 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { let _ = harness .chain - .process_rpc_blobs(blob_1.slot(), blob_1.block_root(), blobs) + .process_rpc_blobs(slot, blob_1.block_root(), blobs) .await .unwrap(); @@ -95,3 +155,49 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { } assert_eq!(sse_blobs, expected_sse_blobs); } + +#[tokio::test] +async fn data_column_sidecar_event_on_process_rpc_columns() { + if fork_name_from_env().is_some_and(|f| !f.fulu_enabled()) { + return; + }; + + let spec = Arc::new(test_spec::()); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.clone()) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // subscribe to blob sidecar events + let event_handler = harness.chain.event_handler.as_ref().unwrap(); + let mut data_column_event_receiver = event_handler.subscribe_data_column_sidecar(); + + // build a valid block + harness.execution_block_generator().set_min_blob_count(1); + + let head_state = harness.get_current_state(); + let slot = head_state.slot() + 1; + let ((signed_block, opt_blobs), _) = harness.make_block(head_state, slot).await; + let (_, blobs) = opt_blobs.unwrap(); + assert!(!blobs.is_empty()); + + // load the precomputed column sidecar to avoid computing them for every block in the tests. + let data_column_sidecars = + generate_data_column_sidecars_from_block(&signed_block, &harness.chain.spec); + let sidecar = data_column_sidecars[0].clone(); + let expected_sse_data_column = SseDataColumnSidecar::from_data_column_sidecar(&sidecar); + + let _ = harness + .chain + .process_rpc_custody_columns(vec![sidecar]) + .await + .unwrap(); + + let sidecar_event = data_column_event_receiver.try_recv().unwrap(); + assert_eq!( + sidecar_event, + EventKind::DataColumnSidecar(expected_sse_data_column) + ); +} diff --git a/beacon_node/beacon_chain/tests/inclusion_list_verification.rs b/beacon_node/beacon_chain/tests/inclusion_list_verification.rs index 91dffaae91..134a16b66d 100644 --- a/beacon_node/beacon_chain/tests/inclusion_list_verification.rs +++ b/beacon_node/beacon_chain/tests/inclusion_list_verification.rs @@ -1,13 +1,13 @@ use std::sync::{Arc, LazyLock}; use beacon_chain::{ + BeaconChainTypes, ChainConfig, inclusion_list_verification::GossipInclusionListError, test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, - BeaconChainTypes, ChainConfig, }; -use bls::{generics::GenericSignature, PublicKeyBytes, SecretKey}; +use bls::{PublicKeyBytes, SecretKey, Keypair, generics::GenericSignature}; use types::{ - ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, InclusionList, Keypair, MainnetEthSpec, + ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, InclusionList, MainnetEthSpec, SignedInclusionList, SignedRoot, Slot, }; @@ -260,7 +260,9 @@ async fn inclusion_list_verification() { .inspect_inclusion_list_err( "inclusion list with too many transactions", |_, il| { - il.message.transactions = vec![vec![0u8; 5].into(); 8193].into(); + il.message.transactions = vec![vec![0u8; 5].try_into().unwrap(); 8193] + .try_into() + .unwrap(); }, |_, error| { assert!(matches!( diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index c1a0414581..66b8b85541 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -1,13 +1,16 @@ mod attestation_production; mod attestation_verification; mod bellatrix; +mod blob_verification; mod block_verification; mod capella; +mod column_verification; mod events; mod inclusion_list_verification; mod op_verification; mod payload_invalidation; mod rewards; +mod schema_stability; mod store_tests; mod sync_committee_verification; mod tests; diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index 86ab0cce80..2f97f10745 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -3,19 +3,20 @@ #![cfg(not(debug_assertions))] use beacon_chain::{ + BeaconChainError, observed_operations::ObservationOutcome, test_utils::{ - test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, + AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, test_spec, }, - BeaconChainError, }; +use bls::Keypair; use state_processing::per_block_processing::errors::{ AttesterSlashingInvalid, BlockOperationError, ExitInvalid, ProposerSlashingInvalid, }; use std::sync::{Arc, LazyLock}; -use store::database::interface::BeaconNodeBackend; use store::StoreConfig; -use tempfile::{tempdir, TempDir}; +use store::database::interface::BeaconNodeBackend; +use tempfile::{TempDir, tempdir}; use types::*; pub const VALIDATOR_COUNT: usize = 24; diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 6b9ff9d6ed..5bd43835e3 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -2,15 +2,15 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{ + BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, + INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, OverrideForkchoiceUpdate, + StateSkipConfig, WhenSlotSkipped, canonical_head::{CachedHead, CanonicalHead}, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, NotifyExecutionLayer, - OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped, - INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, }; use execution_layer::{ - json_structures::{JsonForkchoiceStateV1, JsonPayloadAttributes, JsonPayloadAttributesV1}, ExecutionLayer, ForkchoiceState, PayloadAttributes, + json_structures::{JsonForkchoiceStateV1, JsonPayloadAttributes, JsonPayloadAttributesV1}, }; use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Error as ProtoArrayError, ExecutionStatus}; @@ -22,7 +22,6 @@ use task_executor::ShutdownReason; use types::*; const VALIDATOR_COUNT: usize = 32; -const CGC: usize = 8; type E = MainnetEthSpec; @@ -686,8 +685,7 @@ async fn invalidates_all_descendants() { assert_eq!(fork_parent_state.slot(), fork_parent_slot); let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; - let fork_rpc_block = - RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); + let fork_rpc_block = RpcBlock::new_without_blobs(None, fork_block.clone()); let fork_block_root = rig .harness .chain @@ -789,8 +787,7 @@ async fn switches_heads() { let ((fork_block, _), _fork_post_state) = rig.harness.make_block(fork_parent_state, fork_slot).await; let fork_parent_root = fork_block.parent_root(); - let fork_rpc_block = - RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); + let fork_rpc_block = RpcBlock::new_without_blobs(None, fork_block.clone()); let fork_block_root = rig .harness .chain @@ -825,9 +822,10 @@ async fn switches_heads() { assert_eq!(rig.harness.head_block_root(), fork_parent_root); // The fork block has not yet been validated. - assert!(rig - .execution_status(fork_block_root) - .is_optimistic_or_invalid()); + assert!( + rig.execution_status(fork_block_root) + .is_optimistic_or_invalid() + ); for root in blocks { let slot = rig @@ -875,12 +873,13 @@ async fn invalid_during_processing() { ]; // 0 should be present in the chain. - assert!(rig - .harness - .chain - .get_blinded_block(&roots[0]) - .unwrap() - .is_some()); + assert!( + rig.harness + .chain + .get_blinded_block(&roots[0]) + .unwrap() + .is_some() + ); // 1 should *not* be present in the chain. assert_eq!( rig.harness.chain.get_blinded_block(&roots[1]).unwrap(), @@ -1054,14 +1053,13 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for gossip. assert!(matches!( - rig.harness.chain.clone().verify_block_for_gossip(block.clone(), CGC).await, + rig.harness.chain.clone().verify_block_for_gossip(block.clone()).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); // Ensure the block built atop an invalid payload is invalid for import. - let rpc_block = - RpcBlock::new_without_blobs(None, block.clone(), rig.harness.sampling_column_count); + let rpc_block = RpcBlock::new_without_blobs(None, block.clone()); assert!(matches!( rig.harness.chain.process_block(rpc_block.block_root(), rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -1197,10 +1195,10 @@ async fn attesting_to_optimistic_head() { .unwrap(); match &mut attestation { - Attestation::Base(ref mut att) => { + Attestation::Base(att) => { att.aggregation_bits.set(0, true).unwrap(); } - Attestation::Electra(ref mut att) => { + Attestation::Electra(att) => { att.aggregation_bits.set(0, true).unwrap(); } } @@ -1358,11 +1356,12 @@ impl InvalidHeadSetup { // head block as invalid should not result in another head being chosen. // Rather, it should fail to run fork choice and leave the invalid block as // the head. - assert!(rig - .canonical_head() - .head_execution_status() - .unwrap() - .is_invalid()); + assert!( + rig.canonical_head() + .head_execution_status() + .unwrap() + .is_invalid() + ); // Ensure that we're getting the correct error when trying to find a new // head. @@ -1385,8 +1384,7 @@ async fn recover_from_invalid_head_by_importing_blocks() { } = InvalidHeadSetup::new().await; // Import the fork block, it should become the head. - let fork_rpc_block = - RpcBlock::new_without_blobs(None, fork_block.clone(), rig.harness.sampling_column_count); + let fork_rpc_block = RpcBlock::new_without_blobs(None, fork_block.clone()); rig.harness .chain .process_block( @@ -1516,7 +1514,12 @@ async fn weights_after_resetting_optimistic_status() { .fork_choice_read_lock() .get_block_weight(&head.head_block_root()) .unwrap(), - head.snapshot.beacon_state.validators().get(0).unwrap().effective_balance, + head.snapshot + .beacon_state + .validators() + .get(0) + .unwrap() + .effective_balance, "proposer boost should be removed from the head block and the vote of a single validator applied" ); diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index fa2d028f22..ee9cf511ea 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -2,13 +2,14 @@ use beacon_chain::block_verification_types::AsBlock; use beacon_chain::test_utils::{ - generate_deterministic_keypairs, BeaconChainHarness, EphemeralHarnessType, + BeaconChainHarness, EphemeralHarnessType, generate_deterministic_keypairs, }; use beacon_chain::{ - test_utils::{AttestationStrategy, BlockStrategy, RelativeSyncCommittee}, - types::{Epoch, EthSpec, Keypair, MinimalEthSpec}, BlockError, ChainConfig, StateSkipConfig, WhenSlotSkipped, + test_utils::{AttestationStrategy, BlockStrategy, RelativeSyncCommittee}, + types::{Epoch, EthSpec, MinimalEthSpec}, }; +use bls::Keypair; use eth2::types::{StandardAttestationRewards, TotalAttestationRewards, ValidatorId}; use state_processing::{BlockReplayError, BlockReplayer}; use std::array::IntoIter; @@ -424,9 +425,11 @@ async fn test_rewards_altair() { .unwrap(); // assert ideal rewards are greater than 0 - assert!(ideal_rewards - .iter() - .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + assert!( + ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0) + ); // apply attestation, proposal, and sync committee rewards and penalties to initial balances apply_attestation_rewards(&mut expected_balances, total_rewards); @@ -507,12 +510,16 @@ async fn test_rewards_altair_inactivity_leak() { // assert inactivity penalty for both ideal rewards and individual validators assert!(ideal_rewards.iter().all(|reward| reward.inactivity == 0)); - assert!(total_rewards[..half] - .iter() - .all(|reward| reward.inactivity == 0)); - assert!(total_rewards[half..] - .iter() - .all(|reward| reward.inactivity < 0)); + assert!( + total_rewards[..half] + .iter() + .all(|reward| reward.inactivity == 0) + ); + assert!( + total_rewards[half..] + .iter() + .all(|reward| reward.inactivity < 0) + ); // apply attestation, proposal, and sync committee rewards and penalties to initial balances apply_attestation_rewards(&mut expected_balances, total_rewards); @@ -612,9 +619,11 @@ async fn test_rewards_altair_inactivity_leak_justification_epoch() { .unwrap(); // assert ideal rewards are greater than 0 - assert!(ideal_rewards - .iter() - .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + assert!( + ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0) + ); // apply attestation, proposal, and sync committee rewards and penalties to initial balances apply_attestation_rewards(&mut expected_balances, total_rewards); @@ -688,9 +697,11 @@ async fn test_rewards_electra() { ideal_rewards.len() as u64, spec.max_effective_balance_electra / spec.effective_balance_increment ); - assert!(ideal_rewards - .iter() - .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + assert!( + ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0) + ); // apply attestation, proposal, and sync committee rewards and penalties to initial balances apply_attestation_rewards(&mut expected_balances, total_rewards); @@ -776,9 +787,11 @@ async fn check_all_electra_rewards( harness.spec.max_effective_balance_electra / harness.spec.effective_balance_increment ); - assert!(ideal_rewards - .iter() - .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + assert!( + ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0) + ); // apply attestation, proposal, and sync committee rewards and penalties to initial balances apply_attestation_rewards(&mut balances, total_rewards); diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs new file mode 100644 index 0000000000..db7f7dbdbb --- /dev/null +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -0,0 +1,175 @@ +use beacon_chain::{ + ChainConfig, + persisted_beacon_chain::PersistedBeaconChain, + persisted_custody::PersistedCustody, + test_utils::{BeaconChainHarness, DiskHarnessType, test_spec}, +}; +use bls::Keypair; +use logging::create_test_tracing_subscriber; +use operation_pool::PersistedOperationPool; +use ssz::Encode; +use std::sync::{Arc, LazyLock}; +use store::{ + DBColumn, HotColdDB, StoreConfig, StoreItem, + database::interface::BeaconNodeBackend, + hot_cold_store::Split, + metadata::{DataColumnCustodyInfo, DataColumnInfo}, +}; +use strum::IntoEnumIterator; +use tempfile::{TempDir, tempdir}; +use types::{ChainSpec, Hash256, MainnetEthSpec, Slot}; + +type E = MainnetEthSpec; +type Store = Arc, BeaconNodeBackend>>; +type TestHarness = BeaconChainHarness>; + +const VALIDATOR_COUNT: usize = 32; + +/// A cached set of keys. +static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); + +fn get_store(db_path: &TempDir, config: StoreConfig, spec: Arc) -> Store { + create_test_tracing_subscriber(); + let hot_path = db_path.path().join("chain_db"); + let cold_path = db_path.path().join("freezer_db"); + let blobs_path = db_path.path().join("blobs_db"); + + HotColdDB::open( + &hot_path, + &cold_path, + &blobs_path, + |_, _, _| Ok(()), + config, + spec, + ) + .expect("disk store should initialize") +} + +/// This test checks the database schema stability against previous versions of Lighthouse's code. +/// +/// If you are changing something about how Lighthouse stores data on disk, you almost certainly +/// need to implement a database schema change. This is true even if the data being stored only +/// applies to an upcoming fork that isn't live on mainnet. We never want to be in the situation +/// where commit A writes data in some format, and then a later commit B changes that format +/// without a schema change. This is liable to break any nodes that update from A to B, even if +/// these nodes are just testnet nodes. +/// +/// This test implements partial, imperfect checks on the DB schema which are designed to quickly +/// catch common changes. +/// +/// This test uses hardcoded values, rather than trying to access previous versions of Lighthouse's +/// code. If you've successfully implemented a schema change and you're sure that the new values are +/// correct, you can update the hardcoded values here. +#[tokio::test] +async fn schema_stability() { + let spec = Arc::new(test_spec::()); + + let datadir = tempdir().unwrap(); + let store_config = StoreConfig::default(); + let store = get_store(&datadir, store_config, spec.clone()); + + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }; + + let harness = TestHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS.to_vec()) + .fresh_disk_store(store.clone()) + .mock_execution_layer() + .chain_config(chain_config) + .build(); + harness.advance_slot(); + + let chain = &harness.chain; + + chain.persist_op_pool().unwrap(); + chain.persist_custody_context().unwrap(); + insert_data_column_custody_info(&store, &harness.spec); + + check_db_columns(); + check_metadata_sizes(&store); + check_op_pool(&store); + check_custody_context(&store, &harness.spec); + check_custody_info(&store, &harness.spec); + check_persisted_chain(&store); + + // Not covered here: + // - Fork choice (not tested) + // - DBColumn::DhtEnrs (tested in network crate) +} + +/// Check that the set of database columns is unchanged. +fn check_db_columns() { + let current_columns: Vec<&'static str> = DBColumn::iter().map(|c| c.as_str()).collect(); + let expected_columns = vec![ + "bma", "blk", "blb", "bdc", "bdi", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", + "bst", "exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", + "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", + ]; + assert_eq!(expected_columns, current_columns); +} + +fn insert_data_column_custody_info(store: &Store, spec: &ChainSpec) { + if spec.is_peer_das_scheduled() { + store + .put_data_column_custody_info(Some(Slot::new(0))) + .unwrap(); + } +} + +/// Check the SSZ sizes of known on-disk metadata. +/// +/// New types can be added here as the schema evolves. +fn check_metadata_sizes(store: &Store) { + assert_eq!(Split::default().ssz_bytes_len(), 40); + assert_eq!(store.get_anchor_info().ssz_bytes_len(), 64); + assert_eq!( + store.get_blob_info().ssz_bytes_len(), + if store.get_chain_spec().deneb_fork_epoch.is_some() { + 14 + } else { + 6 + } + ); + assert_eq!(DataColumnInfo::default().ssz_bytes_len(), 5); + assert_eq!(DataColumnCustodyInfo::default().ssz_bytes_len(), 5); +} + +fn check_op_pool(store: &Store) { + let op_pool = store + .get_item::>(&Hash256::ZERO) + .unwrap() + .unwrap(); + assert!(matches!(op_pool, PersistedOperationPool::V20(_))); + assert_eq!(op_pool.ssz_bytes_len(), 28); + assert_eq!(op_pool.as_store_bytes().len(), 28); +} + +fn check_custody_context(store: &Store, spec: &ChainSpec) { + let custody_context_opt = store.get_item::(&Hash256::ZERO).unwrap(); + if spec.is_peer_das_scheduled() { + assert_eq!(custody_context_opt.unwrap().as_store_bytes().len(), 13); + } else { + assert!(custody_context_opt.is_none()); + } +} + +fn check_custody_info(store: &Store, spec: &ChainSpec) { + let data_column_custody_info = store.get_data_column_custody_info().unwrap(); + if spec.is_peer_das_scheduled() { + assert_eq!(data_column_custody_info.unwrap().as_ssz_bytes().len(), 13); + } else { + assert!(data_column_custody_info.is_none()); + } +} + +fn check_persisted_chain(store: &Store) { + let chain = store + .get_item::(&Hash256::ZERO) + .unwrap() + .unwrap(); + assert_eq!(chain.as_store_bytes().len(), 32); +} diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 3343dc101b..ba0621ae72 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3,36 +3,52 @@ use beacon_chain::attestation_verification::Error as AttnError; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::BeaconChainBuilder; +use beacon_chain::custody_context::CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS; use beacon_chain::data_availability_checker::AvailableBlock; +use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::schema_change::migrate_schema; -use beacon_chain::test_utils::SyncCommitteeStrategy; use beacon_chain::test_utils::{ - get_kzg, mock_execution_layer_from_parts, test_spec, AttestationStrategy, BeaconChainHarness, - BlockStrategy, DiskHarnessType, + AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, get_kzg, + mock_execution_layer_from_parts, test_spec, +}; +use beacon_chain::test_utils::{ + SyncCommitteeStrategy, fork_name_from_env, generate_data_column_indices_rand_order, }; use beacon_chain::{ - data_availability_checker::MaybeAvailableBlock, historical_blocks::HistoricalBlockError, - migrate::MigratorConfig, BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, - BlockError, ChainConfig, NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig, + NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, + beacon_proposer_cache::{ + compute_proposer_duties_from_head, ensure_state_can_determine_proposers_for_epoch, + }, + custody_context::NodeCustodyType, + data_availability_checker::MaybeAvailableBlock, + historical_blocks::HistoricalBlockError, + migrate::MigratorConfig, }; +use bls::{Keypair, Signature, SignatureBytes}; +use fixed_bytes::FixedBytesExtended; use logging::create_test_tracing_subscriber; use maplit::hashset; -use rand::rngs::StdRng; use rand::Rng; +use rand::rngs::StdRng; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::{state_advance::complete_state_advance, BlockReplayer}; +use ssz_types::VariableList; +use state_processing::{BlockReplayer, state_advance::complete_state_advance}; use std::collections::HashMap; use std::collections::HashSet; use std::convert::TryInto; +use std::str::FromStr; use std::sync::{Arc, LazyLock}; use std::time::Duration; use store::database::interface::BeaconNodeBackend; -use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN}; +use store::metadata::{CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN, SchemaVersion}; use store::{ - iter::{BlockRootsIterator, StateRootsIterator}, BlobInfo, DBColumn, HotColdDB, StoreConfig, + hdiff::HierarchyConfig, + iter::{BlockRootsIterator, StateRootsIterator}, }; -use tempfile::{tempdir, TempDir}; +use tempfile::{TempDir, tempdir}; +use tracing::info; use types::test_utils::{SeedableRng, XorShiftRng}; use types::*; @@ -88,7 +104,12 @@ fn get_harness( reconstruct_historic_states: true, ..ChainConfig::default() }; - get_harness_generic(store, validator_count, chain_config, false) + get_harness_generic( + store, + validator_count, + chain_config, + NodeCustodyType::Fullnode, + ) } fn get_harness_import_all_data_columns( @@ -100,14 +121,19 @@ fn get_harness_import_all_data_columns( reconstruct_historic_states: true, ..ChainConfig::default() }; - get_harness_generic(store, validator_count, chain_config, true) + get_harness_generic( + store, + validator_count, + chain_config, + NodeCustodyType::Supernode, + ) } fn get_harness_generic( store: Arc, BeaconNodeBackend>>, validator_count: usize, chain_config: ChainConfig, - import_all_data_columns: bool, + node_custody_type: NodeCustodyType, ) -> TestHarness { let harness = TestHarness::builder(MinimalEthSpec) .spec(store.get_chain_spec().clone()) @@ -115,23 +141,25 @@ fn get_harness_generic( .fresh_disk_store(store) .mock_execution_layer() .chain_config(chain_config) - .import_all_data_columns(import_all_data_columns) + .node_custody_type(node_custody_type) .build(); harness.advance_slot(); harness } -fn count_states_descendant_of_block( +fn get_states_descendant_of_block( store: &HotColdDB, BeaconNodeBackend>, block_root: Hash256, -) -> usize { +) -> Vec<(Hash256, Slot)> { let summaries = store.load_hot_state_summaries().unwrap(); summaries .iter() .filter(|(_, s)| s.latest_block_root == block_root) - .count() + .map(|(state_root, summary)| (*state_root, summary.slot)) + .collect() } +// TODO(EIP-7732) Extend to support gloas #[tokio::test] async fn light_client_bootstrap_test() { let spec = test_spec::(); @@ -293,7 +321,7 @@ async fn randomised_skips() { let mut head_slot = 0; for slot in 1..=num_slots { - if rng.gen_bool(0.8) { + if rng.random_bool(0.8) { harness .extend_chain( 1, @@ -406,13 +434,15 @@ async fn randao_genesis_storage() { .await; // Check that genesis value is still present - assert!(harness - .chain - .head_snapshot() - .beacon_state - .randao_mixes() - .iter() - .any(|x| *x == genesis_value)); + assert!( + harness + .chain + .head_snapshot() + .beacon_state + .randao_mixes() + .iter() + .any(|x| *x == genesis_value) + ); // Then upon adding one more block, it isn't harness.advance_slot(); @@ -423,13 +453,15 @@ async fn randao_genesis_storage() { AttestationStrategy::AllValidators, ) .await; - assert!(!harness - .chain - .head_snapshot() - .beacon_state - .randao_mixes() - .iter() - .any(|x| *x == genesis_value)); + assert!( + !harness + .chain + .head_snapshot() + .beacon_state + .randao_mixes() + .iter() + .any(|x| *x == genesis_value) + ); check_finalization(&harness, num_slots); check_split_slot(&harness, store); @@ -491,7 +523,7 @@ async fn epoch_boundary_state_attestation_processing() { .await; let head = harness.chain.head_snapshot(); - late_attestations.extend(harness.get_unaggregated_attestations( + late_attestations.extend(harness.get_single_attestations( &AttestationStrategy::SomeValidators(late_validators.clone()), &head.beacon_state, head.beacon_state_root(), @@ -511,20 +543,23 @@ async fn epoch_boundary_state_attestation_processing() { for (attestation, subnet_id) in late_attestations.into_iter().flatten() { // load_epoch_boundary_state is idempotent! - let block_root = attestation.data().beacon_block_root; + let block_root = attestation.data.beacon_block_root; let block = store .get_blinded_block(&block_root) .unwrap() .expect("block exists"); + // Use get_state as the state may be finalized by this point let mut epoch_boundary_state = store - .load_epoch_boundary_state(&block.state_root()) + .get_state(&block.state_root(), None, CACHE_STATE_IN_TESTS) .expect("no error") - .expect("epoch boundary state exists"); + .unwrap_or_else(|| { + panic!("epoch boundary state should exist {:?}", block.state_root()) + }); let ebs_state_root = epoch_boundary_state.update_tree_hash_cache().unwrap(); let mut ebs_of_ebs = store - .load_epoch_boundary_state(&ebs_state_root) + .get_state(&ebs_state_root, None, CACHE_STATE_IN_TESTS) .expect("no error") - .expect("ebs of ebs exists"); + .unwrap_or_else(|| panic!("ebs of ebs should exist {ebs_state_root:?}")); ebs_of_ebs.apply_pending_mutations().unwrap(); assert_eq!(epoch_boundary_state, ebs_of_ebs); @@ -536,7 +571,7 @@ async fn epoch_boundary_state_attestation_processing() { .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); - let expected_attestation_slot = attestation.data().slot; + let expected_attestation_slot = attestation.data.slot; // Extra -1 to handle gossip clock disparity. let expected_earliest_permissible_slot = current_slot - E::slots_per_epoch() - 1; @@ -1178,6 +1213,452 @@ fn check_shuffling_compatible( } } +/// These tests check the consistency of: +/// +/// - ProtoBlock::proposer_shuffling_root_for_child_block, and +/// - BeaconState::proposer_shuffling_decision_root{_at_epoch} +async fn proposer_shuffling_root_consistency_test( + spec: ChainSpec, + parent_slot: u64, + child_slot: u64, +) { + let child_slot = Slot::new(child_slot); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, Default::default(), spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = TestHarness::builder(MinimalEthSpec) + .spec(spec.into()) + .keypairs(validators_keypairs) + .fresh_disk_store(store) + .mock_execution_layer() + .build(); + let spec = &harness.chain.spec; + + // Build chain out to parent block. + let initial_slots: Vec = (1..=parent_slot).map(Into::into).collect(); + let (state, state_root) = harness.get_current_state_and_root(); + let all_validators = harness.get_all_validators(); + let (_, _, parent_root, _) = harness + .add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators) + .await; + + // Add the child block. + let (state, state_root) = harness.get_current_state_and_root(); + let all_validators = harness.get_all_validators(); + let (_, _, child_root, child_block_state) = harness + .add_attested_blocks_at_slots(state, state_root, &[child_slot], &all_validators) + .await; + + let child_block_epoch = child_slot.epoch(E::slots_per_epoch()); + + // Load parent block from fork choice. + let fc_parent = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&parent_root.into()) + .unwrap(); + + // The proposer shuffling decision root computed using fork choice should equal the root + // computed from the child state. + let decision_root = fc_parent.proposer_shuffling_root_for_child_block(child_block_epoch, spec); + + assert_eq!( + decision_root, + child_block_state + .proposer_shuffling_decision_root(child_root.into(), spec) + .unwrap() + ); + assert_eq!( + decision_root, + child_block_state + .proposer_shuffling_decision_root_at_epoch(child_block_epoch, child_root.into(), spec) + .unwrap() + ); + + // The passed block root argument should be irrelevant for all blocks except the genesis block. + assert_eq!( + decision_root, + child_block_state + .proposer_shuffling_decision_root(Hash256::ZERO, spec) + .unwrap() + ); + assert_eq!( + decision_root, + child_block_state + .proposer_shuffling_decision_root_at_epoch(child_block_epoch, Hash256::ZERO, spec) + .unwrap() + ); +} + +#[tokio::test] +async fn proposer_shuffling_root_consistency_same_epoch() { + let spec = test_spec::(); + proposer_shuffling_root_consistency_test( + spec, + 4 * E::slots_per_epoch(), + 5 * E::slots_per_epoch() - 1, + ) + .await; +} + +#[tokio::test] +async fn proposer_shuffling_root_consistency_next_epoch() { + let spec = test_spec::(); + proposer_shuffling_root_consistency_test( + spec, + 4 * E::slots_per_epoch(), + 6 * E::slots_per_epoch() - 1, + ) + .await; +} + +#[tokio::test] +async fn proposer_shuffling_root_consistency_two_epochs() { + let spec = test_spec::(); + proposer_shuffling_root_consistency_test( + spec, + 4 * E::slots_per_epoch(), + 7 * E::slots_per_epoch() - 1, + ) + .await; +} + +#[tokio::test] +async fn proposer_shuffling_root_consistency_at_fork_boundary() { + let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + spec.fulu_fork_epoch = Some(Epoch::new(4)); + + // Parent block in epoch prior to Fulu fork epoch, child block in Fulu fork epoch. + proposer_shuffling_root_consistency_test( + spec.clone(), + 3 * E::slots_per_epoch(), + 4 * E::slots_per_epoch(), + ) + .await; + + // Parent block and child block in Fulu fork epoch. + proposer_shuffling_root_consistency_test( + spec.clone(), + 4 * E::slots_per_epoch(), + 4 * E::slots_per_epoch() + 1, + ) + .await; + + // Parent block in Fulu fork epoch and child block in epoch after. + proposer_shuffling_root_consistency_test( + spec.clone(), + 4 * E::slots_per_epoch(), + 5 * E::slots_per_epoch(), + ) + .await; + + // Parent block in epoch prior and child block in epoch after. + proposer_shuffling_root_consistency_test( + spec, + 3 * E::slots_per_epoch(), + 5 * E::slots_per_epoch(), + ) + .await; +} + +#[tokio::test] +async fn proposer_shuffling_changing_with_lookahead() { + let initial_blocks = E::slots_per_epoch() * 4 - 1; + + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, Default::default(), spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = TestHarness::builder(MinimalEthSpec) + .spec(spec.into()) + .keypairs(validators_keypairs) + .fresh_disk_store(store) + .mock_execution_layer() + .build(); + let spec = &harness.chain.spec; + + // Start with some blocks, finishing with one slot before a new epoch. + harness.advance_slot(); + harness + .extend_chain( + initial_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let pre_deposit_state = harness.get_current_state(); + assert_eq!(pre_deposit_state.slot(), initial_blocks); + let topup_block_slot = Slot::new(initial_blocks + 1); + let validator_to_topup_index = 1; + let validator_to_topup = pre_deposit_state + .get_validator(validator_to_topup_index) + .unwrap() + .clone(); + + // Craft a block with a deposit request and consolidation. + // XXX: This is a really nasty way to do this, but we need better test facilities in + // MockExecutionLayer to address this. + let deposit_request: DepositRequest = DepositRequest { + index: pre_deposit_state.eth1_deposit_index(), + pubkey: validator_to_topup.pubkey, + withdrawal_credentials: validator_to_topup.withdrawal_credentials, + amount: 63_000_000_000, + signature: SignatureBytes::empty(), + }; + + let consolidation_request: ConsolidationRequest = ConsolidationRequest { + source_address: validator_to_topup + .get_execution_withdrawal_address(spec) + .unwrap(), + source_pubkey: validator_to_topup.pubkey, + target_pubkey: validator_to_topup.pubkey, + }; + + let execution_requests = ExecutionRequests:: { + deposits: VariableList::new(vec![deposit_request]).unwrap(), + withdrawals: vec![].try_into().unwrap(), + consolidations: VariableList::new(vec![consolidation_request]).unwrap(), + }; + + let mut block = Box::pin(harness.make_block_with_modifier( + pre_deposit_state.clone(), + topup_block_slot, + |block| *block.body_mut().execution_requests_mut().unwrap() = execution_requests, + )) + .await + .0; + + let Err(BlockError::StateRootMismatch { + local: true_state_root, + .. + }) = harness + .process_block(topup_block_slot, block.0.canonical_root(), block.clone()) + .await + else { + panic!("state root should not match due to pending deposits changes/etc"); + }; + let mut new_block = block.0.message_fulu().unwrap().clone(); + new_block.state_root = true_state_root; + block.0 = Arc::new(harness.sign_beacon_block(new_block.into(), &pre_deposit_state)); + + harness + .process_block(topup_block_slot, block.0.canonical_root(), block.clone()) + .await + .unwrap(); + + // Advance two epochs to finalize the deposit and process it. + // Start with just a single epoch advance so we can grab the state one epoch prior to where + // we end up. + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Grab the epoch start state. This is the state from which the proposers at the next epoch were + // computed. + let prev_epoch_state = harness.get_current_state(); + assert_eq!(prev_epoch_state.slot() % E::slots_per_epoch(), 0); + + // The deposit should be pending. + let pending_deposits = prev_epoch_state.pending_deposits().unwrap(); + assert_eq!(pending_deposits.len(), 1, "{pending_deposits:?}"); + + // Advance the 2nd epoch to finalize the deposit and process it. + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let current_epoch_state = harness.get_current_state(); + assert_eq!(current_epoch_state.slot() % E::slots_per_epoch(), 0); + + // Deposit is processed! + let pending_deposits = current_epoch_state.pending_deposits().unwrap(); + assert_eq!(pending_deposits.len(), 0, "{pending_deposits:?}"); + + let validator = current_epoch_state + .get_validator(validator_to_topup_index) + .unwrap(); + assert!(validator.has_compounding_withdrawal_credential(spec)); + assert_eq!(validator.effective_balance, 95_000_000_000); + + // The shuffling for the current epoch from `prev_epoch_state` should match the shuffling + // for the current epoch from `current_epoch_state` because we should be correctly using the + // stored lookahead. + let current_epoch = current_epoch_state.current_epoch(); + let proposer_shuffling = prev_epoch_state + .get_beacon_proposer_indices(current_epoch, spec) + .unwrap(); + + assert_eq!( + proposer_shuffling, + current_epoch_state + .get_beacon_proposer_indices(current_epoch, spec) + .unwrap() + ); + + // If we bypass the safety checks in `get_proposer_indices`, we should see that the shuffling + // differs due to the effective balance change. + let unsafe_get_proposer_indices = |state: &BeaconState, epoch| -> Vec { + let indices = state.get_active_validator_indices(epoch, spec).unwrap(); + let preimage = state.get_seed(epoch, Domain::BeaconProposer, spec).unwrap(); + epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| { + let mut preimage = preimage.to_vec(); + preimage.append(&mut int_to_bytes::int_to_bytes8(slot.as_u64())); + let seed = ethereum_hashing::hash(&preimage); + state.compute_proposer_index(&indices, &seed, spec).unwrap() + }) + .collect() + }; + + // The unsafe function is correct when used with lookahead. + assert_eq!( + unsafe_get_proposer_indices(&prev_epoch_state, current_epoch), + proposer_shuffling + ); + + // Computing the shuffling for current epoch without lookahead is WRONG. + assert_ne!( + unsafe_get_proposer_indices(¤t_epoch_state, current_epoch), + proposer_shuffling, + ); +} + +#[tokio::test] +async fn proposer_duties_from_head_fulu() { + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, Default::default(), spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = TestHarness::builder(MinimalEthSpec) + .spec(spec.into()) + .keypairs(validators_keypairs) + .fresh_disk_store(store) + .mock_execution_layer() + .build(); + let spec = &harness.chain.spec; + + let initial_blocks = E::slots_per_epoch() * 3; + + // Build chain out to parent block. + let initial_slots: Vec = (1..=initial_blocks).map(Into::into).collect(); + let (state, state_root) = harness.get_current_state_and_root(); + let all_validators = harness.get_all_validators(); + let (_, _, head_block_root, head_state) = harness + .add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators) + .await; + + // Compute the proposer duties at the next epoch from the head + let next_epoch = head_state.next_epoch().unwrap(); + let (_indices, dependent_root, legacy_dependent_root, _, fork) = + compute_proposer_duties_from_head(next_epoch, &harness.chain).unwrap(); + + assert_eq!( + dependent_root, + head_state + .proposer_shuffling_decision_root_at_epoch(next_epoch, head_block_root.into(), spec) + .unwrap() + ); + assert_ne!(dependent_root, legacy_dependent_root); + assert_eq!(legacy_dependent_root, Hash256::from(head_block_root)); + assert_eq!(fork, head_state.fork()); +} + +/// Test that we can compute the proposer shuffling for the Gloas fork epoch itself using lookahead! +// TODO(EIP-7732): Extend to gloas +// `state.latest_execution_payload_header()` not available in Gloas +// called from `add_block_at_slot` -> `make_block` -> `produce_block_on_state` -> `produce_partial_beacon_block` -> `get_execution_payload` -> `Error` +#[ignore] +#[tokio::test] +async fn proposer_lookahead_gloas_fork_epoch() { + let gloas_fork_epoch = Epoch::new(4); + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, Default::default(), spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = TestHarness::builder(E::default()) + .spec(spec.into()) + .keypairs(validators_keypairs) + .fresh_disk_store(store) + .mock_execution_layer() + .build(); + let spec = &harness.chain.spec; + + let initial_blocks = (gloas_fork_epoch - 1) + .start_slot(E::slots_per_epoch()) + .as_u64(); + + // Build chain out to parent block. + let initial_slots: Vec = (1..=initial_blocks).map(Into::into).collect(); + let (state, state_root) = harness.get_current_state_and_root(); + let all_validators = harness.get_all_validators(); + let (_, _, head_block_root, mut head_state) = harness + .add_attested_blocks_at_slots(state, state_root, &initial_slots, &all_validators) + .await; + let head_state_root = head_state.canonical_root().unwrap(); + + // Check that we have access to the next epoch shuffling according to + // `ensure_state_can_determine_proposers_for_epoch`. + ensure_state_can_determine_proposers_for_epoch( + &mut head_state, + head_state_root, + gloas_fork_epoch, + spec, + ) + .unwrap(); + assert_eq!(head_state.current_epoch(), gloas_fork_epoch - 1); + + // Compute the proposer duties at the fork epoch from the head. + let (indices, dependent_root, legacy_dependent_root, _, fork) = + compute_proposer_duties_from_head(gloas_fork_epoch, &harness.chain).unwrap(); + + assert_eq!( + dependent_root, + head_state + .proposer_shuffling_decision_root_at_epoch( + gloas_fork_epoch, + head_block_root.into(), + spec + ) + .unwrap() + ); + assert_ne!(dependent_root, legacy_dependent_root); + assert_ne!(fork, head_state.fork()); + assert_eq!(fork, spec.fork_at_epoch(gloas_fork_epoch)); + + // Build a block in the Gloas fork epoch and assert that the shuffling does not change. + let gloas_slots = vec![gloas_fork_epoch.start_slot(E::slots_per_epoch())]; + let (_, _, _, _) = harness + .add_attested_blocks_at_slots(head_state, head_state_root, &gloas_slots, &all_validators) + .await; + + let (no_lookahead_indices, no_lookahead_dependent_root, _, _, no_lookahead_fork) = + compute_proposer_duties_from_head(gloas_fork_epoch, &harness.chain).unwrap(); + + assert_eq!(no_lookahead_indices, indices); + assert_eq!(no_lookahead_dependent_root, dependent_root); + assert_eq!(no_lookahead_fork, fork); +} + // Ensure blocks from abandoned forks are pruned from the Hot DB #[tokio::test] async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { @@ -2171,7 +2652,8 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { let slots_per_epoch = E::slots_per_epoch(); - let genesis_state = harness.get_current_state(); + let mut genesis_state = harness.get_current_state(); + let genesis_state_root = genesis_state.update_tree_hash_cache().unwrap(); let block_slot = Slot::new(2 * slots_per_epoch); let ((signed_block, _), state) = harness.make_block(genesis_state, block_slot).await; @@ -2198,7 +2680,7 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { // The bad block parent root is the genesis block root. There's `block_slot - 1` temporary // states to remove + the genesis state = block_slot. assert_eq!( - count_states_descendant_of_block(&store, bad_block_parent_root), + get_states_descendant_of_block(&store, bad_block_parent_root).len(), block_slot.as_usize(), ); @@ -2216,11 +2698,12 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { // Check that the finalization migration ran. assert_ne!(store.get_split_slot(), 0); - // Check that temporary states have been pruned. The genesis block is not a descendant of the - // latest finalized checkpoint, so all its states have been pruned from the hot DB, = 0. + // Check that temporary states have been pruned. assert_eq!( - count_states_descendant_of_block(&store, bad_block_parent_root), - 0 + get_states_descendant_of_block(&store, bad_block_parent_root), + // The genesis state is kept to support the HDiff grid + vec![(genesis_state_root, Slot::new(0))], + "get_states_descendant_of_block({bad_block_parent_root:?})" ); } @@ -2229,7 +2712,15 @@ async fn weak_subjectivity_sync_easy() { let num_initial_slots = E::slots_per_epoch() * 11; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let slots = (1..num_initial_slots).map(Slot::new).collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None, true).await +} + +#[tokio::test] +async fn weak_subjectivity_sync_single_block_batches() { + let num_initial_slots = E::slots_per_epoch() * 11; + let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); + let slots = (1..num_initial_slots).map(Slot::new).collect(); + weak_subjectivity_sync_test(slots, checkpoint_slot, Some(1), true).await } #[tokio::test] @@ -2243,7 +2734,7 @@ async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { slot <= checkpoint_slot - 3 || slot > checkpoint_slot }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None, true).await } #[tokio::test] @@ -2257,7 +2748,7 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { slot <= checkpoint_slot || slot > checkpoint_slot + 3 }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None, true).await } // Regression test for https://github.com/sigp/lighthouse/issues/4817 @@ -2269,10 +2760,190 @@ async fn weak_subjectivity_sync_skips_at_genesis() { let end_slot = E::slots_per_epoch() * 4; let slots = (start_slot..end_slot).map(Slot::new).collect(); let checkpoint_slot = Slot::new(E::slots_per_epoch() * 2); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None, true).await } -async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { +// Checkpoint sync from the genesis state. +// +// This is a regression test for a bug we had involving the storage of the genesis state in the hot +// DB. +#[tokio::test] +async fn weak_subjectivity_sync_from_genesis() { + let start_slot = 1; + let end_slot = E::slots_per_epoch() * 2; + let slots = (start_slot..end_slot).map(Slot::new).collect(); + let checkpoint_slot = Slot::new(0); + weak_subjectivity_sync_test(slots, checkpoint_slot, None, true).await +} + +// Test checkpoint sync without providing blobs - backfill should fetch them. +#[tokio::test] +async fn weak_subjectivity_sync_without_blobs() { + let start_slot = 4; + let end_slot = E::slots_per_epoch() * 4; + let slots = (start_slot..end_slot).map(Slot::new).collect(); + let checkpoint_slot = Slot::new(E::slots_per_epoch() * 2); + weak_subjectivity_sync_test(slots, checkpoint_slot, None, false).await +} + +// Ensures that an unaligned checkpoint sync (the block is older than the state) +// works correctly even when `prune_payloads` is enabled. +// +// Previously, the `HotColdDB` would refuse to load the execution payload for the +// anchor block because it was considered "pruned", causing the node to fail startup. +#[tokio::test] +async fn reproduction_unaligned_checkpoint_sync_pruned_payload() { + let spec = test_spec::(); + + // Requires Execution Payloads. + let Some(_) = spec.deneb_fork_epoch else { + return; + }; + + // Create an unaligned checkpoint with a gap of 3 slots. + let num_initial_slots = E::slots_per_epoch() * 11; + let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9 - 3); + + let slots = (1..num_initial_slots) + .map(Slot::new) + .filter(|&slot| slot <= checkpoint_slot || slot > checkpoint_slot + 3) + .collect::>(); + + let temp1 = tempdir().unwrap(); + let full_store = get_store_generic(&temp1, StoreConfig::default(), spec.clone()); + + let harness = get_harness_import_all_data_columns(full_store.clone(), LOW_VALIDATOR_COUNT); + let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); + + let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); + harness + .add_attested_blocks_at_slots( + genesis_state.clone(), + genesis_state_root, + &slots, + &all_validators, + ) + .await; + + // Extract snapshot data from the harness. + let wss_block_root = harness + .chain + .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + let wss_state_root = harness + .chain + .state_root_at_slot(checkpoint_slot) + .unwrap() + .unwrap(); + + let wss_block = harness + .chain + .store + .get_full_block(&wss_block_root) + .unwrap() + .unwrap(); + + // The test premise requires the anchor block to have a payload. + assert!(wss_block.message().execution_payload().is_ok()); + + let wss_blobs_opt = harness + .chain + .get_or_reconstruct_blobs(&wss_block_root) + .unwrap(); + + let wss_state = full_store + .get_state(&wss_state_root, Some(checkpoint_slot), CACHE_STATE_IN_TESTS) + .unwrap() + .unwrap(); + + // Configure the client with `prune_payloads = true`. + // This triggers the path where `try_get_full_block` must explicitly handle the anchor block. + let temp2 = tempdir().unwrap(); + let store_config = StoreConfig { + prune_payloads: true, + ..StoreConfig::default() + }; + + let store = get_store_generic(&temp2, store_config, spec.clone()); + + let slot_clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(harness.chain.genesis_time), + Duration::from_secs(spec.seconds_per_slot), + ); + slot_clock.set_slot(harness.get_current_slot().as_u64()); + + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }; + + let trusted_setup = get_kzg(&spec); + let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); + let mock = mock_execution_layer_from_parts( + harness.spec.clone(), + harness.runtime.task_executor.clone(), + ); + let all_custody_columns = (0..spec.number_of_custody_groups).collect::>(); + + // Attempt to build the BeaconChain. + // If the bug is present, this will panic with `MissingFullBlockExecutionPayloadPruned`. + let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec, trusted_setup) + .chain_config(chain_config) + .store(store.clone()) + .custom_spec(spec.clone().into()) + .task_executor(harness.chain.task_executor.clone()) + .weak_subjectivity_state( + wss_state, + wss_block.clone(), + wss_blobs_opt.clone(), + genesis_state, + ) + .unwrap() + .store_migrator_config(MigratorConfig::default().blocking()) + .slot_clock(slot_clock) + .shutdown_sender(shutdown_tx) + .event_handler(Some(ServerSentEventHandler::new_with_capacity(1))) + .execution_layer(Some(mock.el)) + .ordered_custody_column_indices(all_custody_columns) + .rng(Box::new(StdRng::seed_from_u64(42))) + .build(); + + assert!( + beacon_chain.is_ok(), + "Beacon Chain failed to build. The anchor payload may have been incorrectly pruned. Error: {:?}", + beacon_chain.err() + ); + + let chain = beacon_chain.as_ref().unwrap(); + let wss_block_slot = wss_block.slot(); + + assert_ne!( + wss_block_slot, + chain.head_snapshot().beacon_state.slot(), + "Test invalid: Checkpoint was aligned (Slot {} == Slot {}). The test did not trigger the unaligned edge case.", + wss_block_slot, + chain.head_snapshot().beacon_state.slot() + ); + + let payload_exists = chain + .store + .execution_payload_exists(&wss_block_root) + .unwrap_or(false); + + assert!( + payload_exists, + "Split block payload must exist in the new node's store after checkpoint sync" + ); +} + +async fn weak_subjectivity_sync_test( + slots: Vec, + checkpoint_slot: Slot, + backfill_batch_size: Option, + provide_blobs: bool, +) { // Build an initial chain on one harness, representing a synced node with full history. let num_final_blocks = E::slots_per_epoch() * 2; @@ -2322,6 +2993,8 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .get_state(&wss_state_root, Some(checkpoint_slot), CACHE_STATE_IN_TESTS) .unwrap() .unwrap(); + let wss_state_slot = wss_state.slot(); + let wss_block_slot = wss_block.slot(); // Add more blocks that advance finalization further. harness.advance_slot(); @@ -2356,25 +3029,35 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { ); slot_clock.set_slot(harness.get_current_slot().as_u64()); + let chain_config = ChainConfig { + // Set reconstruct_historic_states to true from the start in the genesis case. This makes + // some of the later checks more uniform across the genesis/non-genesis cases. + reconstruct_historic_states: checkpoint_slot == 0, + ..ChainConfig::default() + }; + let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec, kzg) + .chain_config(chain_config) .store(store.clone()) .custom_spec(test_spec::().into()) .task_executor(harness.chain.task_executor.clone()) .weak_subjectivity_state( wss_state, wss_block.clone(), - wss_blobs_opt.clone(), + if provide_blobs { + wss_blobs_opt.clone() + } else { + None + }, genesis_state, ) .unwrap() .store_migrator_config(MigratorConfig::default().blocking()) - .dummy_eth1_backend() - .expect("should build dummy backend") .slot_clock(slot_clock) .shutdown_sender(shutdown_tx) - .chain_config(ChainConfig::default()) .event_handler(Some(ServerSentEventHandler::new_with_capacity(1))) .execution_layer(Some(mock.el)) + .ordered_custody_column_indices(generate_data_column_indices_rand_order::()) .rng(Box::new(StdRng::seed_from_u64(42))) .build() .expect("should build"); @@ -2414,12 +3097,14 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .unwrap(); let slot = full_block.slot(); + let full_block_root = full_block.canonical_root(); let state_root = full_block.state_root(); + info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); beacon_chain.slot_clock.set_slot(slot.as_u64()); beacon_chain .process_block( - full_block.canonical_root(), + full_block_root, harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, @@ -2438,85 +3123,156 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); } - // Forwards iterator from 0 should fail as we lack blocks. - assert!(matches!( - beacon_chain.forwards_iter_block_roots(Slot::new(0)), - Err(BeaconChainError::HistoricalBlockOutOfRange { .. }) - )); - - // Simulate processing of a `StatusMessage` with an older finalized epoch by calling - // `block_root_at_slot` with an old slot for which we don't know the block root. It should - // return `None` rather than erroring. - assert_eq!( - beacon_chain - .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) - .unwrap(), - None - ); - - // Simulate querying the API for a historic state that is unknown. It should also return - // `None` rather than erroring. - assert_eq!(beacon_chain.state_root_at_slot(Slot::new(1)).unwrap(), None); - - // Supply blocks backwards to reach genesis. Omit the genesis block to check genesis handling. - let historical_blocks = chain_dump[..wss_block.slot().as_usize()] - .iter() - .filter(|s| s.beacon_block.slot() != 0) - .map(|s| s.beacon_block.clone()) - .collect::>(); - - let mut available_blocks = vec![]; - for blinded in historical_blocks { - let block_root = blinded.canonical_root(); - let full_block = harness - .chain - .get_block(&block_root) - .await - .expect("should get block") - .expect("should get block"); - - if let MaybeAvailableBlock::Available(block) = harness - .chain - .data_availability_checker - .verify_kzg_for_rpc_block( - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), - ) - .expect("should verify kzg") - { - available_blocks.push(block); - } + if checkpoint_slot != 0 { + // Forwards iterator from 0 should fail as we lack blocks (unless checkpoint slot is 0). + assert!(matches!( + beacon_chain.forwards_iter_block_roots(Slot::new(0)), + Err(BeaconChainError::HistoricalBlockOutOfRange { .. }) + )); + } else { + assert_eq!( + beacon_chain + .forwards_iter_block_roots(Slot::new(0)) + .unwrap() + .next() + .unwrap() + .unwrap(), + (wss_block_root, Slot::new(0)) + ); } - // Corrupt the signature on the 1st block to ensure that the backfill processor is checking - // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. - let mut batch_with_invalid_first_block = - available_blocks.iter().map(clone_block).collect::>(); - batch_with_invalid_first_block[0] = { - let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); - let mut corrupt_block = (*block).clone(); - *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), data, Arc::new(spec)) - }; + // The checks in this block only make sense if some data is missing as a result of the + // checkpoint sync, i.e. if we are not just checkpoint syncing from genesis. + if checkpoint_slot != 0 { + // Simulate processing of a `StatusMessage` with an older finalized epoch by calling + // `block_root_at_slot` with an old slot for which we don't know the block root. It should + // return `None` rather than erroring. + assert_eq!( + beacon_chain + .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) + .unwrap(), + None + ); - // Importing the invalid batch should error. - assert!(matches!( - beacon_chain - .import_historical_block_batch(batch_with_invalid_first_block) - .unwrap_err(), - HistoricalBlockError::InvalidSignature - )); + // Simulate querying the API for a historic state that is unknown. It should also return + // `None` rather than erroring. + assert_eq!(beacon_chain.state_root_at_slot(Slot::new(1)).unwrap(), None); - // Importing the batch with valid signatures should succeed. - let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); - beacon_chain - .import_historical_block_batch(available_blocks_dup) - .unwrap(); + // Supply blocks backwards to reach genesis. Omit the genesis block to check genesis handling. + let historical_blocks = chain_dump[..wss_block.slot().as_usize()] + .iter() + .filter(|s| s.beacon_block.slot() != 0) + .map(|s| s.beacon_block.clone()) + .collect::>(); + + let mut available_blocks = vec![]; + for blinded in historical_blocks { + let block_root = blinded.canonical_root(); + let full_block = harness + .chain + .get_block(&block_root) + .await + .expect("should get block") + .expect("should get block"); + + if let MaybeAvailableBlock::Available(block) = harness + .chain + .data_availability_checker + .verify_kzg_for_rpc_block( + harness + .build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + ) + .expect("should verify kzg") + { + available_blocks.push(block); + } + } + + // Corrupt the signature on the 1st block to ensure that the backfill processor is checking + // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. + let mut batch_with_invalid_first_block = + available_blocks.iter().map(clone_block).collect::>(); + batch_with_invalid_first_block[0] = { + let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); + let mut corrupt_block = (*block).clone(); + *corrupt_block.signature_mut() = Signature::empty(); + AvailableBlock::__new_for_testing( + block_root, + Arc::new(corrupt_block), + data, + Arc::new(spec), + ) + }; + + // Importing the invalid batch should error. + assert!(matches!( + beacon_chain + .import_historical_block_batch(batch_with_invalid_first_block) + .unwrap_err(), + HistoricalBlockError::InvalidSignature + )); + assert_eq!(beacon_chain.store.get_oldest_block_slot(), wss_block.slot()); + + let batch_size = backfill_batch_size.unwrap_or(available_blocks.len()); + + for batch in available_blocks.rchunks(batch_size) { + let available_blocks_slots = batch + .iter() + .map(|block| (block.block().slot(), block.block().canonical_root())) + .collect::>(); + info!( + ?available_blocks_slots, + "wss_block_slot" = wss_block.slot().as_usize(), + "Importing historical block batch" + ); + + // Importing the batch with valid signatures should succeed. + let available_blocks_batch1 = batch.iter().map(clone_block).collect::>(); + beacon_chain + .import_historical_block_batch(available_blocks_batch1) + .unwrap(); + + // We should be able to load the block root at the `oldest_block_slot`. + // + // This is a regression test for: https://github.com/sigp/lighthouse/issues/7690 + let oldest_block_imported = &batch[0]; + let (oldest_block_slot, oldest_block_root) = + if oldest_block_imported.block().parent_root() == beacon_chain.genesis_block_root { + (Slot::new(0), beacon_chain.genesis_block_root) + } else { + available_blocks_slots[0] + }; + assert_eq!( + beacon_chain.store.get_oldest_block_slot(), + oldest_block_slot + ); + assert_eq!( + beacon_chain + .block_root_at_slot(oldest_block_slot, WhenSlotSkipped::None) + .unwrap() + .unwrap(), + oldest_block_root + ); + + // Resupplying the blocks should not fail, they can be safely ignored. + let available_blocks_batch2 = batch.iter().map(clone_block).collect::>(); + beacon_chain + .import_historical_block_batch(available_blocks_batch2) + .unwrap(); + } + } assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); - // Resupplying the blocks should not fail, they can be safely ignored. - beacon_chain - .import_historical_block_batch(available_blocks) - .unwrap(); + // Sanity check for non-aligned WSS starts, to make sure the WSS block is persisted properly + if wss_block_slot != wss_state_slot { + let new_node_block_root_at_wss_block = beacon_chain + .store + .get_cold_block_root(wss_block_slot) + .unwrap() + .unwrap(); + info!(?new_node_block_root_at_wss_block, %wss_block_slot); + assert_eq!(new_node_block_root_at_wss_block, wss_block.canonical_root()); + } // The forwards iterator should now match the original chain let forwards = beacon_chain @@ -2546,10 +3302,12 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Prune_payloads is set to false in the default config, so the payload should exist if block.message().execution_payload().is_ok() { - assert!(beacon_chain - .store - .execution_payload_exists(&block_root) - .unwrap(),); + assert!( + beacon_chain + .store + .execution_payload_exists(&block_root) + .unwrap(), + ); } prev_block_root = block_root; @@ -2566,16 +3324,311 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) .unwrap() .unwrap(); + assert_eq!( + state_root, + beacon_chain.state_root_at_slot(slot).unwrap().unwrap() + ); assert_eq!(state.slot(), slot); assert_eq!(state.canonical_root().unwrap(), state_root); } // Anchor slot is still set to the slot of the checkpoint block. - assert_eq!(store.get_anchor_info().anchor_slot, wss_block.slot()); + // Note: since hot tree states the anchor slot is set to the aligned ws state slot + // https://github.com/sigp/lighthouse/pull/6750 + let wss_aligned_slot = if checkpoint_slot % E::slots_per_epoch() == 0 { + checkpoint_slot + } else { + (checkpoint_slot.epoch(E::slots_per_epoch()) + Epoch::new(1)) + .start_slot(E::slots_per_epoch()) + }; + assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); + assert_eq!( + store.get_anchor_info().state_upper_limit, + if checkpoint_slot == 0 { + Slot::new(0) + } else { + Slot::new(u64::MAX) + } + ); + info!(anchor = ?store.get_anchor_info(), "anchor pre"); // Reconstruct states. store.clone().reconstruct_historic_states(None).unwrap(); - assert_eq!(store.get_anchor_info().anchor_slot, 0); + assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); + assert_eq!(store.get_anchor_info().state_upper_limit, Slot::new(0)); +} + +// This test prunes data columns from epoch 0 and then tries to re-import them via +// the same code paths that custody backfill sync imports data columns +#[tokio::test] +async fn test_import_historical_data_columns_batch() { + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + let start_slot = Epoch::new(0).start_slot(E::slots_per_epoch()) + 1; + let end_slot = Epoch::new(0).end_slot(E::slots_per_epoch()); + let cgc = 128; + + let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + (E::slots_per_epoch() * 2) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + harness.advance_slot(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + let mut data_columns_list = vec![]; + + // Get all data columns for epoch 0 + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + for data_column in data_columns.unwrap_or_default() { + data_columns_list.push(data_column); + } + } + + assert!(!data_columns_list.is_empty()); + + harness + .extend_chain( + (E::slots_per_epoch() * 4) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + harness.advance_slot(); + + // Prune data columns + harness + .chain + .store + .try_prune_blobs(true, Epoch::new(2)) + .unwrap(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + // Assert that data columns no longer exist for epoch 0 + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + assert!(data_columns.is_none()) + } + + // Re-import deleted data columns + harness + .chain + .import_historical_data_column_batch(Epoch::new(0), data_columns_list, cgc) + .unwrap(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + // Assert that data columns now exist for epoch 0 + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + if !harness + .get_block(block_root.into()) + .unwrap() + .message() + .body() + .blob_kzg_commitments() + .unwrap() + .is_empty() + { + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + assert!(data_columns.is_some()) + }; + } +} + +// This should verify that a data column sidecar containing mismatched block roots should fail to be imported. +// This also covers any test cases related to data columns with incorrect/invalid/mismatched block roots. +#[tokio::test] +async fn test_import_historical_data_columns_batch_mismatched_block_root() { + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + let start_slot = Slot::new(1); + let end_slot = Slot::new(E::slots_per_epoch() * 2 - 1); + let cgc = 128; + + let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + (E::slots_per_epoch() * 2) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + harness.advance_slot(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + let mut data_columns_list = vec![]; + + // Get all data columns from start_slot to end_slot + // and mutate the data columns with an invalid block root + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + + for data_column in data_columns.unwrap_or_default() { + let mut data_column = (*data_column).clone(); + if data_column.index % 2 == 0 { + data_column.signed_block_header.message.body_root = Hash256::ZERO; + } + + data_columns_list.push(Arc::new(data_column)); + } + } + assert!(!data_columns_list.is_empty()); + + harness + .extend_chain( + (E::slots_per_epoch() * 4) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + harness.advance_slot(); + + // Prune blobs + harness + .chain + .store + .try_prune_blobs(true, Epoch::new(2)) + .unwrap(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + // Assert there are no columns between start_slot and end_slot + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + assert!(data_columns.is_none()) + } + + // Attempt to import data columns with invalid block roots and expect a failure + let error = harness + .chain + .import_historical_data_column_batch( + start_slot.epoch(E::slots_per_epoch()), + data_columns_list, + cgc, + ) + .unwrap_err(); + + assert!(matches!( + error, + HistoricalDataColumnError::NoBlockFound { .. } + )); +} + +// This should verify that a data column sidecar associated to a block root that doesn't exist in the store cannot +// be imported. +#[tokio::test] +async fn test_import_historical_data_columns_batch_no_block_found() { + if fork_name_from_env().is_some_and(|f| !f.fulu_enabled()) { + return; + }; + + let spec = test_spec::(); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + let start_slot = Slot::new(1); + let end_slot = Slot::new(E::slots_per_epoch() * 2 - 1); + let cgc = 128; + + let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + (E::slots_per_epoch() * 2) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + harness.advance_slot(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + let mut data_columns_list = vec![]; + + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + + for data_column in data_columns.unwrap_or_default() { + let mut data_column = (*data_column).clone(); + data_column.signed_block_header.message.body_root = Hash256::ZERO; + data_columns_list.push(Arc::new(data_column)); + } + } + + assert!(!data_columns_list.is_empty()); + + harness + .extend_chain( + (E::slots_per_epoch() * 4) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + harness.advance_slot(); + + harness + .chain + .store + .try_prune_blobs(true, Epoch::new(2)) + .unwrap(); + + let block_root_iter = harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap(); + + for block in block_root_iter { + let (block_root, _) = block.unwrap(); + let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap(); + assert!(data_columns.is_none()) + } + + let error = harness + .chain + .import_historical_data_column_batch(Epoch::new(0), data_columns_list, cgc) + .unwrap_err(); + + assert!(matches!( + error, + HistoricalDataColumnError::NoBlockFound { .. } + )); } /// Test that blocks and attestations that refer to states around an unaligned split state are @@ -2588,7 +3641,12 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { reconstruct_historic_states: false, ..ChainConfig::default() }; - let harness = get_harness_generic(store.clone(), LOW_VALIDATOR_COUNT, chain_config, false); + let harness = get_harness_generic( + store.clone(), + LOW_VALIDATOR_COUNT, + chain_config, + NodeCustodyType::Fullnode, + ); let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); @@ -2644,11 +3702,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert_eq!(split.block_root, valid_fork_block.parent_root()); assert_ne!(split.state_root, unadvanced_split_state_root); - let invalid_fork_rpc_block = RpcBlock::new_without_blobs( - None, - invalid_fork_block.clone(), - harness.sampling_column_count, - ); + let invalid_fork_rpc_block = RpcBlock::new_without_blobs(None, invalid_fork_block.clone()); // Applying the invalid block should fail. let err = harness .chain @@ -2664,11 +3718,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { assert!(matches!(err, BlockError::WouldRevertFinalizedSlot { .. })); // Applying the valid block should succeed, but it should not become head. - let valid_fork_rpc_block = RpcBlock::new_without_blobs( - None, - valid_fork_block.clone(), - harness.sampling_column_count, - ); + let valid_fork_rpc_block = RpcBlock::new_without_blobs(None, valid_fork_block.clone()); harness .chain .process_block( @@ -2712,7 +3762,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { slot, ); harness.advance_slot(); - harness.process_attestations(attestations); + harness.process_attestations(attestations, &advanced_split_state); } } @@ -2763,10 +3813,6 @@ async fn finalizes_after_resuming_from_db() { .chain .persist_op_pool() .expect("should persist the op pool"); - harness - .chain - .persist_eth1_cache() - .expect("should persist the eth1 cache"); let original_chain = harness.chain; @@ -2874,8 +3920,8 @@ async fn revert_minority_fork_on_resume() { ); harness1.set_current_slot(slot); harness2.set_current_slot(slot); - harness1.process_attestations(attestations.clone()); - harness2.process_attestations(attestations); + harness1.process_attestations(attestations.clone(), &state); + harness2.process_attestations(attestations, &state); let ((block, blobs), new_state) = harness1.make_block(state, slot).await; @@ -2915,7 +3961,7 @@ async fn revert_minority_fork_on_resume() { slot, ); harness2.set_current_slot(slot); - harness2.process_attestations(attestations); + harness2.process_attestations(attestations, &state2); // Minority chain block (no attesters). let ((block1, blobs1), new_state1) = harness1.make_block(state1, slot).await; @@ -3007,12 +4053,26 @@ async fn revert_minority_fork_on_resume() { // version is correct. This is the easiest schema test to write without historic versions of // Lighthouse on-hand, but has the disadvantage that the min version needs to be adjusted manually // as old downgrades are deprecated. -#[tokio::test] -async fn schema_downgrade_to_min_version() { +async fn schema_downgrade_to_min_version( + store_config: StoreConfig, + reconstruct_historic_states: bool, +) { let num_blocks_produced = E::slots_per_epoch() * 4; let db_path = tempdir().unwrap(); - let store = get_store(&db_path); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + let spec = test_spec::(); + + let chain_config = ChainConfig { + reconstruct_historic_states, + ..ChainConfig::default() + }; + + let store = get_store_generic(&db_path, store_config.clone(), spec.clone()); + let harness = get_harness_generic( + store.clone(), + LOW_VALIDATOR_COUNT, + chain_config.clone(), + NodeCustodyType::Fullnode, + ); harness .extend_chain( @@ -3022,8 +4082,11 @@ async fn schema_downgrade_to_min_version() { ) .await; - let min_version = SchemaVersion(22); - let genesis_state_root = Some(harness.chain.genesis_state_root); + let min_version = if spec.is_fulu_scheduled() { + SchemaVersion(27) + } else { + SchemaVersion(22) + }; // Save the slot clock so that the new harness doesn't revert in time. let slot_clock = harness.chain.slot_clock.clone(); @@ -3033,49 +4096,106 @@ async fn schema_downgrade_to_min_version() { drop(harness); // Re-open the store. - let store = get_store(&db_path); + let store = get_store_generic(&db_path, store_config, spec); // Downgrade. - migrate_schema::>( - store.clone(), - genesis_state_root, - CURRENT_SCHEMA_VERSION, - min_version, - ) - .expect("schema downgrade to minimum version should work"); + migrate_schema::>(store.clone(), CURRENT_SCHEMA_VERSION, min_version) + .expect("schema downgrade to minimum version should work"); // Upgrade back. - migrate_schema::>( - store.clone(), - genesis_state_root, - min_version, - CURRENT_SCHEMA_VERSION, - ) - .expect("schema upgrade from minimum version should work"); + migrate_schema::>(store.clone(), min_version, CURRENT_SCHEMA_VERSION) + .expect("schema upgrade from minimum version should work"); // Recreate the harness. let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() + .chain_config(chain_config) .keypairs(KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec()) .testing_slot_clock(slot_clock) .resumed_disk_store(store.clone()) .mock_execution_layer() .build(); + // Check chain dump for appropriate range depending on whether this is an archive node. + let chain_dump_start_slot = if reconstruct_historic_states { + Slot::new(0) + } else { + store.get_split_slot() + }; + check_finalization(&harness, num_blocks_produced); check_split_slot(&harness, store.clone()); - check_chain_dump(&harness, num_blocks_produced + 1); - check_iterators(&harness); + check_chain_dump_from_slot( + &harness, + chain_dump_start_slot, + num_blocks_produced + 1 - chain_dump_start_slot.as_u64(), + ); + check_iterators_from_slot(&harness, chain_dump_start_slot); // Check that downgrading beyond the minimum version fails (bound is *tight*). let min_version_sub_1 = SchemaVersion(min_version.as_u64().checked_sub(1).unwrap()); - migrate_schema::>( - store.clone(), - genesis_state_root, - CURRENT_SCHEMA_VERSION, - min_version_sub_1, + migrate_schema::>(store.clone(), CURRENT_SCHEMA_VERSION, min_version_sub_1) + .expect_err("should not downgrade below minimum version"); +} + +// Schema upgrade/downgrade on an archive node where the optimised migration does apply due +// to the split state being aligned to a diff layer. +#[tokio::test] +async fn schema_downgrade_to_min_version_archive_node_grid_aligned() { + // Need to use 3 as the hierarchy exponent to get diffs on every epoch boundary with minimal + // spec. + schema_downgrade_to_min_version( + StoreConfig { + hierarchy_config: HierarchyConfig::from_str("3,4,5").unwrap(), + prune_payloads: false, + ..StoreConfig::default() + }, + true, ) - .expect_err("should not downgrade below minimum version"); + .await +} + +// Schema upgrade/downgrade on an archive node where the optimised migration DOES NOT apply +// due to the split state NOT being aligned to a diff layer. +#[tokio::test] +async fn schema_downgrade_to_min_version_archive_node_grid_unaligned() { + schema_downgrade_to_min_version( + StoreConfig { + hierarchy_config: HierarchyConfig::from_str("7").unwrap(), + prune_payloads: false, + ..StoreConfig::default() + }, + true, + ) + .await +} + +// Schema upgrade/downgrade on a full node with a fairly normal per-epoch diff config. +#[tokio::test] +async fn schema_downgrade_to_min_version_full_node_per_epoch_diffs() { + schema_downgrade_to_min_version( + StoreConfig { + hierarchy_config: HierarchyConfig::from_str("3,4,5").unwrap(), + prune_payloads: false, + ..StoreConfig::default() + }, + false, + ) + .await +} + +// Schema upgrade/downgrade on a full node with dense per-slot diffs. +#[tokio::test] +async fn schema_downgrade_to_min_version_full_node_dense_diffs() { + schema_downgrade_to_min_version( + StoreConfig { + hierarchy_config: HierarchyConfig::from_str("0,3,4,5").unwrap(), + prune_payloads: false, + ..StoreConfig::default() + }, + true, + ) + .await } /// Check that blob pruning prunes blobs older than the data availability boundary. @@ -3085,9 +4205,10 @@ async fn deneb_prune_blobs_happy_case() { let store = get_store(&db_path); if store.get_chain_spec().is_peer_das_scheduled() { - // TODO(fulu): add prune tests for Fulu / PeerDAS data columns. + // Blob pruning no longer needed since Fulu / PeerDAS return; } + let Some(deneb_fork_epoch) = store.get_chain_spec().deneb_fork_epoch else { // No-op prior to Deneb. return; @@ -3136,9 +4257,10 @@ async fn deneb_prune_blobs_no_finalization() { let store = get_store(&db_path); if store.get_chain_spec().is_peer_das_scheduled() { - // TODO(fulu): add prune tests for Fulu / PeerDAS data columns. + // Blob pruning no longer needed since Fulu / PeerDAS return; } + let Some(deneb_fork_epoch) = store.get_chain_spec().deneb_fork_epoch else { // No-op prior to Deneb. return; @@ -3195,29 +4317,46 @@ async fn deneb_prune_blobs_no_finalization() { /// Check that blob pruning does not fail trying to prune across the fork boundary. #[tokio::test] -async fn deneb_prune_blobs_fork_boundary() { - let deneb_fork_epoch = Epoch::new(4); +async fn prune_blobs_across_fork_boundary() { + // This test covers earlier forks and only need to be executed once. + // Note: this test is quite expensive (building a chain to epoch 15) and we should revisit this + if fork_name_from_env() != Some(ForkName::latest_stable()) { + return; + } + let mut spec = ForkName::Capella.make_genesis_spec(E::default_spec()); + + let deneb_fork_epoch = Epoch::new(4); spec.deneb_fork_epoch = Some(deneb_fork_epoch); let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); + let electra_fork_epoch = Epoch::new(8); + spec.electra_fork_epoch = Some(electra_fork_epoch); + + let fulu_fork_epoch = Epoch::new(12); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); + let db_path = tempdir().unwrap(); let store = get_store_generic(&db_path, StoreConfig::default(), spec); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + harness.execution_block_generator().set_min_blob_count(1); - let num_blocks = E::slots_per_epoch() * 7; + let blocks_to_deneb_finalization = E::slots_per_epoch() * 7; + let blocks_to_electra_finalization = E::slots_per_epoch() * 4; + let blocks_to_fulu_finalization = E::slots_per_epoch() * 4; - // Finalize to epoch 5. + // Extend the chain to epoch 7 + // Finalize to epoch 5 (Deneb). harness .extend_chain( - num_blocks as usize, + blocks_to_deneb_finalization as usize, BlockStrategy::OnCanonicalHead, AttestationStrategy::AllValidators, ) .await; - // Finalization should be at epoch 5. + // Finalization should be at epoch 5 (Deneb). let finalized_epoch = Epoch::new(5); let finalized_slot = finalized_epoch.start_slot(E::slots_per_epoch()); assert_eq!( @@ -3256,6 +4395,116 @@ async fn deneb_prune_blobs_fork_boundary() { assert_eq!(store.get_blob_info().oldest_blob_slot, Some(pruned_slot)); check_blob_existence(&harness, Slot::new(0), pruned_slot - 1, false); check_blob_existence(&harness, pruned_slot, harness.head_slot(), true); + + // Extend the chain to epoch 11 + // Finalize to epoch 9 (Electra) + harness.advance_slot(); + harness + .extend_chain( + blocks_to_electra_finalization as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Finalization should be at epoch 9 (Electra). + let finalized_epoch = Epoch::new(9); + let finalized_slot = finalized_epoch.start_slot(E::slots_per_epoch()); + assert_eq!( + harness.get_current_state().finalized_checkpoint().epoch, + finalized_epoch + ); + assert_eq!(store.get_split_slot(), finalized_slot); + + // All blobs since last pruning during Deneb should still be available. + assert_eq!(store.get_blob_info().oldest_blob_slot, Some(pruned_slot)); + + let electra_first_slot = electra_fork_epoch.start_slot(E::slots_per_epoch()); + // Check that blobs exist from the pruned slot to electra + check_blob_existence(&harness, pruned_slot, electra_first_slot - 1, true); + + // Trigger pruning on Electra + let pruned_slot = (electra_fork_epoch + 1).start_slot(E::slots_per_epoch()); + + store.try_prune_blobs(true, finalized_epoch).unwrap(); + assert_eq!(store.get_blob_info().oldest_blob_slot, Some(finalized_slot)); + check_blob_existence(&harness, Slot::new(0), pruned_slot - 1, false); + check_blob_existence(&harness, pruned_slot, harness.head_slot(), true); + + // Check that blobs have been pruned up to the pruned slot + check_blob_existence(&harness, Slot::new(0), pruned_slot - 1, false); + // Check that blobs exist from electra to the current head + check_blob_existence(&harness, electra_first_slot, harness.head_slot(), true); + + // Extend the chain to epoch 15 + // Finalize to epoch 13 (Fulu) + harness.advance_slot(); + harness + .extend_chain( + blocks_to_fulu_finalization as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Finalization should be at epoch 13 (Fulu). + let finalized_epoch = Epoch::new(13); + let finalized_slot = finalized_epoch.start_slot(E::slots_per_epoch()); + assert_eq!( + harness.get_current_state().finalized_checkpoint().epoch, + finalized_epoch + ); + assert_eq!(store.get_split_slot(), finalized_slot); + + // All blobs since last pruning during Electra should still be available. + assert_eq!(store.get_blob_info().oldest_blob_slot, Some(pruned_slot)); + + let fulu_first_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + // Check that blobs have been pruned up to the pruned slot + check_blob_existence(&harness, Slot::new(0), pruned_slot - 1, false); + // Check that blobs exist from the pruned slot to Fulu + check_blob_existence(&harness, pruned_slot, fulu_first_slot - 1, true); + // Check that blobs do not exist from Fulu to the current head + check_blob_existence(&harness, fulu_first_slot, harness.head_slot(), false); + + // Attempt pruning with at different epochs. No pruning should occur for epochs + // preceding Fulu, as we have already triggered pruning pre-Fulu. Pruning should occur + // for epochs after Fulu. + assert!(fulu_fork_epoch < finalized_epoch); + for data_availability_boundary in [ + Epoch::new(7), + electra_fork_epoch, + Epoch::new(9), + Epoch::new(11), + fulu_fork_epoch, + Epoch::new(15), + ] { + store + .try_prune_blobs(true, data_availability_boundary) + .unwrap(); + + let oldest_slot = data_availability_boundary.start_slot(E::slots_per_epoch()); + + if data_availability_boundary < fulu_fork_epoch { + // Pre Fulu fork epochs + // Check oldest blob slot is not updated. + assert!(store.get_blob_info().oldest_blob_slot >= Some(oldest_slot)); + check_blob_existence(&harness, Slot::new(0), oldest_slot - 1, false); + // Blobs should exist + check_blob_existence(&harness, oldest_slot, harness.head_slot(), true); + } else { + // Fulu fork epochs + // Pruning should have been triggered + assert!(store.get_blob_info().oldest_blob_slot <= Some(oldest_slot)); + // Oldest blob slot should never be greater than the first fulu slot + let fulu_first_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + assert!(store.get_blob_info().oldest_blob_slot <= Some(fulu_first_slot)); + // Blobs should not exist post-Fulu + check_blob_existence(&harness, oldest_slot, harness.head_slot(), false); + // Data columns should exist post-Fulu + check_data_column_existence(&harness, oldest_slot, harness.head_slot(), true); + }; + } } /// Check that blob pruning prunes blobs older than the data availability boundary with margin @@ -3284,9 +4533,10 @@ async fn deneb_prune_blobs_margin_test(margin: u64) { let store = get_store_generic(&db_path, config, test_spec::()); if store.get_chain_spec().is_peer_das_scheduled() { - // TODO(fulu): add prune tests for Fulu / PeerDAS data columns. + // Blob pruning no longer needed since Fulu / PeerDAS return; } + let Some(deneb_fork_epoch) = store.get_chain_spec().deneb_fork_epoch else { // No-op prior to Deneb. return; @@ -3396,6 +4646,368 @@ fn check_blob_existence( } } +/// Check that blob pruning prunes data columns older than the data availability boundary. +#[tokio::test] +async fn fulu_prune_data_columns_happy_case() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + return; + } + let Some(fulu_fork_epoch) = store.get_chain_spec().fulu_fork_epoch else { + // No-op prior to Fulu. + return; + }; + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + + let num_blocks_produced = E::slots_per_epoch() * 8; + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Prior to manual pruning with an artifically low data availability boundary all data columns + // should be stored. + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(fulu_fork_slot) + ); + check_data_column_existence(&harness, Slot::new(1), harness.head_slot(), true); + + // Trigger pruning of data columns older than epoch 2. + let data_availability_boundary = Epoch::new(2); + store + .try_prune_blobs(true, data_availability_boundary) + .unwrap(); + + // Check oldest data column slot is updated accordingly and prior data columns have been + // deleted. + let oldest_data_column_slot = store + .get_data_column_info() + .oldest_data_column_slot + .unwrap(); + assert_eq!( + oldest_data_column_slot, + data_availability_boundary.start_slot(E::slots_per_epoch()) + ); + check_data_column_existence(&harness, Slot::new(0), oldest_data_column_slot - 1, false); + check_data_column_existence(&harness, oldest_data_column_slot, harness.head_slot(), true); +} + +/// Check that blob pruning does not prune data columns without finalization. +#[tokio::test] +async fn fulu_prune_data_columns_no_finalization() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + return; + } + let Some(fulu_fork_epoch) = store.get_chain_spec().fulu_fork_epoch else { + // No-op prior to Fulu. + return; + }; + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + + let initial_num_blocks = E::slots_per_epoch() * 5; + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // Finalize to epoch 3. + harness + .extend_chain( + initial_num_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Extend the chain for another few epochs without attestations. + let unfinalized_num_blocks = E::slots_per_epoch() * 3; + harness.advance_slot(); + harness + .extend_chain( + unfinalized_num_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + // Finalization should be at epoch 3. + let finalized_slot = Slot::new(E::slots_per_epoch() * 3); + assert_eq!(harness.get_current_state().finalized_checkpoint().epoch, 3); + assert_eq!(store.get_split_slot(), finalized_slot); + + // All data columns should still be available. + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(fulu_fork_slot) + ); + check_data_column_existence(&harness, Slot::new(0), harness.head_slot(), true); + + // Attempt pruning of data columns older than epoch 4, which is newer than finalization. + let data_availability_boundary = Epoch::new(4); + store + .try_prune_blobs(true, data_availability_boundary) + .unwrap(); + + // Check oldest data column slot is only updated to finalization, and NOT to the DAB. + let oldest_data_column_slot = store + .get_data_column_info() + .oldest_data_column_slot + .unwrap(); + assert_eq!(oldest_data_column_slot, finalized_slot); + check_data_column_existence(&harness, Slot::new(0), finalized_slot - 1, false); + check_data_column_existence(&harness, finalized_slot, harness.head_slot(), true); +} + +/// Check that data column pruning does not fail trying to prune across the fork boundary. +#[tokio::test] +async fn fulu_prune_data_columns_fork_boundary() { + let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let fulu_fork_epoch = Epoch::new(4); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + panic!("PeerDAS not scheduled"); + //return; + } + + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + let num_blocks = E::slots_per_epoch() * 7; + + // Finalize to epoch 5. + harness + .extend_chain( + num_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Finalization should be at epoch 5. + let finalized_epoch = Epoch::new(5); + let finalized_slot = finalized_epoch.start_slot(E::slots_per_epoch()); + assert_eq!( + harness.get_current_state().finalized_checkpoint().epoch, + finalized_epoch + ); + assert_eq!(store.get_split_slot(), finalized_slot); + + // All data columns should still be available. + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(fulu_fork_slot) + ); + check_data_column_existence(&harness, Slot::new(0), harness.head_slot(), true); + + // Attempt pruning with data availability epochs that precede the fork epoch. + // No pruning should occur. + assert!(fulu_fork_epoch < finalized_epoch); + for data_availability_boundary in [Epoch::new(0), Epoch::new(3), fulu_fork_epoch] { + store + .try_prune_blobs(true, data_availability_boundary) + .unwrap(); + + // Check oldest data column slot is not updated. + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(fulu_fork_slot) + ); + } + // All data columns should still be available. + check_data_column_existence(&harness, Slot::new(0), harness.head_slot(), true); + + // Prune one epoch past the fork. + let pruned_slot = (fulu_fork_epoch + 1).start_slot(E::slots_per_epoch()); + store.try_prune_blobs(true, fulu_fork_epoch + 1).unwrap(); + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(pruned_slot) + ); + check_data_column_existence(&harness, Slot::new(0), pruned_slot - 1, false); + check_data_column_existence(&harness, pruned_slot, harness.head_slot(), true); +} + +#[tokio::test] +async fn test_column_da_boundary() { + let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let fulu_fork_epoch = Epoch::new(4); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + panic!("PeerDAS not scheduled"); + } + + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // The column da boundary should be the fulu fork epoch + assert_eq!( + harness.chain.column_data_availability_boundary(), + Some(fulu_fork_epoch) + ); +} + +#[tokio::test] +async fn test_earliest_custodied_data_column_epoch() { + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, StoreConfig::default(), spec); + let custody_info_epoch = Epoch::new(4); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + panic!("PeerDAS not scheduled"); + } + + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // earliest custody info is set to the last slot in `custody_info_epoch` + harness + .chain + .update_data_column_custody_info(Some(custody_info_epoch.end_slot(E::slots_per_epoch()))); + + // earliest custodied data column epoch should be `custody_info_epoch` + 1 + assert_eq!( + harness.chain.earliest_custodied_data_column_epoch(), + Some(custody_info_epoch + 1) + ); + + // earliest custody info is set to the first slot in `custody_info_epoch` + harness + .chain + .update_data_column_custody_info(Some(custody_info_epoch.start_slot(E::slots_per_epoch()))); + + // earliest custodied data column epoch should be `custody_info_epoch` + assert_eq!( + harness.chain.earliest_custodied_data_column_epoch(), + Some(custody_info_epoch) + ); +} + +/// Check that blob pruning prunes data columns older than the data availability boundary with +/// margin applied. +#[tokio::test] +async fn fulu_prune_data_columns_margin1() { + fulu_prune_data_columns_margin_test(1).await; +} + +#[tokio::test] +async fn fulu_prune_data_columns_margin3() { + fulu_prune_data_columns_margin_test(3).await; +} + +#[tokio::test] +async fn fulu_prune_data_columns_margin4() { + fulu_prune_data_columns_margin_test(4).await; +} + +async fn fulu_prune_data_columns_margin_test(margin: u64) { + let config = StoreConfig { + blob_prune_margin_epochs: margin, + ..StoreConfig::default() + }; + let db_path = tempdir().unwrap(); + let store = get_store_generic(&db_path, config, test_spec::()); + + if !store.get_chain_spec().is_peer_das_scheduled() { + // No-op if PeerDAS not scheduled. + return; + } + let Some(fulu_fork_epoch) = store.get_chain_spec().fulu_fork_epoch else { + // No-op prior to Fulu. + return; + }; + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); + + let num_blocks_produced = E::slots_per_epoch() * 8; + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + harness + .extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Prior to manual pruning with an artifically low data availability boundary all blobs should + // be stored. + assert_eq!( + store.get_data_column_info().oldest_data_column_slot, + Some(fulu_fork_slot) + ); + check_data_column_existence(&harness, Slot::new(1), harness.head_slot(), true); + + // Trigger blob pruning of blobs older than epoch 6 - margin (6 is the minimum, due to + // finalization). + let data_availability_boundary = Epoch::new(6); + let effective_data_availability_boundary = + data_availability_boundary - store.get_config().blob_prune_margin_epochs; + assert!( + effective_data_availability_boundary > 0, + "must be > 0 because epoch 0 won't get pruned alone" + ); + store + .try_prune_blobs(true, data_availability_boundary) + .unwrap(); + + // Check oldest blob slot is updated accordingly and prior blobs have been deleted. + let oldest_data_column_slot = store + .get_data_column_info() + .oldest_data_column_slot + .unwrap(); + assert_eq!( + oldest_data_column_slot, + effective_data_availability_boundary.start_slot(E::slots_per_epoch()) + ); + check_data_column_existence(&harness, Slot::new(0), oldest_data_column_slot - 1, false); + check_data_column_existence(&harness, oldest_data_column_slot, harness.head_slot(), true); +} + +/// Check that there are data column sidecars (or not) at every slot in the range. +fn check_data_column_existence( + harness: &TestHarness, + start_slot: Slot, + end_slot: Slot, + should_exist: bool, +) { + let mut columns_seen = 0; + for (block_root, slot) in harness + .chain + .forwards_iter_block_roots_until(start_slot, end_slot) + .unwrap() + .map(Result::unwrap) + { + if let Some(columns) = harness.chain.store.get_data_columns(&block_root).unwrap() { + assert!(should_exist, "columns at slot {slot} exist but should not"); + columns_seen += columns.len(); + } else { + // We don't actually store empty columns, so unfortunately we can't assert anything + // meaningful here (like asserting that the column should not exist). + } + } + if should_exist { + assert_ne!(columns_seen, 0, "expected non-zero number of columns"); + } +} + #[tokio::test] async fn prune_historic_states() { let num_blocks_produced = E::slots_per_epoch() * 5; @@ -3427,10 +5039,12 @@ async fn prune_historic_states() { .map(Result::unwrap) .collect::>(); for &(state_root, slot) in &first_epoch_state_roots { - assert!(store - .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) - .unwrap() - .is_some()); + assert!( + store + .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) + .unwrap() + .is_some() + ); } store @@ -3463,6 +5077,405 @@ async fn prune_historic_states() { check_split_slot(&harness, store); } +// Test the function `get_ancestor_state_root` for slots prior to the split where we only have +// sparse summaries stored. +#[tokio::test] +async fn ancestor_state_root_prior_to_split() { + let db_path = tempdir().unwrap(); + + let spec = test_spec::(); + + let store_config = StoreConfig { + prune_payloads: false, + hierarchy_config: HierarchyConfig::from_str("5,7,8").unwrap(), + ..StoreConfig::default() + }; + let chain_config = ChainConfig { + reconstruct_historic_states: false, + ..ChainConfig::default() + }; + + let store = get_store_generic(&db_path, store_config, spec); + let harness = get_harness_generic( + store.clone(), + LOW_VALIDATOR_COUNT, + chain_config, + NodeCustodyType::Fullnode, + ); + + // Produce blocks until we have passed through two full snapshot periods. This period length is + // determined by the hierarchy config set above. + let num_blocks = 2 * store + .hierarchy + .next_snapshot_slot(Slot::new(1)) + .unwrap() + .as_u64(); + + for num_blocks_so_far in 0..num_blocks { + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + harness.advance_slot(); + + // Check that `get_ancestor_state_root` can look up the grid-aligned ancestors of every hot + // state, even at ancestor slots prior to the split. + let head_state = harness.get_current_state(); + assert_eq!(head_state.slot().as_u64(), num_blocks_so_far + 1); + + let split_slot = store.get_split_slot(); + let anchor_slot = store.get_anchor_info().anchor_slot; + + for state_slot in (split_slot.as_u64()..=num_blocks_so_far).map(Slot::new) { + for ancestor_slot in store + .hierarchy + .closest_layer_points(state_slot, anchor_slot) + { + // The function currently doesn't consider a state an ancestor of itself, so this + // does not work. + if ancestor_slot == state_slot { + continue; + } + let ancestor_state_root = store::hot_cold_store::get_ancestor_state_root( + &store, + &head_state, + ancestor_slot, + ) + .unwrap_or_else(|e| { + panic!( + "get_ancestor_state_root failed for state_slot={state_slot}, \ + ancestor_slot={ancestor_slot}, head_slot={}. error: {e:?}", + head_state.slot() + ) + }); + + // Check state root correctness. + assert_eq!( + store + .load_hot_state_summary(&ancestor_state_root) + .unwrap() + .unwrap_or_else(|| panic!( + "no summary found for {ancestor_state_root:?} (slot {ancestor_slot})" + )) + .slot, + ancestor_slot, + ) + } + } + } + + // This test only makes sense if the split is non-zero by the end. + assert_ne!(store.get_split_slot(), 0); +} + +// Test that the chain operates correctly when the split state is stored as a ReplayFrom. +#[tokio::test] +async fn replay_from_split_state() { + let db_path = tempdir().unwrap(); + + let spec = test_spec::(); + + let store_config = StoreConfig { + prune_payloads: false, + hierarchy_config: HierarchyConfig::from_str("5").unwrap(), + ..StoreConfig::default() + }; + let chain_config = ChainConfig { + reconstruct_historic_states: false, + ..ChainConfig::default() + }; + + let store = get_store_generic(&db_path, store_config.clone(), spec.clone()); + let harness = get_harness_generic( + store.clone(), + LOW_VALIDATOR_COUNT, + chain_config, + NodeCustodyType::Fullnode, + ); + + // Produce blocks until we finalize epoch 3 which will not be stored as a snapshot. + let num_blocks = 5 * E::slots_per_epoch() as usize; + + harness + .extend_chain( + num_blocks, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let split = store.get_split_info(); + let anchor_slot = store.get_anchor_info().anchor_slot; + assert_eq!(split.slot, 3 * E::slots_per_epoch()); + assert_eq!(anchor_slot, 0); + assert!( + store + .hierarchy + .storage_strategy(split.slot, anchor_slot) + .unwrap() + .is_replay_from() + ); + + // Close the database and reopen it. + drop(store); + drop(harness); + + let store = get_store_generic(&db_path, store_config, spec); + + // Check that the split state is still accessible. + assert_eq!(store.get_split_slot(), split.slot); + let state = store + .get_hot_state(&split.state_root, false) + .unwrap() + .expect("split state should be present"); + assert_eq!(state.slot(), split.slot); +} + +/// Test that regular nodes filter and store only custody columns when processing blocks with data columns. +#[tokio::test] +async fn test_custody_column_filtering_regular_node() { + // Skip test if PeerDAS is not scheduled + if !test_spec::().is_peer_das_scheduled() { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + + // Generate a block with data columns + harness.execution_block_generator().set_min_blob_count(1); + let current_slot = harness.get_current_slot(); + let block_root = harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Get custody columns for this epoch - regular nodes only store a subset + let expected_custody_columns: HashSet<_> = harness + .chain + .custody_columns_for_epoch(Some(current_slot.epoch(E::slots_per_epoch()))) + .iter() + .copied() + .collect(); + + // Check what actually got stored in the database + let stored_column_indices: HashSet<_> = store + .get_data_column_keys(block_root) + .expect("should get stored column keys") + .into_iter() + .collect(); + + assert_eq!( + stored_column_indices, expected_custody_columns, + "Regular node should only store custody columns" + ); +} + +/// Test that supernodes store all data columns when processing blocks with data columns. +#[tokio::test] +async fn test_custody_column_filtering_supernode() { + // Skip test if PeerDAS is not scheduled + if !test_spec::().is_peer_das_scheduled() { + return; + } + + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT); + + // Generate a block with data columns + harness.execution_block_generator().set_min_blob_count(1); + let block_root = harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Supernodes are expected to store all data columns + let expected_custody_columns: HashSet<_> = (0..E::number_of_columns() as u64).collect(); + + // Check what actually got stored in the database + let stored_column_indices: HashSet<_> = store + .get_data_column_keys(block_root) + .expect("should get stored column keys") + .into_iter() + .collect(); + + assert_eq!( + stored_column_indices, expected_custody_columns, + "Supernode should store all custody columns" + ); +} + +#[tokio::test] +async fn test_missing_columns_after_cgc_change() { + let spec = test_spec::(); + + let num_validators = 8; + + let num_epochs_before_increase = 4; + + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.clone().into()) + .deterministic_keypairs(num_validators) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let state = harness.chain.head_beacon_state_cloned(); + + if !state.fork_name_unchecked().fulu_enabled() { + return; + } + + let custody_context = harness.chain.data_availability_checker.custody_context(); + + harness.advance_slot(); + harness + .extend_chain( + (E::slots_per_epoch() * num_epochs_before_increase) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let epoch_before_increase = Epoch::new(num_epochs_before_increase); + + let missing_columns = harness + .chain + .get_missing_columns_for_epoch(epoch_before_increase); + + // We should have no missing columns + assert_eq!(missing_columns.len(), 0); + + let epoch_after_increase = Epoch::new(num_epochs_before_increase + 2); + + let cgc_change_slot = epoch_before_increase.end_slot(E::slots_per_epoch()); + custody_context.register_validators(vec![(1, 32_000_000_000 * 9)], cgc_change_slot, &spec); + + harness.advance_slot(); + harness + .extend_chain( + (E::slots_per_epoch() * 5) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // We should have missing columns from before the cgc increase + let missing_columns = harness + .chain + .get_missing_columns_for_epoch(epoch_before_increase); + + assert!(!missing_columns.is_empty()); + + // We should have no missing columns after the cgc increase + let missing_columns = harness + .chain + .get_missing_columns_for_epoch(epoch_after_increase); + + assert!(missing_columns.is_empty()); +} + +#[tokio::test] +async fn test_safely_backfill_data_column_custody_info() { + let spec = test_spec::(); + + let num_validators = 8; + + let start_epochs = 4; + + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.clone().into()) + .deterministic_keypairs(num_validators) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let state = harness.chain.head_beacon_state_cloned(); + + if !state.fork_name_unchecked().fulu_enabled() { + return; + } + + let custody_context = harness.chain.data_availability_checker.custody_context(); + + harness.advance_slot(); + harness + .extend_chain( + (E::slots_per_epoch() * start_epochs) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let epoch_before_increase = Epoch::new(start_epochs); + let effective_delay_slots = + CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS / harness.chain.spec.seconds_per_slot; + + let cgc_change_slot = epoch_before_increase.end_slot(E::slots_per_epoch()); + + custody_context.register_validators(vec![(1, 32_000_000_000 * 16)], cgc_change_slot, &spec); + + let epoch_after_increase = + (cgc_change_slot + effective_delay_slots).epoch(E::slots_per_epoch()); + + harness.advance_slot(); + harness + .extend_chain( + (E::slots_per_epoch() * 5) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let head_slot = harness.chain.head().snapshot.beacon_block.slot(); + + harness + .chain + .update_data_column_custody_info(Some(head_slot)); + + // We can only safely update custody column info 1 epoch at a time + // Skipping an epoch should return an error + harness + .chain + .safely_backfill_data_column_custody_info(head_slot.epoch(E::slots_per_epoch()) - 2) + .unwrap_err(); + + // Iterate from the head epoch back to 0 and try to backfill data column custody info + for epoch in (0..head_slot.epoch(E::slots_per_epoch()).into()).rev() { + // This is an epoch before the cgc change took into effect, we shouldnt be able to update + // without performing custody backfill sync + if epoch <= epoch_after_increase.into() { + harness + .chain + .safely_backfill_data_column_custody_info(Epoch::new(epoch)) + .unwrap_err(); + } else { + // This is an epoch after the cgc change took into effect, we should be able to update + // as long as we iterate epoch by epoch + harness + .chain + .safely_backfill_data_column_custody_info(Epoch::new(epoch)) + .unwrap(); + let earliest_available_epoch = harness + .chain + .earliest_custodied_data_column_epoch() + .unwrap(); + assert_eq!(Epoch::new(epoch), earliest_available_epoch); + } + } +} + /// Checks that two chains are the same, for the purpose of these tests. /// /// Several fields that are hard/impossible to check are ignored (e.g., the store). @@ -3556,7 +5569,11 @@ fn check_split_slot( /// Check that all the states in a chain dump have the correct tree hash. fn check_chain_dump(harness: &TestHarness, expected_len: u64) { - let mut chain_dump = harness.chain.chain_dump().unwrap(); + check_chain_dump_from_slot(harness, Slot::new(0), expected_len) +} + +fn check_chain_dump_from_slot(harness: &TestHarness, from_slot: Slot, expected_len: u64) { + let mut chain_dump = harness.chain.chain_dump_from_slot(from_slot).unwrap(); assert_eq!(chain_dump.len() as u64, expected_len); @@ -3604,7 +5621,7 @@ fn check_chain_dump(harness: &TestHarness, expected_len: u64) { let mut forward_block_roots = harness .chain - .forwards_iter_block_roots(Slot::new(0)) + .forwards_iter_block_roots(from_slot) .expect("should get iter") .map(Result::unwrap) .collect::>(); @@ -3625,10 +5642,14 @@ fn check_chain_dump(harness: &TestHarness, expected_len: u64) { /// Check that every state from the canonical chain is in the database, and that the /// reverse state and block root iterators reach genesis. fn check_iterators(harness: &TestHarness) { + check_iterators_from_slot(harness, Slot::new(0)) +} + +fn check_iterators_from_slot(harness: &TestHarness, slot: Slot) { let mut max_slot = None; for (state_root, slot) in harness .chain - .forwards_iter_state_roots(Slot::new(0)) + .forwards_iter_state_roots(slot) .expect("should get iter") .map(Result::unwrap) { @@ -3650,7 +5671,7 @@ fn check_iterators(harness: &TestHarness) { assert_eq!( harness .chain - .forwards_iter_block_roots(Slot::new(0)) + .forwards_iter_block_roots(slot) .expect("should get iter") .last() .map(Result::unwrap) @@ -3663,7 +5684,6 @@ fn get_finalized_epoch_boundary_blocks( dump: &[BeaconSnapshot>], ) -> HashSet { dump.iter() - .cloned() .map(|checkpoint| checkpoint.beacon_state.finalized_checkpoint().root.into()) .collect() } @@ -3672,7 +5692,6 @@ fn get_blocks( dump: &[BeaconSnapshot>], ) -> HashSet { dump.iter() - .cloned() .map(|checkpoint| checkpoint.beacon_block_root.into()) .collect() } diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index c8bbcce20d..d2124c6641 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -2,19 +2,22 @@ use beacon_chain::sync_committee_verification::{Error as SyncCommitteeError, SyncCommitteeData}; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee}; +use bls::{AggregateSignature, Keypair, SecretKey}; +use fixed_bytes::FixedBytesExtended; use int_to_bytes::int_to_bytes32; use safe_arith::SafeArith; use state_processing::{ - per_block_processing::{altair::sync_committee::process_sync_aggregate, VerifySignatures}, + per_block_processing::{VerifySignatures, altair::sync_committee::process_sync_aggregate}, state_advance::complete_state_advance, }; use std::sync::LazyLock; use store::{SignedContributionAndProof, SyncCommitteeMessage}; use tree_hash::TreeHash; +use typenum::Unsigned; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::{ - AggregateSignature, Epoch, EthSpec, FixedBytesExtended, Hash256, Keypair, MainnetEthSpec, - SecretKey, Slot, SyncContributionData, SyncSelectionProof, SyncSubnetId, Unsigned, + Epoch, EthSpec, Hash256, MainnetEthSpec, Slot, SyncContributionData, SyncSelectionProof, + SyncSubnetId, }; pub type E = MainnetEthSpec; diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index c801361fd5..17d9c5f697 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -1,19 +1,21 @@ #![cfg(not(debug_assertions))] use beacon_chain::{ + BeaconChain, ChainConfig, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, attestation_verification::Error as AttnError, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, OP_POOL_DB_KEY, }, - BeaconChain, ChainConfig, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, }; +use bls::Keypair; use operation_pool::PersistedOperationPool; +use state_processing::EpochProcessingError; use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; use std::sync::LazyLock; use types::{ - BeaconState, BeaconStateError, BlockImportSource, Checkpoint, EthSpec, Hash256, Keypair, - MinimalEthSpec, RelativeEpoch, Slot, + BeaconState, BeaconStateError, BlockImportSource, Checkpoint, EthSpec, Hash256, MinimalEthSpec, + RelativeEpoch, Slot, }; type E = MinimalEthSpec; @@ -67,11 +69,23 @@ fn massive_skips() { }; assert!(state.slot() > 1, "the state should skip at least one slot"); - assert_eq!( - error, - SlotProcessingError::BeaconStateError(BeaconStateError::InsufficientValidators), - "should return error indicating that validators have been slashed out" - ) + + if state.fork_name_unchecked().fulu_enabled() { + // post-fulu this is done in per_epoch_processing + assert_eq!( + error, + SlotProcessingError::EpochProcessingError(EpochProcessingError::BeaconStateError( + BeaconStateError::InsufficientValidators + )), + "should return error indicating that validators have been slashed out" + ) + } else { + assert_eq!( + error, + SlotProcessingError::BeaconStateError(BeaconStateError::InsufficientValidators), + "should return error indicating that validators have been slashed out" + ) + } } #[tokio::test] @@ -567,7 +581,7 @@ async fn attestations_with_increasing_slots() { let head = harness.chain.head_snapshot(); let head_state_root = head.beacon_state_root(); - attestations.extend(harness.get_unaggregated_attestations( + attestations.extend(harness.get_single_attestations( &AttestationStrategy::AllValidators, &head.beacon_state, head_state_root, @@ -584,7 +598,7 @@ async fn attestations_with_increasing_slots() { .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); - let expected_attestation_slot = attestation.data().slot; + let expected_attestation_slot = attestation.data.slot; let expected_earliest_permissible_slot = current_slot - MinimalEthSpec::slots_per_epoch() - 1; @@ -1022,11 +1036,13 @@ async fn pseudo_finalize_test_generic( // This is a regression test for https://github.com/sigp/lighthouse/pull/7105 if !expect_true_finalization_migration { assert_eq!(expected_split_slot, pseudo_finalized_slot); - assert!(!harness - .chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&split.block_root)); + assert!( + !harness + .chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&split.block_root) + ); } } diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs index bca37b4e6d..521fc4ac97 100644 --- a/beacon_node/beacon_chain/tests/validator_monitor.rs +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -1,9 +1,10 @@ use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; -use beacon_chain::validator_monitor::{ValidatorMonitorConfig, MISSED_BLOCK_LAG_SLOTS}; +use beacon_chain::validator_monitor::{MISSED_BLOCK_LAG_SLOTS, ValidatorMonitorConfig}; +use bls::{Keypair, PublicKeyBytes}; use std::sync::LazyLock; -use types::{Epoch, EthSpec, Keypair, MainnetEthSpec, PublicKeyBytes, Slot}; +use types::{Epoch, EthSpec, Hash256, MainnetEthSpec, Slot}; // Should ideally be divisible by 3. pub const VALIDATOR_COUNT: usize = 48; @@ -74,14 +75,14 @@ async fn missed_blocks_across_epochs() { .get_hot_state(state_roots_by_slot[&start_slot]) .unwrap(); let decision_root = state - .proposer_shuffling_decision_root(genesis_block_root) + .proposer_shuffling_decision_root(genesis_block_root, &harness.chain.spec) .unwrap(); proposer_shuffling_cache .insert( epoch, decision_root, state - .get_beacon_proposer_indices(&harness.chain.spec) + .get_beacon_proposer_indices(epoch, &harness.chain.spec) .unwrap(), state.fork(), ) @@ -147,10 +148,12 @@ async fn missed_blocks_basic() { let mut slot_in_epoch = slot % slots_per_epoch; let mut prev_slot = Slot::new(idx - 1); let mut duplicate_block_root = *_state.block_roots().get(idx as usize).unwrap(); - let mut validator_indexes = _state.get_beacon_proposer_indices(&harness1.spec).unwrap(); + let mut validator_indexes = _state + .get_beacon_proposer_indices(epoch, &harness1.spec) + .unwrap(); let mut missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; let mut proposer_shuffling_decision_root = _state - .proposer_shuffling_decision_root(duplicate_block_root) + .proposer_shuffling_decision_root(duplicate_block_root, &harness1.chain.spec) .unwrap(); let beacon_proposer_cache = harness1 @@ -219,7 +222,9 @@ async fn missed_blocks_basic() { prev_slot = Slot::new(idx - 1); slot_in_epoch = slot % slots_per_epoch; duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); - validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); + validator_indexes = _state2 + .get_beacon_proposer_indices(epoch, &harness2.spec) + .unwrap(); missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; let beacon_proposer_cache = harness2 @@ -231,17 +236,20 @@ async fn missed_blocks_basic() { // Let's fill the cache with the proposers for the current epoch // and push the duplicate_block_root to the block_roots vector assert_eq!( - beacon_proposer_cache.lock().insert( - epoch, - duplicate_block_root, - validator_indexes.clone(), - _state2.fork() - ), + _state2.set_block_root(prev_slot, duplicate_block_root), Ok(()) ); + let decision_block_root = _state2 + .proposer_shuffling_decision_root_at_epoch(epoch, Hash256::ZERO, &harness2.chain.spec) + .unwrap(); assert_eq!( - _state2.set_block_root(prev_slot, duplicate_block_root), + beacon_proposer_cache.lock().insert( + epoch, + decision_block_root, + validator_indexes.clone(), + _state2.fork() + ), Ok(()) ); @@ -317,10 +325,16 @@ async fn missed_blocks_basic() { slot_in_epoch = slot % slots_per_epoch; prev_slot = Slot::new(idx - 1); duplicate_block_root = *_state3.block_roots().get(idx as usize).unwrap(); - validator_indexes = _state3.get_beacon_proposer_indices(&harness3.spec).unwrap(); + validator_indexes = _state3 + .get_beacon_proposer_indices(epoch, &harness3.spec) + .unwrap(); missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; proposer_shuffling_decision_root = _state3 - .proposer_shuffling_decision_root_at_epoch(epoch, duplicate_block_root) + .proposer_shuffling_decision_root_at_epoch( + epoch, + duplicate_block_root, + &harness1.chain.spec, + ) .unwrap(); let beacon_proposer_cache = harness3 diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index eb4b96255c..da6acfaf2e 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -39,14 +39,15 @@ //! task. use crate::work_reprocessing_queue::{ - QueuedBackfillBatch, QueuedGossipBlock, ReprocessQueueMessage, + QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, ReprocessQueueMessage, }; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; use lighthouse_network::{MessageId, NetworkGlobals, PeerId}; -use logging::crit; use logging::TimeLatch; +use logging::crit; use parking_lot::Mutex; +pub use scheduler::work_reprocessing_queue; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use std::cmp; @@ -56,24 +57,24 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; use std::task::Context; -use std::time::Duration; +use std::time::{Duration, Instant}; use strum::IntoStaticStr; -use task_executor::TaskExecutor; +use task_executor::{RayonPoolType, TaskExecutor}; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TrySendError; use tracing::{debug, error, trace, warn}; use types::{ - Attestation, BeaconState, ChainSpec, EthSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, + BeaconState, ChainSpec, EthSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, }; +use work_reprocessing_queue::IgnoredRpcBlock; use work_reprocessing_queue::{ - spawn_reprocess_scheduler, QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock, - QueuedUnaggregate, ReadyWork, + QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock, QueuedUnaggregate, ReadyWork, + spawn_reprocess_scheduler, }; -use work_reprocessing_queue::{IgnoredRpcBlock, QueuedSamplingRequest}; mod metrics; -pub mod work_reprocessing_queue; +pub mod scheduler; /// The maximum size of the channel for work events to the `BeaconProcessor`. /// @@ -111,12 +112,10 @@ pub struct BeaconProcessorQueueLengths { gossip_proposer_slashing_queue: usize, gossip_attester_slashing_queue: usize, unknown_light_client_update_queue: usize, - unknown_block_sampling_request_queue: usize, rpc_block_queue: usize, rpc_blob_queue: usize, rpc_custody_column_queue: usize, - rpc_verify_data_column_queue: usize, - sampling_result_queue: usize, + column_reconstruction_queue: usize, chain_segment_queue: usize, backfill_chain_segment: usize, gossip_block_queue: usize, @@ -124,10 +123,10 @@ pub struct BeaconProcessorQueueLengths { gossip_data_column_queue: usize, delayed_block_queue: usize, status_queue: usize, - bbrange_queue: usize, - bbroots_queue: usize, - blbroots_queue: usize, - blbrange_queue: usize, + block_brange_queue: usize, + block_broots_queue: usize, + blob_broots_queue: usize, + blob_brange_queue: usize, dcbroots_queue: usize, dcbrange_queue: usize, gossip_bls_to_execution_change_queue: usize, @@ -180,11 +179,10 @@ impl BeaconProcessorQueueLengths { unknown_light_client_update_queue: 128, rpc_block_queue: 1024, rpc_blob_queue: 1024, - // TODO(das): Placeholder values - rpc_custody_column_queue: 1000, - rpc_verify_data_column_queue: 1000, - unknown_block_sampling_request_queue: 16384, - sampling_result_queue: 1000, + // We don't request more than `PARENT_DEPTH_TOLERANCE` (32) lookups, so we can limit + // this queue size. With 48 max blobs per block, each column sidecar list could be up to 12MB. + rpc_custody_column_queue: 64, + column_reconstruction_queue: 1, chain_segment_queue: 64, backfill_chain_segment: 64, gossip_block_queue: 1024, @@ -192,11 +190,10 @@ impl BeaconProcessorQueueLengths { gossip_data_column_queue: 1024, delayed_block_queue: 1024, status_queue: 1024, - bbrange_queue: 1024, - bbroots_queue: 1024, - blbroots_queue: 1024, - blbrange_queue: 1024, - // TODO(das): pick proper values + block_brange_queue: 1024, + block_broots_queue: 1024, + blob_broots_queue: 1024, + blob_brange_queue: 1024, dcbroots_queue: 1024, dcbrange_queue: 1024, gossip_bls_to_execution_change_queue: 16384, @@ -265,22 +262,16 @@ impl Default for BeaconProcessorConfig { pub struct BeaconProcessorChannels { pub beacon_processor_tx: BeaconProcessorSend, pub beacon_processor_rx: mpsc::Receiver>, - pub work_reprocessing_tx: mpsc::Sender, - pub work_reprocessing_rx: mpsc::Receiver, } impl BeaconProcessorChannels { pub fn new(config: &BeaconProcessorConfig) -> Self { let (beacon_processor_tx, beacon_processor_rx) = mpsc::channel(config.max_work_event_queue_len); - let (work_reprocessing_tx, work_reprocessing_rx) = - mpsc::channel(config.max_scheduled_work_queue_len); Self { beacon_processor_tx: BeaconProcessorSend(beacon_processor_tx), beacon_processor_rx, - work_reprocessing_rx, - work_reprocessing_tx, } } } @@ -493,14 +484,16 @@ impl From for WorkEvent { process_fn, }, }, - ReadyWork::SamplingRequest(QueuedSamplingRequest { process_fn, .. }) => Self { - drop_during_sync: true, - work: Work::UnknownBlockSamplingRequest { process_fn }, - }, ReadyWork::BackfillSync(QueuedBackfillBatch(process_fn)) => Self { drop_during_sync: false, work: Work::ChainSegmentBackfill(process_fn), }, + ReadyWork::ColumnReconstruction(QueuedColumnReconstruction { process_fn, .. }) => { + Self { + drop_during_sync: true, + work: Work::ColumnReconstruction(process_fn), + } + } } } } @@ -552,32 +545,23 @@ pub enum BlockingOrAsync { Blocking(BlockingFn), Async(AsyncFn), } -pub type GossipAttestationBatch = Vec>>; +pub type GossipAttestationBatch = Vec>; /// Indicates the type of work to be performed and therefore its priority and /// queuing specifics. pub enum Work { GossipAttestation { - attestation: Box>>, - process_individual: Box>) + Send + Sync>, - process_batch: Box) + Send + Sync>, - }, - // Attestation requiring conversion before processing. - // - // For now this is a `SingleAttestation`, but eventually we will switch this around so that - // legacy `Attestation`s are converted and the main processing pipeline operates on - // `SingleAttestation`s. - GossipAttestationToConvert { attestation: Box>, process_individual: Box) + Send + Sync>, + process_batch: Box, }, UnknownBlockAttestation { process_fn: BlockingFn, }, GossipAttestationBatch { - attestations: GossipAttestationBatch, - process_batch: Box) + Send + Sync>, + attestations: GossipAttestationBatch, + process_batch: Box, }, GossipAggregate { aggregate: Box>, @@ -591,9 +575,6 @@ pub enum Work { parent_root: Hash256, process_fn: BlockingFn, }, - UnknownBlockSamplingRequest { - process_fn: BlockingFn, - }, GossipAggregateBatch { aggregates: Vec>, process_batch: Box>) + Send + Sync>, @@ -620,13 +601,12 @@ pub enum Work { process_fn: AsyncFn, }, RpcCustodyColumn(AsyncFn), - RpcVerifyDataColumn(AsyncFn), - SamplingResult(AsyncFn), + ColumnReconstruction(AsyncFn), IgnoredRpcBlock { process_fn: BlockingFn, }, ChainSegment(AsyncFn), - ChainSegmentBackfill(AsyncFn), + ChainSegmentBackfill(BlockingFn), Status(BlockingFn), BlocksByRangeRequest(AsyncFn), BlocksByRootsRequest(AsyncFn), @@ -642,6 +622,7 @@ pub enum Work { GossipInclusionList(BlockingFn), ApiRequestP0(BlockingOrAsync), ApiRequestP1(BlockingOrAsync), + Reprocess(ReprocessQueueMessage), } impl fmt::Debug for Work { @@ -650,7 +631,7 @@ impl fmt::Debug for Work { } } -#[derive(IntoStaticStr, PartialEq, Eq, Debug)] +#[derive(IntoStaticStr, PartialEq, Eq, Debug, Clone)] #[strum(serialize_all = "snake_case")] pub enum WorkType { GossipAttestation, @@ -660,7 +641,6 @@ pub enum WorkType { GossipAggregate, UnknownBlockAggregate, UnknownLightClientOptimisticUpdate, - UnknownBlockSamplingRequest, GossipAggregateBatch, GossipBlock, GossipBlobSidecar, @@ -676,8 +656,7 @@ pub enum WorkType { RpcBlock, RpcBlobs, RpcCustodyColumn, - RpcVerifyDataColumn, - SamplingResult, + ColumnReconstruction, IgnoredRpcBlock, ChainSegment, ChainSegmentBackfill, @@ -696,6 +675,7 @@ pub enum WorkType { GossipInclusionList, ApiRequestP0, ApiRequestP1, + Reprocess, } impl Work { @@ -707,7 +687,6 @@ impl Work { fn to_type(&self) -> WorkType { match self { Work::GossipAttestation { .. } => WorkType::GossipAttestation, - Work::GossipAttestationToConvert { .. } => WorkType::GossipAttestationToConvert, Work::GossipAttestationBatch { .. } => WorkType::GossipAttestationBatch, Work::GossipAggregate { .. } => WorkType::GossipAggregate, Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch, @@ -728,8 +707,7 @@ impl Work { Work::RpcBlock { .. } => WorkType::RpcBlock, Work::RpcBlobs { .. } => WorkType::RpcBlobs, Work::RpcCustodyColumn { .. } => WorkType::RpcCustodyColumn, - Work::RpcVerifyDataColumn { .. } => WorkType::RpcVerifyDataColumn, - Work::SamplingResult { .. } => WorkType::SamplingResult, + Work::ColumnReconstruction(_) => WorkType::ColumnReconstruction, Work::IgnoredRpcBlock { .. } => WorkType::IgnoredRpcBlock, Work::ChainSegment { .. } => WorkType::ChainSegment, Work::ChainSegmentBackfill(_) => WorkType::ChainSegmentBackfill, @@ -748,13 +726,13 @@ impl Work { Work::LightClientUpdatesByRangeRequest(_) => WorkType::LightClientUpdatesByRangeRequest, Work::UnknownBlockAttestation { .. } => WorkType::UnknownBlockAttestation, Work::UnknownBlockAggregate { .. } => WorkType::UnknownBlockAggregate, - Work::UnknownBlockSamplingRequest { .. } => WorkType::UnknownBlockSamplingRequest, Work::UnknownLightClientOptimisticUpdate { .. } => { WorkType::UnknownLightClientOptimisticUpdate } Work::GossipInclusionList { .. } => WorkType::GossipInclusionList, Work::ApiRequestP0 { .. } => WorkType::ApiRequestP0, Work::ApiRequestP1 { .. } => WorkType::ApiRequestP1, + Work::Reprocess { .. } => WorkType::Reprocess, } } } @@ -764,9 +742,9 @@ enum InboundEvent { /// A worker has completed a task and is free. WorkerIdle, /// There is new work to be done. - WorkEvent(WorkEvent), + WorkEvent((WorkEvent, Instant)), /// A work event that was queued for re-processing has become ready. - ReprocessingWork(WorkEvent), + ReprocessingWork((WorkEvent, Instant)), } /// Combines the various incoming event streams for the `BeaconProcessor` into a single stream. @@ -775,11 +753,11 @@ enum InboundEvent { /// control (specifically in the ordering of event processing). struct InboundEvents { /// Used by workers when they finish a task. - idle_rx: mpsc::Receiver<()>, + idle_rx: mpsc::Receiver, /// Used by upstream processes to send new work to the `BeaconProcessor`. event_rx: mpsc::Receiver>, /// Used internally for queuing work ready to be re-processed. - reprocess_work_rx: mpsc::Receiver, + ready_work_rx: mpsc::Receiver, } impl Stream for InboundEvents { @@ -789,7 +767,7 @@ impl Stream for InboundEvents { // Always check for idle workers before anything else. This allows us to ensure that a big // stream of new events doesn't suppress the processing of existing events. match self.idle_rx.poll_recv(cx) { - Poll::Ready(Some(())) => { + Poll::Ready(Some(_)) => { return Poll::Ready(Some(InboundEvent::WorkerIdle)); } Poll::Ready(None) => { @@ -800,9 +778,12 @@ impl Stream for InboundEvents { // Poll for delayed blocks before polling for new work. It might be the case that a delayed // block is required to successfully process some new work. - match self.reprocess_work_rx.poll_recv(cx) { + match self.ready_work_rx.poll_recv(cx) { Poll::Ready(Some(ready_work)) => { - return Poll::Ready(Some(InboundEvent::ReprocessingWork(ready_work.into()))); + return Poll::Ready(Some(InboundEvent::ReprocessingWork(( + ready_work.into(), + Instant::now(), + )))); } Poll::Ready(None) => { return Poll::Ready(None); @@ -812,7 +793,7 @@ impl Stream for InboundEvents { match self.event_rx.poll_recv(cx) { Poll::Ready(Some(event)) => { - return Poll::Ready(Some(InboundEvent::WorkEvent(event))); + return Poll::Ready(Some(InboundEvent::WorkEvent((event, Instant::now())))); } Poll::Ready(None) => { return Poll::Ready(None); @@ -851,15 +832,13 @@ impl BeaconProcessor { pub fn spawn_manager( mut self, event_rx: mpsc::Receiver>, - work_reprocessing_tx: mpsc::Sender, - work_reprocessing_rx: mpsc::Receiver, work_journal_tx: Option>, slot_clock: S, maximum_gossip_clock_disparity: Duration, queue_lengths: BeaconProcessorQueueLengths, ) -> Result<(), String> { // Used by workers to communicate that they are finished a task. - let (idle_tx, idle_rx) = mpsc::channel::<()>(MAX_IDLE_QUEUE_LEN); + let (idle_tx, idle_rx) = mpsc::channel::(MAX_IDLE_QUEUE_LEN); // Using LIFO queues for attestations since validator profits rely upon getting fresh // attestations into blocks. Additionally, later attestations contain more information than @@ -893,12 +872,8 @@ impl BeaconProcessor { let mut rpc_block_queue = FifoQueue::new(queue_lengths.rpc_block_queue); let mut rpc_blob_queue = FifoQueue::new(queue_lengths.rpc_blob_queue); let mut rpc_custody_column_queue = FifoQueue::new(queue_lengths.rpc_custody_column_queue); - let mut rpc_verify_data_column_queue = - FifoQueue::new(queue_lengths.rpc_verify_data_column_queue); - // TODO(das): the sampling_request_queue is never read - let mut sampling_result_queue = FifoQueue::new(queue_lengths.sampling_result_queue); - let mut unknown_block_sampling_request_queue = - FifoQueue::new(queue_lengths.unknown_block_sampling_request_queue); + let mut column_reconstruction_queue = + LifoQueue::new(queue_lengths.column_reconstruction_queue); let mut chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let mut backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let mut gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); @@ -907,10 +882,10 @@ impl BeaconProcessor { let mut delayed_block_queue = FifoQueue::new(queue_lengths.delayed_block_queue); let mut status_queue = FifoQueue::new(queue_lengths.status_queue); - let mut bbrange_queue = FifoQueue::new(queue_lengths.bbrange_queue); - let mut bbroots_queue = FifoQueue::new(queue_lengths.bbroots_queue); - let mut blbroots_queue = FifoQueue::new(queue_lengths.blbroots_queue); - let mut blbrange_queue = FifoQueue::new(queue_lengths.blbrange_queue); + let mut block_brange_queue = FifoQueue::new(queue_lengths.block_brange_queue); + let mut block_broots_queue = FifoQueue::new(queue_lengths.block_broots_queue); + let mut blob_broots_queue = FifoQueue::new(queue_lengths.blob_broots_queue); + let mut blob_brange_queue = FifoQueue::new(queue_lengths.blob_brange_queue); let mut dcbroots_queue = FifoQueue::new(queue_lengths.dcbroots_queue); let mut dcbrange_queue = FifoQueue::new(queue_lengths.dcbrange_queue); @@ -940,9 +915,13 @@ impl BeaconProcessor { // receive them back once they are ready (`ready_work_rx`). let (ready_work_tx, ready_work_rx) = mpsc::channel::(self.config.max_scheduled_work_queue_len); + + let (reprocess_work_tx, reprocess_work_rx) = + mpsc::channel::(self.config.max_scheduled_work_queue_len); + spawn_reprocess_scheduler( ready_work_tx, - work_reprocessing_rx, + reprocess_work_rx, &self.executor, Arc::new(slot_clock), maximum_gossip_clock_disparity, @@ -956,21 +935,23 @@ impl BeaconProcessor { let mut inbound_events = InboundEvents { idle_rx, event_rx, - reprocess_work_rx: ready_work_rx, + ready_work_rx, }; let enable_backfill_rate_limiting = self.config.enable_backfill_rate_limiting; loop { - let work_event = match inbound_events.next().await { + let (work_event, created_timestamp) = match inbound_events.next().await { Some(InboundEvent::WorkerIdle) => { self.current_workers = self.current_workers.saturating_sub(1); - None + (None, Instant::now()) } - Some(InboundEvent::WorkEvent(event)) if enable_backfill_rate_limiting => { + Some(InboundEvent::WorkEvent((event, created_timestamp))) + if enable_backfill_rate_limiting => + { match QueuedBackfillBatch::try_from(event) { Ok(backfill_batch) => { - match work_reprocessing_tx + match reprocess_work_tx .try_send(ReprocessQueueMessage::BackfillSync(backfill_batch)) { Err(e) => { @@ -984,7 +965,10 @@ impl BeaconProcessor { match reprocess_queue_message { ReprocessQueueMessage::BackfillSync( backfill_batch, - ) => Some(backfill_batch.into()), + ) => ( + Some(backfill_batch.into()), + created_timestamp, + ), other => { crit!( message_type = other.as_ref(), @@ -1003,11 +987,13 @@ impl BeaconProcessor { } } } - Err(event) => Some(event), + Err(event) => (Some(event), created_timestamp), } } - Some(InboundEvent::WorkEvent(event)) - | Some(InboundEvent::ReprocessingWork(event)) => Some(event), + Some(InboundEvent::WorkEvent((event, created_timestamp))) + | Some(InboundEvent::ReprocessingWork((event, created_timestamp))) => { + (Some(event), created_timestamp) + } None => { debug!(msg = "stream ended", "Gossip processor stopped"); break; @@ -1032,8 +1018,10 @@ impl BeaconProcessor { .unwrap_or(WORKER_FREED); // We don't care if this message was successfully sent, we only use the journal - // during testing. - let _ = work_journal_tx.try_send(id); + // during testing. We also ignore reprocess messages to ensure our test cases can pass. + if id != "reprocess" { + let _ = work_journal_tx.try_send(id); + } } let can_spawn = self.current_workers < self.config.max_workers; @@ -1061,13 +1049,8 @@ impl BeaconProcessor { Some(item) } else if let Some(item) = rpc_custody_column_queue.pop() { Some(item) - // TODO(das): decide proper prioritization for sampling columns } else if let Some(item) = rpc_custody_column_queue.pop() { Some(item) - } else if let Some(item) = rpc_verify_data_column_queue.pop() { - Some(item) - } else if let Some(item) = sampling_result_queue.pop() { - Some(item) // Check delayed blocks before gossip blocks, the gossip blocks might rely // on the delayed ones. } else if let Some(item) = delayed_block_queue.pop() { @@ -1080,6 +1063,8 @@ impl BeaconProcessor { Some(item) } else if let Some(item) = gossip_data_column_queue.pop() { Some(item) + } else if let Some(item) = column_reconstruction_queue.pop() { + Some(item) // Check the priority 0 API requests after blocks and blobs, but before attestations. } else if let Some(item) = api_request_p0_queue.pop() { Some(item) @@ -1215,21 +1200,18 @@ impl BeaconProcessor { // and BlocksByRoot) } else if let Some(item) = status_queue.pop() { Some(item) - } else if let Some(item) = bbrange_queue.pop() { + } else if let Some(item) = block_brange_queue.pop() { Some(item) - } else if let Some(item) = bbroots_queue.pop() { + } else if let Some(item) = block_broots_queue.pop() { Some(item) - } else if let Some(item) = blbrange_queue.pop() { + } else if let Some(item) = blob_brange_queue.pop() { Some(item) - } else if let Some(item) = blbroots_queue.pop() { + } else if let Some(item) = blob_broots_queue.pop() { Some(item) } else if let Some(item) = dcbroots_queue.pop() { Some(item) } else if let Some(item) = dcbrange_queue.pop() { Some(item) - // Prioritize sampling requests after block syncing requests - } else if let Some(item) = unknown_block_sampling_request_queue.pop() { - Some(item) // Check slashings after all other consensus messages so we prioritize // following head. // @@ -1283,7 +1265,7 @@ impl BeaconProcessor { if let Some(work_event) = work_event { let work_type = work_event.to_type(); - self.spawn_worker(work_event, idle_tx); + self.spawn_worker(work_event, created_timestamp, idle_tx); Some(work_type) } else { None @@ -1323,11 +1305,16 @@ impl BeaconProcessor { let work_type = work.to_type(); match work { - _ if can_spawn => self.spawn_worker(work, idle_tx), - Work::GossipAttestation { .. } => attestation_queue.push(work), - Work::GossipAttestationToConvert { .. } => { - attestation_to_convert_queue.push(work) + Work::Reprocess(work_event) => { + if let Err(e) = reprocess_work_tx.try_send(work_event) { + error!( + error = ?e, + "Failed to reprocess work event" + ) + } } + _ if can_spawn => self.spawn_worker(work, created_timestamp, idle_tx), + Work::GossipAttestation { .. } => attestation_queue.push(work), // Attestation batches are formed internally within the // `BeaconProcessor`, they are not sent from external services. Work::GossipAttestationBatch { .. } => crit!( @@ -1351,6 +1338,9 @@ impl BeaconProcessor { Work::DelayedImportBlock { .. } => { delayed_block_queue.push(work, work_id) } + Work::GossipInclusionList { .. } => { + gossip_inclusion_list_queue.push(work, work_id) + } Work::GossipVoluntaryExit { .. } => { gossip_voluntary_exit_queue.push(work, work_id) } @@ -1377,18 +1367,21 @@ impl BeaconProcessor { Work::RpcCustodyColumn { .. } => { rpc_custody_column_queue.push(work, work_id) } - Work::RpcVerifyDataColumn(_) => { - rpc_verify_data_column_queue.push(work, work_id) - } - Work::SamplingResult(_) => sampling_result_queue.push(work, work_id), + Work::ColumnReconstruction(_) => column_reconstruction_queue.push(work), Work::ChainSegment { .. } => chain_segment_queue.push(work, work_id), Work::ChainSegmentBackfill { .. } => { backfill_chain_segment.push(work, work_id) } Work::Status { .. } => status_queue.push(work, work_id), - Work::BlocksByRangeRequest { .. } => bbrange_queue.push(work, work_id), - Work::BlocksByRootsRequest { .. } => bbroots_queue.push(work, work_id), - Work::BlobsByRangeRequest { .. } => blbrange_queue.push(work, work_id), + Work::BlocksByRangeRequest { .. } => { + block_brange_queue.push(work, work_id) + } + Work::BlocksByRootsRequest { .. } => { + block_broots_queue.push(work, work_id) + } + Work::BlobsByRangeRequest { .. } => { + blob_brange_queue.push(work, work_id) + } Work::LightClientBootstrapRequest { .. } => { lc_bootstrap_queue.push(work, work_id) } @@ -1410,7 +1403,9 @@ impl BeaconProcessor { Work::GossipBlsToExecutionChange { .. } => { gossip_bls_to_execution_change_queue.push(work, work_id) } - Work::BlobsByRootsRequest { .. } => blbroots_queue.push(work, work_id), + Work::BlobsByRootsRequest { .. } => { + blob_broots_queue.push(work, work_id) + } Work::DataColumnsByRootsRequest { .. } => { dcbroots_queue.push(work, work_id) } @@ -1420,12 +1415,6 @@ impl BeaconProcessor { Work::UnknownLightClientOptimisticUpdate { .. } => { unknown_light_client_update_queue.push(work, work_id) } - Work::UnknownBlockSamplingRequest { .. } => { - unknown_block_sampling_request_queue.push(work, work_id) - } - Work::GossipInclusionList { .. } => { - gossip_inclusion_list_queue.push(work, work_id) - } Work::ApiRequestP0 { .. } => api_request_p0_queue.push(work, work_id), Work::ApiRequestP1 { .. } => api_request_p1_queue.push(work, work_id), }; @@ -1433,11 +1422,6 @@ impl BeaconProcessor { } }; - metrics::set_gauge( - &metrics::BEACON_PROCESSOR_WORKERS_ACTIVE_TOTAL, - self.current_workers as i64, - ); - if let Some(modified_queue_id) = modified_queue_id { let queue_len = match modified_queue_id { WorkType::GossipAttestation => attestation_queue.len(), @@ -1449,9 +1433,6 @@ impl BeaconProcessor { WorkType::UnknownLightClientOptimisticUpdate => { unknown_light_client_update_queue.len() } - WorkType::UnknownBlockSamplingRequest => { - unknown_block_sampling_request_queue.len() - } WorkType::GossipAggregateBatch => 0, // No queue WorkType::GossipBlock => gossip_block_queue.len(), WorkType::GossipBlobSidecar => gossip_blob_queue.len(), @@ -1471,15 +1452,14 @@ impl BeaconProcessor { WorkType::RpcBlock => rpc_block_queue.len(), WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => rpc_blob_queue.len(), WorkType::RpcCustodyColumn => rpc_custody_column_queue.len(), - WorkType::RpcVerifyDataColumn => rpc_verify_data_column_queue.len(), - WorkType::SamplingResult => sampling_result_queue.len(), + WorkType::ColumnReconstruction => column_reconstruction_queue.len(), WorkType::ChainSegment => chain_segment_queue.len(), WorkType::ChainSegmentBackfill => backfill_chain_segment.len(), WorkType::Status => status_queue.len(), - WorkType::BlocksByRangeRequest => blbrange_queue.len(), - WorkType::BlocksByRootsRequest => blbroots_queue.len(), - WorkType::BlobsByRangeRequest => bbrange_queue.len(), - WorkType::BlobsByRootsRequest => bbroots_queue.len(), + WorkType::BlocksByRangeRequest => block_brange_queue.len(), + WorkType::BlocksByRootsRequest => block_broots_queue.len(), + WorkType::BlobsByRangeRequest => blob_brange_queue.len(), + WorkType::BlobsByRootsRequest => blob_broots_queue.len(), WorkType::DataColumnsByRootsRequest => dcbroots_queue.len(), WorkType::DataColumnsByRangeRequest => dcbrange_queue.len(), WorkType::GossipBlsToExecutionChange => { @@ -1496,6 +1476,7 @@ impl BeaconProcessor { WorkType::LightClientUpdatesByRangeRequest => lc_update_range_queue.len(), WorkType::ApiRequestP0 => api_request_p0_queue.len(), WorkType::ApiRequestP1 => api_request_p1_queue.len(), + WorkType::Reprocess => 0, }; metrics::observe_vec( &metrics::BEACON_PROCESSOR_QUEUE_LENGTH, @@ -1530,8 +1511,22 @@ impl BeaconProcessor { /// Spawns a blocking worker thread to process some `Work`. /// /// Sends an message on `idle_tx` when the work is complete and the task is stopping. - fn spawn_worker(&mut self, work: Work, idle_tx: mpsc::Sender<()>) { + fn spawn_worker( + &mut self, + work: Work, + created_timestamp: Instant, + idle_tx: mpsc::Sender, + ) { let work_id = work.str_id(); + let work_type = work.to_type(); + + // This metric tracks how long a work event has been in the queue + metrics::observe_timer_vec( + &metrics::BEACON_PROCESSOR_QUEUE_TIME, + &[work_type.into()], + Instant::now() - created_timestamp, + ); + let worker_timer = metrics::start_timer_vec(&metrics::BEACON_PROCESSOR_WORKER_TIME, &[work_id]); metrics::inc_counter(&metrics::BEACON_PROCESSOR_WORKERS_SPAWNED_TOTAL); @@ -1540,12 +1535,18 @@ impl BeaconProcessor { &[work.str_id()], ); + metrics::inc_gauge_vec( + &metrics::BEACON_PROCESSOR_WORKERS_ACTIVE_GAUGE_BY_TYPE, + &[work_id], + ); + // Wrap the `idle_tx` in a struct that will fire the idle message whenever it is dropped. // // This helps ensure that the worker is always freed in the case of an early exit or panic. // As such, this instantiation should happen as early in the function as possible. let send_idle_on_drop = SendOnDrop { tx: idle_tx, + work_type: work.to_type(), _worker_timer: worker_timer, }; @@ -1573,12 +1574,6 @@ impl BeaconProcessor { } => task_spawner.spawn_blocking(move || { process_individual(*attestation); }), - Work::GossipAttestationToConvert { - attestation, - process_individual, - } => task_spawner.spawn_blocking(move || { - process_individual(*attestation); - }), Work::GossipAttestationBatch { attestations, process_batch, @@ -1603,8 +1598,7 @@ impl BeaconProcessor { }), Work::UnknownBlockAttestation { process_fn } | Work::UnknownBlockAggregate { process_fn } - | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } - | Work::UnknownBlockSamplingRequest { process_fn } => { + | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } => { task_spawner.spawn_blocking(process_fn) } Work::DelayedImportBlock { @@ -1615,8 +1609,7 @@ impl BeaconProcessor { Work::RpcBlock { process_fn } | Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) - | Work::RpcVerifyDataColumn(process_fn) - | Work::SamplingResult(process_fn) => task_spawner.spawn_async(process_fn), + | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) | Work::GossipBlobSidecar(work) @@ -1632,7 +1625,14 @@ impl BeaconProcessor { Work::BlocksByRangeRequest(work) | Work::BlocksByRootsRequest(work) => { task_spawner.spawn_async(work) } - Work::ChainSegmentBackfill(process_fn) => task_spawner.spawn_async(process_fn), + Work::ChainSegmentBackfill(process_fn) => { + if self.config.enable_backfill_rate_limiting { + task_spawner.spawn_blocking_with_rayon(RayonPoolType::LowPriority, process_fn) + } else { + // use the global rayon thread pool if backfill rate limiting is disabled. + task_spawner.spawn_blocking(process_fn) + } + } Work::ApiRequestP0(process_fn) | Work::ApiRequestP1(process_fn) => match process_fn { BlockingOrAsync::Blocking(process_fn) => task_spawner.spawn_blocking(process_fn), BlockingOrAsync::Async(process_fn) => task_spawner.spawn_async(process_fn), @@ -1646,11 +1646,14 @@ impl BeaconProcessor { | Work::GossipLightClientOptimisticUpdate(process_fn) | Work::Status(process_fn) | Work::GossipBlsToExecutionChange(process_fn) + | Work::GossipInclusionList(process_fn) | Work::LightClientBootstrapRequest(process_fn) | Work::LightClientOptimisticUpdateRequest(process_fn) | Work::LightClientFinalityUpdateRequest(process_fn) - | Work::LightClientUpdatesByRangeRequest(process_fn) - | Work::GossipInclusionList(process_fn) => task_spawner.spawn_blocking(process_fn), + | Work::LightClientUpdatesByRangeRequest(process_fn) => { + task_spawner.spawn_blocking(process_fn) + } + Work::Reprocess(_) => {} }; } } @@ -1692,6 +1695,21 @@ impl TaskSpawner { WORKER_TASK_NAME, ) } + + /// Spawns a blocking task on a rayon thread pool, dropping the `SendOnDrop` after task completion. + fn spawn_blocking_with_rayon(self, rayon_pool_type: RayonPoolType, task: F) + where + F: FnOnce() + Send + 'static, + { + self.executor.spawn_blocking_with_rayon( + move || { + task(); + drop(self.send_idle_on_drop) + }, + rayon_pool_type, + WORKER_TASK_NAME, + ) + } } /// This struct will send a message on `self.tx` when it is dropped. An error will be logged @@ -1705,14 +1723,20 @@ impl TaskSpawner { /// /// https://doc.rust-lang.org/std/ops/trait.Drop.html#panics pub struct SendOnDrop { - tx: mpsc::Sender<()>, + tx: mpsc::Sender, + work_type: WorkType, // The field is unused, but it's here to ensure the timer is dropped once the task has finished. _worker_timer: Option, } impl Drop for SendOnDrop { fn drop(&mut self) { - if let Err(e) = self.tx.try_send(()) { + metrics::dec_gauge_vec( + &metrics::BEACON_PROCESSOR_WORKERS_ACTIVE_GAUGE_BY_TYPE, + &[self.work_type.clone().into()], + ); + + if let Err(e) = self.tx.try_send(self.work_type.clone()) { warn!( msg = "did not free worker, shutdown may be underway", error = %e, diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index fc8c712f4e..3770473df5 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -42,11 +42,12 @@ pub static BEACON_PROCESSOR_WORKERS_SPAWNED_TOTAL: LazyLock> "The number of workers ever spawned by the gossip processing pool.", ) }); -pub static BEACON_PROCESSOR_WORKERS_ACTIVE_TOTAL: LazyLock> = +pub static BEACON_PROCESSOR_WORKERS_ACTIVE_GAUGE_BY_TYPE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_processor_workers_active_total", - "Count of active workers in the gossip processing pool.", + try_create_int_gauge_vec( + "beacon_processor_workers_active_gauge_by_type", + "Int gauge of the number of active workers per work type", + &["type"], ) }); pub static BEACON_PROCESSOR_IDLE_EVENTS_TOTAL: LazyLock> = LazyLock::new(|| { @@ -87,9 +88,9 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_processor_reprocessing_queue_expired_attestations", - "Number of queued attestations which have expired before a matching block has been found." - ) + "beacon_processor_reprocessing_queue_expired_attestations", + "Number of queued attestations which have expired before a matching block has been found.", + ) }); pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_ATTESTATIONS: LazyLock> = LazyLock::new(|| { @@ -98,15 +99,6 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_ATTESTATIONS: LazyLock, -> = LazyLock::new(|| { - try_create_int_counter( - "beacon_processor_reprocessing_queue_matched_sampling_requests", - "Number of queued sampling requests where a matching block has been imported.", - ) -}); /* * Light client update reprocessing queue metrics. @@ -116,7 +108,7 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_EXPIRED_OPTIMISTIC_UPDATES: LazyL > = LazyLock::new(|| { try_create_int_counter( "beacon_processor_reprocessing_queue_expired_optimistic_updates", - "Number of queued light client optimistic updates which have expired before a matching block has been found." + "Number of queued light client optimistic updates which have expired before a matching block has been found.", ) }); pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: LazyLock< @@ -124,7 +116,7 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: LazyL > = LazyLock::new(|| { try_create_int_counter( "beacon_processor_reprocessing_queue_matched_optimistic_updates", - "Number of queued light client optimistic updates where a matching block has been imported." + "Number of queued light client optimistic updates where a matching block has been imported.", ) }); @@ -137,3 +129,10 @@ pub static BEACON_PROCESSOR_SEND_ERROR_PER_WORK_TYPE: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( + "beacon_processor_queue_time", + "The delay between when a work event was queued in the beacon processor and when it was popped from the queue", + &["work_type"], + ) +}); diff --git a/beacon_node/beacon_processor/src/scheduler/mod.rs b/beacon_node/beacon_processor/src/scheduler/mod.rs new file mode 100644 index 0000000000..e1a076a7c5 --- /dev/null +++ b/beacon_node/beacon_processor/src/scheduler/mod.rs @@ -0,0 +1 @@ +pub mod work_reprocessing_queue; diff --git a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs similarity index 75% rename from beacon_node/beacon_processor/src/work_reprocessing_queue.rs rename to beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs index 2b6e72ae0c..c99388287c 100644 --- a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs @@ -16,9 +16,10 @@ use fnv::FnvHashMap; use futures::task::Poll; use futures::{Stream, StreamExt}; use itertools::Itertools; -use logging::crit; use logging::TimeLatch; +use logging::crit; use slot_clock::SlotClock; +use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; @@ -36,7 +37,9 @@ const TASK_NAME: &str = "beacon_processor_reprocess_queue"; const GOSSIP_BLOCKS: &str = "gossip_blocks"; const RPC_BLOCKS: &str = "rpc_blocks"; const ATTESTATIONS: &str = "attestations"; +const ATTESTATIONS_PER_ROOT: &str = "attestations_per_root"; const LIGHT_CLIENT_UPDATES: &str = "lc_updates"; +const LIGHT_CLIENT_UPDATES_PER_PARENT_ROOT: &str = "lc_updates_per_parent_root"; /// Queue blocks for re-processing with an `ADDITIONAL_QUEUED_BLOCK_DELAY` after the slot starts. /// This is to account for any slight drift in the system clock. @@ -54,6 +57,9 @@ pub const QUEUED_RPC_BLOCK_DELAY: Duration = Duration::from_secs(4); /// For how long to queue sampling requests for reprocessing. pub const QUEUED_SAMPLING_REQUESTS_DELAY: Duration = Duration::from_secs(12); +/// For how long to queue delayed column reconstruction. +pub const QUEUED_RECONSTRUCTION_DELAY: Duration = Duration::from_millis(150); + /// Set an arbitrary upper-bound on the number of queued blocks to avoid DoS attacks. The fact that /// we signature-verify blocks before putting them in the queue *should* protect against this, but /// it's nice to have extra protection. @@ -65,10 +71,6 @@ const MAXIMUM_QUEUED_ATTESTATIONS: usize = 16_384; /// How many light client updates we keep before new ones get dropped. const MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES: usize = 128; -/// How many sampling requests we queue before new ones get dropped. -/// TODO(das): choose a sensible value -const MAXIMUM_QUEUED_SAMPLING_REQUESTS: usize = 16_384; - // Process backfill batch 50%, 60%, 80% through each slot. // // Note: use caution to set these fractions in a way that won't cause panic-y @@ -82,6 +84,10 @@ pub const BACKFILL_SCHEDULE_IN_SLOT: [(u32, u32); 3] = [ (4, 5), ]; +/// Fraction of slot duration after which column reconstruction is triggered, makes it easier for +/// different slot timings to have a generalised deadline +pub const RECONSTRUCTION_DEADLINE: (u64, u64) = (1, 4); + /// Messages that the scheduler can receive. #[derive(AsRefStr)] pub enum ReprocessQueueMessage { @@ -105,10 +111,10 @@ pub enum ReprocessQueueMessage { UnknownBlockAggregate(QueuedAggregate), /// A light client optimistic update that references a parent root that has not been seen as a parent. UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), - /// A sampling request that references an unknown block. - UnknownBlockSamplingRequest(QueuedSamplingRequest), /// A new backfill batch that needs to be scheduled for processing. BackfillSync(QueuedBackfillBatch), + /// A delayed column reconstruction that needs checking + DelayColumnReconstruction(QueuedColumnReconstruction), } /// Events sent by the scheduler once they are ready for re-processing. @@ -119,8 +125,8 @@ pub enum ReadyWork { Unaggregate(QueuedUnaggregate), Aggregate(QueuedAggregate), LightClientUpdate(QueuedLightClientUpdate), - SamplingRequest(QueuedSamplingRequest), BackfillSync(QueuedBackfillBatch), + ColumnReconstruction(QueuedColumnReconstruction), } /// An Attestation for which the corresponding block was not seen while processing, queued for @@ -144,12 +150,6 @@ pub struct QueuedLightClientUpdate { pub process_fn: BlockingFn, } -/// A sampling request for which the corresponding block is not known while processing. -pub struct QueuedSamplingRequest { - pub beacon_block_root: Hash256, - pub process_fn: BlockingFn, -} - /// A block that arrived early and has been queued for later import. pub struct QueuedGossipBlock { pub beacon_block_slot: Slot, @@ -174,7 +174,13 @@ pub struct IgnoredRpcBlock { } /// A backfill batch work that has been queued for processing later. -pub struct QueuedBackfillBatch(pub AsyncFn); +pub struct QueuedBackfillBatch(pub BlockingFn); + +pub struct QueuedColumnReconstruction { + pub block_root: Hash256, + pub slot: Slot, + pub process_fn: AsyncFn, +} impl TryFrom> for QueuedBackfillBatch { type Error = WorkEvent; @@ -212,6 +218,8 @@ enum InboundEvent { ReadyLightClientUpdate(QueuedLightClientUpdateId), /// A backfill batch that was queued is ready for processing. ReadyBackfillSync(QueuedBackfillBatch), + /// A column reconstruction that was queued is ready for processing. + ReadyColumnReconstruction(QueuedColumnReconstruction), /// A message sent to the `ReprocessQueue` Msg(ReprocessQueueMessage), } @@ -232,8 +240,8 @@ struct ReprocessQueue { attestations_delay_queue: DelayQueue, /// Queue to manage scheduled light client updates. lc_updates_delay_queue: DelayQueue, - /// Queue to manage scheduled sampling requests - sampling_requests_delay_queue: DelayQueue, + /// Queue to manage scheduled column reconstructions. + column_reconstructions_delay_queue: DelayQueue, /* Queued items */ /// Queued blocks. @@ -248,10 +256,8 @@ struct ReprocessQueue { queued_lc_updates: FnvHashMap, /// Light Client Updates per parent_root. awaiting_lc_updates_per_parent_root: HashMap>, - /// Queued sampling requests. - queued_sampling_requests: FnvHashMap, - /// Sampling requests per block root. - awaiting_sampling_requests_per_block_root: HashMap>, + /// Column reconstruction per block root. + queued_column_reconstructions: HashMap, /// Queued backfill batches queued_backfill_batches: Vec, @@ -259,18 +265,15 @@ struct ReprocessQueue { /// Next attestation id, used for both aggregated and unaggregated attestations next_attestation: usize, next_lc_update: usize, - next_sampling_request_update: usize, early_block_debounce: TimeLatch, rpc_block_debounce: TimeLatch, attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, - sampling_request_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, slot_clock: Arc, } pub type QueuedLightClientUpdateId = usize; -pub type QueuedSamplingRequestId = usize; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum QueuedAttestationId { @@ -343,6 +346,15 @@ impl Stream for ReprocessQueue { Poll::Ready(None) | Poll::Pending => (), } + match self.column_reconstructions_delay_queue.poll_expired(cx) { + Poll::Ready(Some(reconstruction)) => { + return Poll::Ready(Some(InboundEvent::ReadyColumnReconstruction( + reconstruction.into_inner(), + ))); + } + Poll::Ready(None) | Poll::Pending => (), + } + if let Some(next_backfill_batch_event) = self.next_backfill_batch_event.as_mut() { match next_backfill_batch_event.as_mut().poll(cx) { Poll::Ready(_) => { @@ -409,24 +421,21 @@ impl ReprocessQueue { rpc_block_delay_queue: DelayQueue::new(), attestations_delay_queue: DelayQueue::new(), lc_updates_delay_queue: DelayQueue::new(), - sampling_requests_delay_queue: <_>::default(), + column_reconstructions_delay_queue: DelayQueue::new(), queued_gossip_block_roots: HashSet::new(), queued_lc_updates: FnvHashMap::default(), queued_aggregates: FnvHashMap::default(), queued_unaggregates: FnvHashMap::default(), - queued_sampling_requests: <_>::default(), awaiting_attestations_per_root: HashMap::new(), awaiting_lc_updates_per_parent_root: HashMap::new(), - awaiting_sampling_requests_per_block_root: <_>::default(), queued_backfill_batches: Vec::new(), + queued_column_reconstructions: HashMap::new(), next_attestation: 0, next_lc_update: 0, - next_sampling_request_update: 0, early_block_debounce: TimeLatch::default(), rpc_block_debounce: TimeLatch::default(), attestation_delay_debounce: TimeLatch::default(), lc_update_delay_debounce: TimeLatch::default(), - sampling_request_delay_debounce: <_>::default(), next_backfill_batch_event: None, slot_clock, } @@ -478,15 +487,14 @@ impl ReprocessQueue { // This logic is slightly awkward since `SlotClock::duration_to_slot` // doesn't distinguish between a slot that has already arrived and an // error reading the slot clock. - if let Some(now) = self.slot_clock.now() { - if block_slot <= now - && self - .ready_work_tx - .try_send(ReadyWork::Block(early_block)) - .is_err() - { - error!("Failed to send block"); - } + if let Some(now) = self.slot_clock.now() + && block_slot <= now + && self + .ready_work_tx + .try_send(ReadyWork::Block(early_block)) + .is_err() + { + error!("Failed to send block"); } } } @@ -635,34 +643,6 @@ impl ReprocessQueue { self.next_lc_update += 1; } - InboundEvent::Msg(UnknownBlockSamplingRequest(queued_sampling_request)) => { - if self.sampling_requests_delay_queue.len() >= MAXIMUM_QUEUED_SAMPLING_REQUESTS { - if self.sampling_request_delay_debounce.elapsed() { - error!( - queue_size = MAXIMUM_QUEUED_SAMPLING_REQUESTS, - "Sampling requests delay queue is full" - ); - } - // Drop the inbound message. - return; - } - - let id: QueuedSamplingRequestId = self.next_sampling_request_update; - self.next_sampling_request_update += 1; - - // Register the delay. - let delay_key = self - .sampling_requests_delay_queue - .insert(id, QUEUED_SAMPLING_REQUESTS_DELAY); - - self.awaiting_sampling_requests_per_block_root - .entry(queued_sampling_request.beacon_block_root) - .or_default() - .push(id); - - self.queued_sampling_requests - .insert(id, (queued_sampling_request, delay_key)); - } InboundEvent::Msg(BlockImported { block_root, parent_root, @@ -722,48 +702,6 @@ impl ReprocessQueue { ); } } - // Unqueue the sampling requests we have for this root, if any. - if let Some(queued_ids) = self - .awaiting_sampling_requests_per_block_root - .remove(&block_root) - { - let mut sent_count = 0; - let mut failed_to_send_count = 0; - - for id in queued_ids { - metrics::inc_counter( - &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_SAMPLING_REQUESTS, - ); - - if let Some((queued, delay_key)) = self.queued_sampling_requests.remove(&id) - { - // Remove the delay. - self.sampling_requests_delay_queue.remove(&delay_key); - - // Send the work. - let work = ReadyWork::SamplingRequest(queued); - - if self.ready_work_tx.try_send(work).is_err() { - failed_to_send_count += 1; - } else { - sent_count += 1; - } - } else { - // This should never happen. - error!(?block_root, ?id, "Unknown sampling request for block root"); - } - } - - if failed_to_send_count > 0 { - error!( - hint = "system may be overloaded", - ?block_root, - failed_to_send_count, - sent_count, - "Ignored scheduled sampling requests for block" - ); - } - } } InboundEvent::Msg(NewLightClientOptimisticUpdate { parent_root }) => { // Unqueue the light client optimistic updates we have for this root, if any. @@ -817,6 +755,35 @@ impl ReprocessQueue { self.recompute_next_backfill_batch_event(); } } + InboundEvent::Msg(DelayColumnReconstruction(request)) => { + let mut reconstruction_delay = QUEUED_RECONSTRUCTION_DELAY; + let slot_duration = self.slot_clock.slot_duration().as_millis() as u64; + let reconstruction_deadline_millis = + (slot_duration * RECONSTRUCTION_DEADLINE.0) / RECONSTRUCTION_DEADLINE.1; + let reconstruction_deadline = Duration::from_millis(reconstruction_deadline_millis); + if let Some(duration_from_current_slot) = + self.slot_clock.millis_from_current_slot_start() + && let Some(current_slot) = self.slot_clock.now() + && duration_from_current_slot >= reconstruction_deadline + && current_slot == request.slot + { + // If we are at least `reconstruction_deadline` seconds into the current slot, + // and the reconstruction request is for the current slot, process reconstruction immediately. + reconstruction_delay = Duration::from_secs(0); + } + match self.queued_column_reconstructions.entry(request.block_root) { + Entry::Occupied(key) => { + self.column_reconstructions_delay_queue + .reset(key.get(), reconstruction_delay); + } + Entry::Vacant(vacant) => { + let delay_key = self + .column_reconstructions_delay_queue + .insert(request, reconstruction_delay); + vacant.insert(delay_key); + } + } + } // A block that was queued for later processing is now ready to be processed. InboundEvent::ReadyGossipBlock(ready_block) => { let block_root = ready_block.beacon_block_root; @@ -869,9 +836,18 @@ impl ReprocessQueue { ); } - if let Some(queued_atts) = self.awaiting_attestations_per_root.get_mut(&root) { - if let Some(index) = queued_atts.iter().position(|&id| id == queued_id) { - queued_atts.swap_remove(index); + if let Entry::Occupied(mut queued_atts) = + self.awaiting_attestations_per_root.entry(root) + && let Some(index) = + queued_atts.get().iter().position(|&id| id == queued_id) + { + let queued_atts_mut = queued_atts.get_mut(); + queued_atts_mut.swap_remove(index); + + // If the vec is empty after this attestation's removal, we need to delete + // the entry to prevent bloating the hashmap indefinitely. + if queued_atts_mut.is_empty() { + queued_atts.remove_entry(); } } } @@ -893,14 +869,18 @@ impl ReprocessQueue { error!("Failed to send scheduled light client optimistic update"); } - if let Some(queued_lc_updates) = self - .awaiting_lc_updates_per_parent_root - .get_mut(&parent_root) + if let Entry::Occupied(mut queued_lc_updates) = + self.awaiting_lc_updates_per_parent_root.entry(parent_root) + && let Some(index) = queued_lc_updates + .get() + .iter() + .position(|&id| id == queued_id) { - if let Some(index) = - queued_lc_updates.iter().position(|&id| id == queued_id) - { - queued_lc_updates.swap_remove(index); + let queued_lc_updates_mut = queued_lc_updates.get_mut(); + queued_lc_updates_mut.swap_remove(index); + + if queued_lc_updates_mut.is_empty() { + queued_lc_updates.remove_entry(); } } } @@ -940,6 +920,20 @@ impl ReprocessQueue { _ => crit!("Unexpected return from try_send error"), } } + InboundEvent::ReadyColumnReconstruction(column_reconstruction) => { + self.queued_column_reconstructions + .remove(&column_reconstruction.block_root); + if self + .ready_work_tx + .try_send(ReadyWork::ColumnReconstruction(column_reconstruction)) + .is_err() + { + error!( + hint = "system may be overloaded", + "Ignored scheduled column reconstruction" + ); + } + } } metrics::set_gauge_vec( @@ -957,11 +951,21 @@ impl ReprocessQueue { &[ATTESTATIONS], self.attestations_delay_queue.len() as i64, ); + metrics::set_gauge_vec( + &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL, + &[ATTESTATIONS_PER_ROOT], + self.awaiting_attestations_per_root.len() as i64, + ); metrics::set_gauge_vec( &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL, &[LIGHT_CLIENT_UPDATES], self.lc_updates_delay_queue.len() as i64, ); + metrics::set_gauge_vec( + &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL, + &[LIGHT_CLIENT_UPDATES_PER_PARENT_ROOT], + self.awaiting_lc_updates_per_parent_root.len() as i64, + ); } fn recompute_next_backfill_batch_event(&mut self) { @@ -1007,6 +1011,7 @@ impl ReprocessQueue { #[cfg(test)] mod tests { use super::*; + use crate::BeaconProcessorConfig; use logging::create_test_tracing_subscriber; use slot_clock::{ManualSlotClock, TestingSlotClock}; use std::ops::Add; @@ -1084,7 +1089,7 @@ mod tests { // Now queue a backfill sync batch. work_reprocessing_tx .try_send(ReprocessQueueMessage::BackfillSync(QueuedBackfillBatch( - Box::pin(async {}), + Box::new(|| {}), ))) .unwrap(); tokio::task::yield_now().await; @@ -1129,4 +1134,209 @@ mod tests { Duration::from_secs(slot_duration), ) } + + fn test_queue() -> ReprocessQueue { + create_test_tracing_subscriber(); + + let config = BeaconProcessorConfig::default(); + let (ready_work_tx, _) = mpsc::channel::(config.max_scheduled_work_queue_len); + let (_, reprocess_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let slot_clock = Arc::new(testing_slot_clock(12)); + + ReprocessQueue::new(ready_work_tx, reprocess_work_rx, slot_clock) + } + + // This is a regression test for a memory leak in `awaiting_attestations_per_root`. + // See: https://github.com/sigp/lighthouse/pull/8065 + #[tokio::test] + async fn prune_awaiting_attestations_per_root() { + create_test_tracing_subscriber(); + + let mut queue = test_queue(); + + // Pause time so it only advances manually + tokio::time::pause(); + + let beacon_block_root = Hash256::repeat_byte(0xaf); + + // Insert an attestation. + let att = ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate { + beacon_block_root, + process_fn: Box::new(|| {}), + }); + + // Process the event to enter it into the delay queue. + queue.handle_message(InboundEvent::Msg(att)); + + // Check that it is queued. + assert_eq!(queue.awaiting_attestations_per_root.len(), 1); + assert!( + queue + .awaiting_attestations_per_root + .contains_key(&beacon_block_root) + ); + + // Advance time to expire the attestation. + advance_time(&queue.slot_clock, 2 * QUEUED_ATTESTATION_DELAY).await; + let ready_msg = queue.next().await.unwrap(); + assert!(matches!(ready_msg, InboundEvent::ReadyAttestation(_))); + queue.handle_message(ready_msg); + + // The entry for the block root should be gone. + assert!(queue.awaiting_attestations_per_root.is_empty()); + } + + // This is a regression test for a memory leak in `awaiting_lc_updates_per_parent_root`. + // See: https://github.com/sigp/lighthouse/pull/8065 + #[tokio::test] + async fn prune_awaiting_lc_updates_per_parent_root() { + create_test_tracing_subscriber(); + + let mut queue = test_queue(); + + // Pause time so it only advances manually + tokio::time::pause(); + + let parent_root = Hash256::repeat_byte(0xaf); + + // Insert an attestation. + let msg = + ReprocessQueueMessage::UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate { + parent_root, + process_fn: Box::new(|| {}), + }); + + // Process the event to enter it into the delay queue. + queue.handle_message(InboundEvent::Msg(msg)); + + // Check that it is queued. + assert_eq!(queue.awaiting_lc_updates_per_parent_root.len(), 1); + assert!( + queue + .awaiting_lc_updates_per_parent_root + .contains_key(&parent_root) + ); + + // Advance time to expire the update. + advance_time(&queue.slot_clock, 2 * QUEUED_LIGHT_CLIENT_UPDATE_DELAY).await; + let ready_msg = queue.next().await.unwrap(); + assert!(matches!(ready_msg, InboundEvent::ReadyLightClientUpdate(_))); + queue.handle_message(ready_msg); + + // The entry for the block root should be gone. + assert!(queue.awaiting_lc_updates_per_parent_root.is_empty()); + } + + async fn test_reconstruction_immediate_at_deadline(slot_duration_secs: u64) { + let config = BeaconProcessorConfig::default(); + let (ready_work_tx, _) = mpsc::channel::(config.max_scheduled_work_queue_len); + let (_, reprocess_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let slot_clock = Arc::new(testing_slot_clock(slot_duration_secs)); + let mut queue = ReprocessQueue::new(ready_work_tx, reprocess_work_rx, slot_clock); + + let slot_duration = queue.slot_clock.slot_duration(); + let reconstruction_deadline_millis = (slot_duration.as_millis() as u64 + * RECONSTRUCTION_DEADLINE.0) + / RECONSTRUCTION_DEADLINE.1; + let reconstruction_deadline = Duration::from_millis(reconstruction_deadline_millis); + + // Advance time to just after the deadline + advance_time( + &queue.slot_clock, + reconstruction_deadline + Duration::from_millis(10), + ) + .await; + + let current_slot = queue.slot_clock.now().unwrap(); + let block_root = Hash256::repeat_byte(0xaa); + + // Queue a reconstruction for the current slot after the deadline + let reconstruction_request = QueuedColumnReconstruction { + block_root, + slot: current_slot, + process_fn: Box::pin(async {}), + }; + queue.handle_message(InboundEvent::Msg( + ReprocessQueueMessage::DelayColumnReconstruction(reconstruction_request), + )); + + assert_eq!(queue.queued_column_reconstructions.len(), 1); + + // Should be immediately ready (0 delay since we're past deadline) + let ready_msg = queue.next().await.unwrap(); + assert!(matches!( + ready_msg, + InboundEvent::ReadyColumnReconstruction(_) + )); + + if let InboundEvent::ReadyColumnReconstruction(reconstruction) = ready_msg { + assert_eq!(reconstruction.block_root, block_root); + queue.handle_message(InboundEvent::ReadyColumnReconstruction(reconstruction)); + } + + assert!(queue.queued_column_reconstructions.is_empty()); + } + + /// Tests that column reconstruction queued after the deadline is triggered immediately + /// on mainnet (12s slots). + /// + /// When a reconstruction for the current slot is queued after the reconstruction deadline + /// (1/4 of slot duration = 3s for mainnet), it should be processed immediately with 0 delay. + #[tokio::test] + async fn column_reconstruction_immediate_processing_at_deadline_mainnet() { + tokio::time::pause(); + test_reconstruction_immediate_at_deadline(12).await; + } + + /// Tests that column reconstruction queued after the deadline is triggered immediately + /// on Gnosis (5s slots). + /// + /// When a reconstruction for the current slot is queued after the reconstruction deadline + /// (1/4 of slot duration = 1.25s for Gnosis), it should be processed immediately with 0 delay. + #[tokio::test] + async fn column_reconstruction_immediate_processing_at_deadline_gnosis() { + tokio::time::pause(); + test_reconstruction_immediate_at_deadline(5).await; + } + + /// Tests that column reconstruction uses the standard delay when queued before the deadline. + /// + /// When a reconstruction for the current slot is queued before the deadline, it should wait + /// for the standard QUEUED_RECONSTRUCTION_DELAY (150ms) before being triggered. + #[tokio::test] + async fn column_reconstruction_uses_standard_delay() { + tokio::time::pause(); + + let mut queue = test_queue(); + let current_slot = queue.slot_clock.now().unwrap(); + let block_root = Hash256::repeat_byte(0xcc); + + // Queue a reconstruction at the start of the slot (before deadline) + let reconstruction_request = QueuedColumnReconstruction { + block_root, + slot: current_slot, + process_fn: Box::pin(async {}), + }; + queue.handle_message(InboundEvent::Msg( + ReprocessQueueMessage::DelayColumnReconstruction(reconstruction_request), + )); + + assert_eq!(queue.queued_column_reconstructions.len(), 1); + + // Advance time by QUEUED_RECONSTRUCTION_DELAY + advance_time(&queue.slot_clock, QUEUED_RECONSTRUCTION_DELAY).await; + + // Should be ready after the standard delay + let ready_msg = queue.next().await.unwrap(); + assert!(matches!( + ready_msg, + InboundEvent::ReadyColumnReconstruction(_) + )); + + if let InboundEvent::ReadyColumnReconstruction(reconstruction) = ready_msg { + assert_eq!(reconstruction.block_root, block_root); + } + } } diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index 1920bd0ebb..09bf3f48b4 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -5,6 +5,8 @@ edition = { workspace = true } authors = ["Sean Anderson "] [dependencies] +bls = { workspace = true } +context_deserialize = { workspace = true } eth2 = { workspace = true } ethereum_ssz = { workspace = true } lighthouse_version = { workspace = true } @@ -12,3 +14,7 @@ reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } + +[dev-dependencies] +mockito = { workspace = true } +tokio = { workspace = true } diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index d193eaf1d8..4fc6b3a379 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -1,24 +1,26 @@ +use bls::PublicKeyBytes; +use context_deserialize::ContextDeserialize; +pub use eth2::Error; use eth2::types::beacon_response::EmptyMetadata; use eth2::types::builder_bid::SignedBuilderBid; use eth2::types::{ - ContentType, ContextDeserialize, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, - ForkVersionedResponse, PublicKeyBytes, SignedValidatorRegistrationData, Slot, + ContentType, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionedResponse, + SignedValidatorRegistrationData, Slot, }; use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; -pub use eth2::Error; use eth2::{ - ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, - JSON_CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER, + CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error, success_or_error, }; -use reqwest::header::{HeaderMap, HeaderValue, ACCEPT}; +use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; use reqwest::{IntoUrl, Response}; use sensitive_url::SensitiveUrl; -use serde::de::DeserializeOwned; use serde::Serialize; +use serde::de::DeserializeOwned; use ssz::Encode; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000; @@ -155,15 +157,7 @@ impl BuilderHttpClient { } ContentType::Json => { self.ssz_available.store(false, Ordering::SeqCst); - let mut de = serde_json::Deserializer::from_slice(&response_bytes); - let data = - T::context_deserialize(&mut de, fork_name).map_err(Error::InvalidJson)?; - - Ok(ForkVersionedResponse { - version: fork_name, - metadata: EmptyMetadata {}, - data, - }) + serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson) } } } @@ -249,7 +243,7 @@ impl BuilderHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } async fn post_with_raw_response( @@ -270,7 +264,7 @@ impl BuilderHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } /// `POST /eth/v1/builder/validators` @@ -278,7 +272,7 @@ impl BuilderHttpClient { &self, validator: &[SignedValidatorRegistrationData], ) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -293,11 +287,11 @@ impl BuilderHttpClient { } /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body - pub async fn post_builder_blinded_blocks_ssz( + pub async fn post_builder_blinded_blocks_v1_ssz( &self, blinded_block: &SignedBlindedBeaconBlock, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); let body = blinded_block.as_ssz_bytes(); @@ -340,12 +334,62 @@ impl BuilderHttpClient { .map_err(Error::InvalidSsz) } + /// `POST /eth/v2/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_v2_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.expose_full().clone(); + + let body = blinded_block.as_ssz_bytes(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_ssz_with_raw_response( + path, + body, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + /// `POST /eth/v1/builder/blinded_blocks` - pub async fn post_builder_blinded_blocks( + pub async fn post_builder_blinded_blocks_v1( &self, blinded_block: &SignedBlindedBeaconBlock, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -383,6 +427,54 @@ impl BuilderHttpClient { .await?) } + /// `POST /eth/v2/builder/blinded_blocks` + pub async fn post_builder_blinded_blocks_v2( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.expose_full().clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(JSON_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_with_raw_response( + path, + &blinded_block, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + /// `GET /eth/v1/builder/header` pub async fn get_builder_header( &self, @@ -390,7 +482,7 @@ impl BuilderHttpClient { parent_hash: ExecutionBlockHash, pubkey: &PublicKeyBytes, ) -> Result>>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -431,7 +523,7 @@ impl BuilderHttpClient { /// `GET /eth/v1/builder/status` pub async fn get_builder_status(&self) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -448,6 +540,13 @@ impl BuilderHttpClient { #[cfg(test)] mod tests { use super::*; + use bls::Signature; + use eth2::types::MainnetEthSpec; + use eth2::types::builder_bid::{BuilderBid, BuilderBidFulu}; + use eth2::types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; + use mockito::{Matcher, Server, ServerGuard}; + + type E = MainnetEthSpec; #[test] fn test_headers_no_panic() { @@ -458,4 +557,146 @@ mod tests { assert!(HeaderValue::from_str(JSON_ACCEPT_VALUE).is_ok()); assert!(HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER).is_ok()); } + + #[tokio::test] + async fn test_get_builder_header_ssz_response() { + // Set up mock server + let mut server = Server::new_async().await; + let mock_response_body = fulu_signed_builder_bid(); + mock_get_header_response( + &mut server, + Some("fulu"), + ContentType::Ssz, + mock_response_body.clone(), + ); + + let builder_client = BuilderHttpClient::new( + SensitiveUrl::from_str(&server.url()).unwrap(), + None, + None, + false, + ) + .unwrap(); + + let response = builder_client + .get_builder_header( + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + &PublicKeyBytes::empty(), + ) + .await + .expect("should succeed in get_builder_header") + .expect("should have response body"); + + assert_eq!(response, mock_response_body); + } + + #[tokio::test] + async fn test_get_builder_header_json_response() { + // Set up mock server + let mut server = Server::new_async().await; + let mock_response_body = fulu_signed_builder_bid(); + mock_get_header_response( + &mut server, + None, + ContentType::Json, + mock_response_body.clone(), + ); + + let builder_client = BuilderHttpClient::new( + SensitiveUrl::from_str(&server.url()).unwrap(), + None, + None, + false, + ) + .unwrap(); + + let response = builder_client + .get_builder_header( + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + &PublicKeyBytes::empty(), + ) + .await + .expect("should succeed in get_builder_header") + .expect("should have response body"); + + assert_eq!(response, mock_response_body); + } + + #[tokio::test] + async fn test_get_builder_header_no_version_header_fallback_json() { + // Set up mock server + let mut server = Server::new_async().await; + let mock_response_body = fulu_signed_builder_bid(); + mock_get_header_response( + &mut server, + Some("fulu"), + ContentType::Json, + mock_response_body.clone(), + ); + + let builder_client = BuilderHttpClient::new( + SensitiveUrl::from_str(&server.url()).unwrap(), + None, + None, + false, + ) + .unwrap(); + + let response = builder_client + .get_builder_header( + Slot::new(1), + ExecutionBlockHash::repeat_byte(1), + &PublicKeyBytes::empty(), + ) + .await + .expect("should succeed in get_builder_header") + .expect("should have response body"); + + assert_eq!(response, mock_response_body); + } + + fn mock_get_header_response( + server: &mut ServerGuard, + header_version_opt: Option<&str>, + content_type: ContentType, + response_body: ForkVersionedResponse>, + ) { + let mut mock = server.mock( + "GET", + Matcher::Regex(r"^/eth/v1/builder/header/\d+/.+/.+$".to_string()), + ); + + if let Some(version) = header_version_opt { + mock = mock.with_header(CONSENSUS_VERSION_HEADER, version); + } + + match content_type { + ContentType::Json => { + mock = mock + .with_header(CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER) + .with_body(serde_json::to_string(&response_body).unwrap()); + } + ContentType::Ssz => { + mock = mock + .with_header(CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER) + .with_body(response_body.data.as_ssz_bytes()); + } + } + + mock.with_status(200).create(); + } + + fn fulu_signed_builder_bid() -> ForkVersionedResponse> { + let rng = &mut XorShiftRng::from_seed([42; 16]); + ForkVersionedResponse { + version: ForkName::Fulu, + metadata: EmptyMetadata {}, + data: SignedBuilderBid { + message: BuilderBid::Fulu(BuilderBidFulu::random_for_test(rng)), + signature: Signature::empty(), + }, + } + } } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 195c53c4a0..3c4b2572c9 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -4,19 +4,12 @@ version = "0.2.0" authors = ["Sigma Prime "] edition = { workspace = true } -[dev-dependencies] -operation_pool = { workspace = true } -serde_yaml = { workspace = true } -state_processing = { workspace = true } -tokio = { workspace = true } - [dependencies] beacon_chain = { workspace = true } beacon_processor = { workspace = true } directory = { workspace = true } dirs = { workspace = true } environment = { workspace = true } -eth1 = { workspace = true } eth2 = { workspace = true } eth2_config = { workspace = true } ethereum_ssz = { workspace = true } @@ -46,3 +39,9 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } types = { workspace = true } + +[dev-dependencies] +operation_pool = { workspace = true } +serde_yaml = { workspace = true } +state_processing = { workspace = true } +tokio = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index a581d5c128..c48021e45d 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1,54 +1,52 @@ +use crate::Client; use crate::compute_light_client_updates::{ - compute_light_client_updates, LIGHT_CLIENT_SERVER_CHANNEL_CAPACITY, + LIGHT_CLIENT_SERVER_CHANNEL_CAPACITY, compute_light_client_updates, }; use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; -use crate::Client; use beacon_chain::attestation_simulator::start_attestation_simulator_service; use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; use beacon_chain::{ + BeaconChain, BeaconChainTypes, MigratorConfig, ServerSentEventHandler, builder::{BeaconChainBuilder, Witness}, - eth1_chain::{CachingEth1Backend, Eth1Chain}, slot_clock::{SlotClock, SystemTimeSlotClock}, state_advance_timer::spawn_state_advance_timer, store::{HotColdDB, ItemStore, StoreConfig}, - BeaconChain, BeaconChainTypes, Eth1ChainBackend, MigratorConfig, ServerSentEventHandler, }; use beacon_chain::{Kzg, LightClientProducerEvent}; use beacon_processor::{BeaconProcessor, BeaconProcessorChannels}; use beacon_processor::{BeaconProcessorConfig, BeaconProcessorQueueLengths}; use environment::RuntimeContext; -use eth1::{Config as Eth1Config, Service as Eth1Service}; use eth2::{ - types::{BlockId, StateId}, BeaconNodeHttpClient, Error as ApiError, Timeouts, + types::{BlockId, StateId}, }; -use execution_layer::test_utils::generate_genesis_header; use execution_layer::ExecutionLayer; +use execution_layer::test_utils::generate_genesis_header; use futures::channel::mpsc::Receiver; -use genesis::{interop_genesis_state, Eth1GenesisService, DEFAULT_ETH1_BLOCK_HASH}; -use lighthouse_network::{prometheus_client::registry::Registry, NetworkGlobals}; +use genesis::{DEFAULT_ETH1_BLOCK_HASH, interop_genesis_state}; +use lighthouse_network::identity::Keypair; +use lighthouse_network::{NetworkGlobals, prometheus_client::registry::Registry}; use monitoring_api::{MonitoringHttpClient, ProcessType}; use network::{NetworkConfig, NetworkSenders, NetworkService}; -use rand::rngs::{OsRng, StdRng}; use rand::SeedableRng; +use rand::rngs::{OsRng, StdRng}; use slasher::Slasher; use slasher_service::SlasherService; -use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use store::database::interface::BeaconNodeBackend; use timer::spawn_timer; -use tokio::sync::oneshot; -use tracing::{debug, info, warn}; +use tracing::{debug, info, instrument, warn}; +use types::data_column_custody_group::compute_ordered_custody_column_indices; use types::{ - test_utils::generate_deterministic_keypairs, BeaconState, BlobSidecarList, ChainSpec, EthSpec, - ExecutionBlockHash, Hash256, SignedBeaconBlock, + BeaconState, BlobSidecarList, ChainSpec, EthSpec, ExecutionBlockHash, Hash256, + SignedBeaconBlock, test_utils::generate_deterministic_keypairs, }; /// Interval between polling the eth1 node for genesis information. @@ -80,7 +78,6 @@ pub struct ClientBuilder { chain_spec: Option>, beacon_chain_builder: Option>, beacon_chain: Option>>, - eth1_service: Option, network_globals: Option>>, network_senders: Option>, libp2p_registry: Option, @@ -95,11 +92,10 @@ pub struct ClientBuilder { eth_spec_instance: T::EthSpec, } -impl - ClientBuilder> +impl + ClientBuilder> where TSlotClock: SlotClock + Clone + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, @@ -115,7 +111,6 @@ where chain_spec: None, beacon_chain_builder: None, beacon_chain: None, - eth1_service: None, network_globals: None, network_senders: None, libp2p_registry: None, @@ -156,10 +151,12 @@ where /// Initializes the `BeaconChainBuilder`. The `build_beacon_chain` method will need to be /// called later in order to actually instantiate the `BeaconChain`. + #[instrument(skip_all)] pub async fn beacon_chain_builder( mut self, client_genesis: ClientGenesis, config: ClientConfig, + node_id: [u8; 32], ) -> Result { let store = self.store.clone(); let chain_spec = self.chain_spec.clone(); @@ -191,15 +188,17 @@ where }; let kzg_err_msg = |e| format!("Failed to load trusted setup: {:?}", e); - let trusted_setup = config.trusted_setup.clone(); let kzg = if spec.is_peer_das_scheduled() { - Kzg::new_from_trusted_setup_das_enabled(trusted_setup).map_err(kzg_err_msg)? - } else if spec.deneb_fork_epoch.is_some() { - Kzg::new_from_trusted_setup(trusted_setup).map_err(kzg_err_msg)? + Kzg::new_from_trusted_setup(&config.trusted_setup).map_err(kzg_err_msg)? } else { - Kzg::new_from_trusted_setup_no_precomp(trusted_setup).map_err(kzg_err_msg)? + Kzg::new_from_trusted_setup_no_precomp(&config.trusted_setup).map_err(kzg_err_msg)? }; + let ordered_custody_column_indices = + compute_ordered_custody_column_indices::(node_id, &spec).map_err(|e| { + format!("Failed to compute ordered custody column indices: {:?}", e) + })?; + let builder = BeaconChainBuilder::new(eth_spec_instance, Arc::new(kzg)) .store(store) .task_executor(context.executor.clone()) @@ -211,10 +210,12 @@ where .beacon_graffiti(beacon_graffiti) .event_handler(event_handler) .execution_layer(execution_layer) - .import_all_data_columns(config.network.subscribe_all_data_column_subnets) + .node_custody_type(config.chain.node_custody_type) + .ordered_custody_column_indices(ordered_custody_column_indices) .validator_monitor_config(config.validator_monitor.clone()) .rng(Box::new( - StdRng::from_rng(OsRng).map_err(|e| format!("Failed to create RNG: {:?}", e))?, + StdRng::try_from_rng(&mut OsRng) + .map_err(|e| format!("Failed to create RNG: {:?}", e))?, )); let builder = if let Some(slasher) = self.slasher.clone() { @@ -261,7 +262,7 @@ where client_genesis }; - let (beacon_chain_builder, eth1_service_option) = match client_genesis { + let beacon_chain_builder = match client_genesis { ClientGenesis::Interop { validator_count, genesis_time, @@ -274,7 +275,7 @@ where None, &spec, )?; - builder.genesis_state(genesis_state).map(|v| (v, None))? + builder.genesis_state(genesis_state)? } ClientGenesis::InteropMerge { validator_count, @@ -289,7 +290,7 @@ where execution_payload_header, &spec, )?; - builder.genesis_state(genesis_state).map(|v| (v, None))? + builder.genesis_state(genesis_state)? } ClientGenesis::GenesisState => { info!("Starting from known genesis state"); @@ -303,41 +304,41 @@ where // It doesn't make sense to try and sync the chain if we can't // verify blob availability by downloading blobs from the P2P // network. The user should do a checkpoint sync instead. - if !config.allow_insecure_genesis_sync { - if let Some(deneb_fork_epoch) = spec.deneb_fork_epoch { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| format!("Unable to read system time: {e:}"))? - .as_secs(); - let genesis_time = genesis_state.genesis_time(); - let deneb_time = genesis_time - + (deneb_fork_epoch.as_u64() - * E::slots_per_epoch() - * spec.seconds_per_slot); - - // Shrink the blob availability window so users don't start - // a sync right before blobs start to disappear from the P2P - // network. - let reduced_p2p_availability_epochs = spec - .min_epochs_for_blob_sidecars_requests - .saturating_sub(BLOB_AVAILABILITY_REDUCTION_EPOCHS); - let blob_availability_window = reduced_p2p_availability_epochs + if !config.allow_insecure_genesis_sync + && let Some(deneb_fork_epoch) = spec.deneb_fork_epoch + { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("Unable to read system time: {e:}"))? + .as_secs(); + let genesis_time = genesis_state.genesis_time(); + let deneb_time = genesis_time + + (deneb_fork_epoch.as_u64() * E::slots_per_epoch() - * spec.seconds_per_slot; + * spec.seconds_per_slot); - if now > deneb_time + blob_availability_window { - return Err( + // Shrink the blob availability window so users don't start + // a sync right before blobs start to disappear from the P2P + // network. + let reduced_p2p_availability_epochs = spec + .min_epochs_for_blob_sidecars_requests + .saturating_sub(BLOB_AVAILABILITY_REDUCTION_EPOCHS); + let blob_availability_window = reduced_p2p_availability_epochs + * E::slots_per_epoch() + * spec.seconds_per_slot; + + if now > deneb_time + blob_availability_window { + return Err( "Syncing from genesis is insecure and incompatible with data availability checks. \ You should instead perform a checkpoint sync from a trusted node using the --checkpoint-sync-url option. \ For a list of public endpoints, see: https://eth-clients.github.io/checkpoint-sync-endpoints/ \ Alternatively, use --allow-insecure-genesis-sync if the risks are understood." .to_string(), ); - } } } - builder.genesis_state(genesis_state).map(|v| (v, None))? + builder.genesis_state(genesis_state)? } ClientGenesis::WeakSubjSszBytes { anchor_state_bytes, @@ -353,10 +354,11 @@ where .map_err(|e| format!("Unable to parse weak subj state SSZ: {:?}", e))?; let anchor_block = SignedBeaconBlock::from_ssz_bytes(&anchor_block_bytes, &spec) .map_err(|e| format!("Unable to parse weak subj block SSZ: {:?}", e))?; - let anchor_blobs = if anchor_block.message().body().has_blobs() { + + // Providing blobs is optional now and not providing them is recommended. + // Backfill can handle downloading the blobs or columns for the checkpoint block. + let anchor_blobs = if let Some(anchor_blobs_bytes) = anchor_blobs_bytes { let max_blobs_len = spec.max_blobs_per_block(anchor_block.epoch()) as usize; - let anchor_blobs_bytes = anchor_blobs_bytes - .ok_or("Blobs for checkpoint must be provided using --checkpoint-blobs")?; Some( BlobSidecarList::from_ssz_bytes(&anchor_blobs_bytes, max_blobs_len) .map_err(|e| format!("Unable to parse weak subj blobs SSZ: {e:?}"))?, @@ -366,14 +368,12 @@ where }; let genesis_state = genesis_state(&runtime_context, &config).await?; - builder - .weak_subjectivity_state( - anchor_state, - anchor_block, - anchor_blobs, - genesis_state, - ) - .map(|v| (v, None))? + builder.weak_subjectivity_state( + anchor_state, + anchor_block, + anchor_blobs, + genesis_state, + )? } ClientGenesis::CheckpointSyncUrl { url } => { info!( @@ -391,47 +391,6 @@ where )), ); - let deposit_snapshot = if config.sync_eth1_chain { - // We want to fetch deposit snapshot before fetching the finalized beacon state to - // ensure that the snapshot is not newer than the beacon state that satisfies the - // deposit finalization conditions - debug!("Downloading deposit snapshot"); - let deposit_snapshot_result = remote - .get_deposit_snapshot() - .await - .map_err(|e| match e { - ApiError::InvalidSsz(e) => format!( - "Unable to parse SSZ: {:?}. Ensure the checkpoint-sync-url refers to a \ - node for the correct network", - e - ), - e => format!("Error fetching deposit snapshot from remote: {:?}", e), - }); - match deposit_snapshot_result { - Ok(Some(deposit_snapshot)) => { - if deposit_snapshot.is_valid() { - Some(deposit_snapshot) - } else { - warn!("Remote BN sent invalid deposit snapshot!"); - None - } - } - Ok(None) => { - warn!("Remote BN does not support EIP-4881 fast deposit sync"); - None - } - Err(e) => { - warn!( - error = e, - "Remote BN does not support EIP-4881 fast deposit sync" - ); - None - } - } - } else { - None - }; - debug!("Downloading finalized state"); let state = remote .get_debug_beacon_states_ssz::(StateId::Finalized, &spec) @@ -460,10 +419,14 @@ where debug!("Downloaded finalized block"); - let blobs = if block.message().body().has_blobs() { + // `get_blob_sidecars` API is deprecated from Fulu and may not be supported by all servers + let is_before_fulu = !spec + .fork_name_at_slot::(finalized_block_slot) + .fulu_enabled(); + let blobs = if is_before_fulu && block.message().body().has_blobs() { debug!("Downloading finalized blobs"); if let Some(response) = remote - .get_blobs::(BlockId::Root(block_root), None, &spec) + .get_blob_sidecars::(BlockId::Root(block_root), None, &spec) .await .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? { @@ -491,126 +454,24 @@ where "Loaded checkpoint block and state" ); - let service = - deposit_snapshot.and_then(|snapshot| match Eth1Service::from_deposit_snapshot( - config.eth1, - spec.clone(), - &snapshot, - ) { - Ok(service) => { - info!( - deposits_loaded = snapshot.deposit_count, - "Loaded deposit tree snapshot" - ); - Some(service) - } - Err(e) => { - warn!(error = ?e, - "Unable to load deposit snapshot" - ); - None - } - }); - - builder - .weak_subjectivity_state(state, block, blobs, genesis_state) - .map(|v| (v, service))? + builder.weak_subjectivity_state(state, block, blobs, genesis_state)? } ClientGenesis::DepositContract => { - info!( - eth1_endpoints = ?config.eth1.endpoint, - contract_deploy_block = config.eth1.deposit_contract_deploy_block, - deposit_contract = &config.eth1.deposit_contract_address, - "Waiting for eth2 genesis from eth1" - ); - - let genesis_service = - Eth1GenesisService::new(config.eth1, context.eth2_config().spec.clone())?; - - // If the HTTP API server is enabled, start an instance of it where it only - // contains a reference to the eth1 service (all non-eth1 endpoints will fail - // gracefully). - // - // Later in this function we will shutdown this temporary "waiting for genesis" - // server so the real one can be started later. - let (exit_tx, exit_rx) = oneshot::channel::<()>(); - let http_listen_opt = if self.http_api_config.enabled { - #[allow(clippy::type_complexity)] - let ctx: Arc< - http_api::Context< - Witness, - >, - > = Arc::new(http_api::Context { - config: self.http_api_config.clone(), - chain: None, - network_senders: None, - network_globals: None, - beacon_processor_send: None, - beacon_processor_reprocess_send: None, - eth1_service: Some(genesis_service.eth1_service.clone()), - sse_logging_components: runtime_context.sse_logging_components.clone(), - }); - - // Discard the error from the oneshot. - let exit_future = async { - let _ = exit_rx.await; - }; - - let (listen_addr, server) = http_api::serve(ctx, exit_future) - .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; - - let http_api_task = async move { - server.await; - debug!("HTTP API server task ended"); - }; - - context - .clone() - .executor - .spawn_without_exit(http_api_task, "http-api"); - - Some(listen_addr) - } else { - None - }; - - let genesis_state = genesis_service - .wait_for_genesis_state(Duration::from_millis( - ETH1_GENESIS_UPDATE_INTERVAL_MILLIS, - )) - .await?; - - let _ = exit_tx.send(()); - - if let Some(http_listen) = http_listen_opt { - // This is a bit of a hack to ensure that the HTTP server has indeed shutdown. - // - // We will restart it again after we've finished setting up for genesis. - while TcpListener::bind(http_listen).is_err() { - warn!( - port = %http_listen, - "Waiting for HTTP server port to open" - ); - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - - builder - .genesis_state(genesis_state) - .map(|v| (v, Some(genesis_service.into_core_service())))? + return Err("Loading genesis from deposit contract no longer supported".to_string()); } - ClientGenesis::FromStore => builder.resume_from_db().map(|v| (v, None))?, + ClientGenesis::FromStore => builder.resume_from_db()?, }; - if config.sync_eth1_chain { - self.eth1_service = eth1_service_option; - } self.beacon_chain_builder = Some(beacon_chain_builder); Ok(self) } /// Starts the networking stack. - pub async fn network(mut self, config: Arc) -> Result { + pub async fn network( + mut self, + config: Arc, + local_keypair: Keypair, + ) -> Result { let beacon_chain = self .beacon_chain .clone() @@ -633,12 +494,12 @@ where }; let (network_globals, network_senders) = NetworkService::start( - beacon_chain, + beacon_chain.clone(), config, context.executor, libp2p_registry.as_mut(), beacon_processor_channels.beacon_processor_tx.clone(), - beacon_processor_channels.work_reprocessing_tx.clone(), + local_keypair, ) .await .map_err(|e| format!("Failed to start network: {:?}", e))?; @@ -753,9 +614,10 @@ where /// /// If type inference errors are being raised, see the comment on the definition of `Self`. #[allow(clippy::type_complexity)] + #[instrument(name = "build_client", skip_all)] pub fn build( mut self, - ) -> Result>, String> { + ) -> Result>, String> { let runtime_context = self .runtime_context .as_ref() @@ -775,11 +637,7 @@ where chain: self.beacon_chain.clone(), network_senders: self.network_senders.clone(), network_globals: self.network_globals.clone(), - eth1_service: self.eth1_service.clone(), beacon_processor_send: Some(beacon_processor_channels.beacon_processor_tx.clone()), - beacon_processor_reprocess_send: Some( - beacon_processor_channels.work_reprocessing_tx.clone(), - ), sse_logging_components: runtime_context.sse_logging_components.clone(), }); @@ -843,8 +701,6 @@ where } .spawn_manager( beacon_processor_channels.beacon_processor_rx, - beacon_processor_channels.work_reprocessing_tx.clone(), - beacon_processor_channels.work_reprocessing_rx, None, beacon_chain.slot_clock.clone(), beacon_chain.spec.maximum_gossip_clock_disparity(), @@ -918,7 +774,7 @@ where compute_light_client_updates( &inner_chain, light_client_server_rv, - beacon_processor_channels.work_reprocessing_tx, + beacon_processor_channels.beacon_processor_tx, ) .await }, @@ -950,16 +806,16 @@ where } } -impl - ClientBuilder> +impl + ClientBuilder> where TSlotClock: SlotClock + Clone + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, { /// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self. + #[instrument(skip_all)] pub fn build_beacon_chain(mut self) -> Result { let context = self .runtime_context @@ -987,11 +843,10 @@ where } } -impl - ClientBuilder, BeaconNodeBackend>> +impl + ClientBuilder, BeaconNodeBackend>> where TSlotClock: SlotClock + 'static, - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, { /// Specifies that the `Client` should use a `HotColdDB` database. @@ -1002,11 +857,6 @@ where blobs_path: &Path, config: StoreConfig, ) -> Result { - let context = self - .runtime_context - .as_ref() - .ok_or("disk_store requires a log")? - .service_context("freezer_db".into()); let spec = self .chain_spec .clone() @@ -1015,22 +865,8 @@ where self.db_path = Some(hot_path.into()); self.freezer_db_path = Some(cold_path.into()); - // Optionally grab the genesis state root. - // This will only be required if a DB upgrade to V22 is needed. - let genesis_state_root = context - .eth2_network_config - .as_ref() - .and_then(|config| config.genesis_state_root::().transpose()) - .transpose()?; - - let schema_upgrade = |db, from, to| { - migrate_schema::>( - db, - genesis_state_root, - from, - to, - ) - }; + let schema_upgrade = + |db, from, to| migrate_schema::>(db, from, to); let store = HotColdDB::open( hot_path, @@ -1046,102 +882,8 @@ where } } -impl - ClientBuilder, E, THotStore, TColdStore>> +impl ClientBuilder> where - TSlotClock: SlotClock + 'static, - E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, -{ - /// Specifies that the `BeaconChain` should cache eth1 blocks/logs from a remote eth1 node - /// (e.g., Parity/Geth) and refer to that cache when collecting deposits or eth1 votes during - /// block production. - pub async fn caching_eth1_backend(mut self, config: Eth1Config) -> Result { - let context = self - .runtime_context - .as_ref() - .ok_or("caching_eth1_backend requires a runtime_context")? - .service_context("deposit_contract_rpc".into()); - let beacon_chain_builder = self - .beacon_chain_builder - .ok_or("caching_eth1_backend requires a beacon_chain_builder")?; - let spec = self - .chain_spec - .clone() - .ok_or("caching_eth1_backend requires a chain spec")?; - - let backend = if let Some(eth1_service_from_genesis) = self.eth1_service { - eth1_service_from_genesis.update_config(config)?; - - // This cache is not useful because it's first (earliest) block likely the block that - // triggered genesis. - // - // In order to vote we need to be able to go back at least 2 * `ETH1_FOLLOW_DISTANCE` - // from the genesis-triggering block. Presently the block cache does not support - // importing blocks with decreasing block numbers, it only accepts them in increasing - // order. If this turns out to be a bottleneck we can update the block cache to allow - // adding earlier blocks too. - eth1_service_from_genesis.drop_block_cache(); - - CachingEth1Backend::from_service(eth1_service_from_genesis) - } else if config.purge_cache { - CachingEth1Backend::new(config, spec)? - } else { - beacon_chain_builder - .get_persisted_eth1_backend()? - .map(|persisted| { - Eth1Chain::from_ssz_container(&persisted, config.clone(), spec.clone()) - .map(|chain| chain.into_backend()) - }) - .unwrap_or_else(|| CachingEth1Backend::new(config, spec.clone()))? - }; - - self.eth1_service = Some(backend.core.clone()); - - // Starts the service that connects to an eth1 node and periodically updates caches. - backend.start(context.executor); - - self.beacon_chain_builder = Some(beacon_chain_builder.eth1_backend(Some(backend))); - - Ok(self) - } - - /// Do not use any eth1 backend. The client will not be able to produce beacon blocks. - pub fn no_eth1_backend(mut self) -> Result { - let beacon_chain_builder = self - .beacon_chain_builder - .ok_or("caching_eth1_backend requires a beacon_chain_builder")?; - - self.beacon_chain_builder = Some(beacon_chain_builder.no_eth1_backend()); - - Ok(self) - } - - /// Use an eth1 backend that can produce blocks but is not connected to an Eth1 node. - /// - /// This backend will never produce deposits so it's impossible to add validators after - /// genesis. The `Eth1Data` votes will be deterministic junk data. - /// - /// ## Notes - /// - /// The client is given the `CachingEth1Backend` type, but the http backend is never started and the - /// caches are never used. - pub fn dummy_eth1_backend(mut self) -> Result { - let beacon_chain_builder = self - .beacon_chain_builder - .ok_or("caching_eth1_backend requires a beacon_chain_builder")?; - - self.beacon_chain_builder = Some(beacon_chain_builder.dummy_eth1_backend()?); - - Ok(self) - } -} - -impl - ClientBuilder> -where - TEth1Backend: Eth1ChainBackend + 'static, E: EthSpec + 'static, THotStore: ItemStore + 'static, TColdStore: ItemStore + 'static, diff --git a/beacon_node/client/src/compute_light_client_updates.rs b/beacon_node/client/src/compute_light_client_updates.rs index fab284c428..0ef35588df 100644 --- a/beacon_node/client/src/compute_light_client_updates.rs +++ b/beacon_node/client/src/compute_light_client_updates.rs @@ -1,9 +1,9 @@ use beacon_chain::{BeaconChain, BeaconChainTypes, LightClientProducerEvent}; use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; -use futures::channel::mpsc::Receiver; +use beacon_processor::{BeaconProcessorSend, Work, WorkEvent}; use futures::StreamExt; -use tokio::sync::mpsc::Sender; -use tracing::error; +use futures::channel::mpsc::Receiver; +use tracing::{debug, error}; // Each `LightClientProducerEvent` is ~200 bytes. With the light_client server producing only recent // updates it is okay to drop some events in case of overloading. In normal network conditions @@ -14,7 +14,7 @@ pub(crate) const LIGHT_CLIENT_SERVER_CHANNEL_CAPACITY: usize = 32; pub async fn compute_light_client_updates( chain: &BeaconChain, mut light_client_server_rv: Receiver>, - reprocess_tx: Sender, + beacon_processor_send: BeaconProcessorSend, ) { // Should only receive events for recent blocks, import_block filters by blocks close to clock. // @@ -27,11 +27,17 @@ pub async fn compute_light_client_updates( chain .recompute_and_cache_light_client_updates(event) .unwrap_or_else(|e| { - error!("error computing light_client updates {:?}", e); + debug!("error computing light_client updates {:?}", e); }); let msg = ReprocessQueueMessage::NewLightClientOptimisticUpdate { parent_root }; - if reprocess_tx.try_send(msg).is_err() { + if beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: true, + work: Work::Reprocess(msg), + }) + .is_err() + { error!(%parent_root,"Failed to inform light client update") }; } diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index becc781ed3..aeaa196df8 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,6 +1,5 @@ use beacon_chain::graffiti_calculator::GraffitiOrigin; use beacon_chain::validator_monitor::ValidatorMonitorConfig; -use beacon_chain::TrustedSetup; use beacon_processor::BeaconProcessorConfig; use directory::DEFAULT_ROOT_DIR; use environment::LoggerConfig; @@ -59,7 +58,6 @@ pub struct Config { /// Path where the blobs database will be located if blobs should be in a separate database. pub blobs_db_path: Option, pub log_file: PathBuf, - pub sync_eth1_chain: bool, /// Graffiti to be inserted everytime we create a block if the validator doesn't specify. pub beacon_graffiti: GraffitiOrigin, pub validator_monitor: ValidatorMonitorConfig, @@ -70,9 +68,8 @@ pub struct Config { pub store: store::StoreConfig, pub network: network::NetworkConfig, pub chain: beacon_chain::ChainConfig, - pub eth1: eth1::Config, pub execution_layer: Option, - pub trusted_setup: TrustedSetup, + pub trusted_setup: Vec, pub http_api: http_api::Config, pub http_metrics: http_metrics::Config, pub monitoring_api: Option, @@ -86,9 +83,6 @@ pub struct Config { impl Default for Config { fn default() -> Self { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .expect("Unable to read trusted setup file"); - Self { data_dir: PathBuf::from(DEFAULT_ROOT_DIR), db_name: "chain_db".to_string(), @@ -99,10 +93,8 @@ impl Default for Config { store: <_>::default(), network: NetworkConfig::default(), chain: <_>::default(), - sync_eth1_chain: true, - eth1: <_>::default(), execution_layer: None, - trusted_setup, + trusted_setup: get_trusted_setup(), beacon_graffiti: GraffitiOrigin::default(), http_api: <_>::default(), http_metrics: <_>::default(), diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index 0b6550c208..916dae6db0 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -10,7 +10,7 @@ use lighthouse_network::{Enr, Multiaddr, NetworkGlobals}; use std::net::SocketAddr; use std::sync::Arc; -pub use beacon_chain::{BeaconChainTypes, Eth1ChainBackend}; +pub use beacon_chain::BeaconChainTypes; pub use builder::ClientBuilder; pub use config::{ClientGenesis, Config as ClientConfig}; pub use eth2_config::Eth2Config; diff --git a/beacon_node/client/src/metrics.rs b/beacon_node/client/src/metrics.rs index e5c07baddc..6ff3eb6a70 100644 --- a/beacon_node/client/src/metrics.rs +++ b/beacon_node/client/src/metrics.rs @@ -11,7 +11,14 @@ pub static SYNC_SLOTS_PER_SECOND: LazyLock> = LazyLock::new(|| pub static IS_SYNCED: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "sync_eth2_synced", - "Metric to check if the beacon chain is synced to head. 0 if not synced and non-zero if synced" + "Metric to check if the beacon chain is synced to head. 0 if not synced and non-zero if synced", + ) +}); + +pub static IS_OPTIMISTIC_SYNC: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "optimistic_sync", + "Metric to check if the beacon chain is in optimistic sync mode. 0 if synced and 1 if optimistic sync", ) }); diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index d2f8c9eb2e..6236bcc3e1 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -1,14 +1,19 @@ use crate::metrics; use beacon_chain::{ - bellatrix_readiness::{BellatrixReadiness, GenesisExecutionPayloadStatus, MergeConfig}, - capella_readiness::CapellaReadiness, - deneb_readiness::DenebReadiness, - eip7805_readiness::Eip7805Readiness, - electra_readiness::ElectraReadiness, - fulu_readiness::FuluReadiness, BeaconChain, BeaconChainTypes, ExecutionStatus, + bellatrix_readiness::{ + BellatrixReadiness, GenesisExecutionPayloadStatus, MergeConfig, SECONDS_IN_A_WEEK, + }, }; -use lighthouse_network::{types::SyncState, NetworkGlobals}; +use execution_layer::{ + EngineCapabilities, + http::{ + ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_GET_PAYLOAD_V2, + ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V2, + ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, + }, +}; +use lighthouse_network::{NetworkGlobals, types::SyncState}; use logging::crit; use slot_clock::SlotClock; use std::sync::Arc; @@ -31,6 +36,9 @@ const SPEEDO_OBSERVATIONS: usize = 4; /// The number of slots between logs that give detail about backfill process. const BACKFILL_LOG_INTERVAL: u64 = 5; +pub const FORK_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; +pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; + /// Spawns a notifier service which periodically logs information about the node. pub fn spawn_notifier( executor: task_executor::TaskExecutor, @@ -49,6 +57,9 @@ pub fn spawn_notifier( // Store info if we are required to do a backfill sync. let original_oldest_block_slot = beacon_chain.store.get_anchor_info().oldest_block_slot; + // Use this info during custody backfill sync. + let mut original_earliest_data_column_slot = None; + let interval_future = async move { // Perform pre-genesis logging. loop { @@ -61,9 +72,8 @@ pub fn spawn_notifier( wait_time = estimated_time_pretty(Some(next_slot.as_secs() as f64)), "Waiting for genesis" ); - eth1_logging(&beacon_chain); bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; - capella_readiness_logging(Slot::new(0), &beacon_chain).await; + post_bellatrix_readiness_logging(Slot::new(0), &beacon_chain).await; genesis_execution_payload_logging(&beacon_chain).await; sleep(slot_duration).await; } @@ -73,6 +83,7 @@ pub fn spawn_notifier( // Perform post-genesis logging. let mut last_backfill_log_slot = None; + let mut last_custody_backfill_log_slot = None; loop { // Run the notifier half way through each slot. @@ -105,6 +116,18 @@ pub fn spawn_notifier( let mut speedo = speedo.lock().await; speedo.clear(); } + (_, SyncState::CustodyBackFillSyncing { .. }) => { + // We have transitioned to a custody backfill sync. Reset the speedo. + let mut speedo = speedo.lock().await; + last_custody_backfill_log_slot = None; + speedo.clear(); + } + (SyncState::CustodyBackFillSyncing { .. }, _) => { + // We have transitioned from a custody backfill sync, reset the speedo + let mut speedo = speedo.lock().await; + last_custody_backfill_log_slot = None; + speedo.clear(); + } (_, _) => {} } current_sync_state = sync_state; @@ -147,6 +170,38 @@ pub fn spawn_notifier( Instant::now(), ); } + SyncState::CustodyBackFillSyncing { .. } => { + match beacon_chain.store.get_data_column_custody_info() { + Ok(data_column_custody_info) => { + if let Some(earliest_data_column_slot) = data_column_custody_info + .and_then(|info| info.earliest_data_column_slot) + && let Some(da_boundary) = beacon_chain.get_column_da_boundary() + { + sync_distance = earliest_data_column_slot.saturating_sub( + da_boundary.start_slot(T::EthSpec::slots_per_epoch()), + ); + + // We keep track of our starting point for custody backfill sync + // so we can measure our speed of progress. + if original_earliest_data_column_slot.is_none() { + original_earliest_data_column_slot = + Some(earliest_data_column_slot) + } + + if let Some(original_earliest_data_column_slot) = + original_earliest_data_column_slot + { + speedo.observe( + original_earliest_data_column_slot + .saturating_sub(earliest_data_column_slot), + Instant::now(), + ); + } + } + } + Err(e) => error!(error=?e, "Unable to get data column custody info"), + } + } SyncState::SyncingFinalized { .. } | SyncState::SyncingHead { .. } | SyncState::SyncTransition => { @@ -183,6 +238,8 @@ pub fn spawn_notifier( // Log if we are backfilling. let is_backfilling = matches!(current_sync_state, SyncState::BackFillSyncing { .. }); + let is_custody_backfilling = + matches!(current_sync_state, SyncState::CustodyBackFillSyncing { .. }); if is_backfilling && last_backfill_log_slot .is_none_or(|slot| slot + BACKFILL_LOG_INTERVAL <= current_slot) @@ -227,6 +284,51 @@ pub fn spawn_notifier( info!("Historical block download complete"); } + if is_custody_backfilling + && last_custody_backfill_log_slot + .is_none_or(|slot| slot + BACKFILL_LOG_INTERVAL <= current_slot) + { + last_custody_backfill_log_slot = Some(current_slot); + + let distance = format!( + "{} slots ({})", + sync_distance.as_u64(), + slot_distance_pretty(sync_distance, slot_duration) + ); + + let speed = speedo.slots_per_second(); + let display_speed = speed.is_some_and(|speed| speed != 0.0); + let est_time_in_secs = if let (Some(da_boundary_epoch), Some(original_slot)) = ( + beacon_chain.get_column_da_boundary(), + original_earliest_data_column_slot, + ) { + let target = original_slot.saturating_sub( + da_boundary_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ); + speedo.estimated_time_till_slot(target) + } else { + None + }; + if display_speed { + info!( + distance, + speed = sync_speed_pretty(speed), + est_time = estimated_time_pretty(est_time_in_secs), + "Downloading historical data columns" + ); + } else { + info!( + distance, + est_time = estimated_time_pretty(est_time_in_secs), + "Downloading historical data columns" + ); + } + } else if !is_custody_backfilling && last_custody_backfill_log_slot.is_some() { + last_custody_backfill_log_slot = None; + original_earliest_data_column_slot = None; + info!("Historical data column download complete"); + } + // Log if we are syncing if current_sync_state.is_syncing() { metrics::set_gauge(&metrics::IS_SYNCED, 0); @@ -267,8 +369,12 @@ pub fn spawn_notifier( let block_hash = match beacon_chain.canonical_head.head_execution_status() { Ok(ExecutionStatus::Irrelevant(_)) => "n/a".to_string(), - Ok(ExecutionStatus::Valid(hash)) => format!("{} (verified)", hash), + Ok(ExecutionStatus::Valid(hash)) => { + metrics::set_gauge(&metrics::IS_OPTIMISTIC_SYNC, 0); + format!("{} (verified)", hash) + } Ok(ExecutionStatus::Optimistic(hash)) => { + metrics::set_gauge(&metrics::IS_OPTIMISTIC_SYNC, 1); warn!( info = "chain not fully verified, \ block and attestation production disabled until execution engine syncs", @@ -310,13 +416,8 @@ pub fn spawn_notifier( ); } - eth1_logging(&beacon_chain); bellatrix_readiness_logging(current_slot, &beacon_chain).await; - capella_readiness_logging(current_slot, &beacon_chain).await; - deneb_readiness_logging(current_slot, &beacon_chain).await; - electra_readiness_logging(current_slot, &beacon_chain).await; - eip7805_readiness_logging(current_slot, &beacon_chain).await; - fulu_readiness_logging(current_slot, &beacon_chain).await; + post_bellatrix_readiness_logging(current_slot, &beacon_chain).await; } }; @@ -350,18 +451,6 @@ async fn bellatrix_readiness_logging( return; } - if merge_completed && !has_execution_layer { - // Logging of the EE being offline is handled in the other readiness logging functions. - if !beacon_chain.is_time_to_prepare_for_capella(current_slot) { - error!( - info = "you need an execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", - "Execution endpoint required" - ); - } - return; - } - match beacon_chain.check_bellatrix_readiness(current_slot).await { BellatrixReadiness::Ready { config, @@ -410,265 +499,156 @@ async fn bellatrix_readiness_logging( } /// Provides some helpful logging to users to indicate if their node is ready for Capella -async fn capella_readiness_logging( +async fn post_bellatrix_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, ) { - let capella_completed = beacon_chain + if let Some(fork) = find_next_fork_to_prepare(current_slot, beacon_chain) { + let readiness = if let Some(el) = beacon_chain.execution_layer.as_ref() { + match el + .get_engine_capabilities(Some(Duration::from_secs( + ENGINE_CAPABILITIES_REFRESH_INTERVAL, + ))) + .await + { + Err(e) => Err(format!("Exchange capabilities failed: {e:?}")), + Ok(capabilities) => { + let missing_methods = methods_required_for_fork(fork, capabilities); + if missing_methods.is_empty() { + Ok(()) + } else { + Err(format!("Missing required methods: {missing_methods:?}")) + } + } + } + } else { + Err("No execution endpoint".to_string()) + }; + + if let Err(readiness) = readiness { + warn!( + info = %readiness, + "Not ready for {}", fork + ); + } else { + info!( + info = "ensure the execution endpoint is updated to the latest release", + "Ready for {}", fork + ) + } + } +} + +fn find_next_fork_to_prepare( + current_slot: Slot, + beacon_chain: &BeaconChain, +) -> Option { + let head_fork = beacon_chain .canonical_head .cached_head() .snapshot .beacon_state - .fork_name_unchecked() - .capella_enabled(); + .fork_name_unchecked(); - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if capella_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_capella(current_slot) + // Iterate forks from latest to oldest + for (fork, fork_epoch) in ForkName::list_all_fork_epochs(&beacon_chain.spec) + .iter() + .rev() { - return; + // This readiness only handles capella and post fork + if *fork <= ForkName::Bellatrix { + break; + } + + // head state has already activated this fork + if head_fork >= *fork { + break; + } + + // Find the first fork that is scheduled and close to happen + if let Some(fork_epoch) = fork_epoch { + let fork_slot = fork_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let preparation_slots = + FORK_READINESS_PREPARATION_SECONDS / beacon_chain.spec.seconds_per_slot; + let in_fork_preparation_period = current_slot + preparation_slots > fork_slot; + if in_fork_preparation_period { + return Some(*fork); + } + } } - if capella_completed && !has_execution_layer { - // Logging of the EE being offline is handled in the other readiness logging functions. - if !beacon_chain.is_time_to_prepare_for_deneb(current_slot) { - error!( - info = "you need a Capella enabled execution engine to validate blocks, see: \ - https://lighthouse-book.sigmaprime.io/archived_merge_migration.html", - "Execution endpoint required" + None +} + +fn methods_required_for_fork( + fork: ForkName, + capabilities: EngineCapabilities, +) -> Vec<&'static str> { + let mut missing_methods = vec![]; + match fork { + ForkName::Base | ForkName::Altair | ForkName::Bellatrix => { + warn!( + fork = %fork, + "Invalid methods_required_for_fork call" ); } - return; - } - - match beacon_chain.check_capella_readiness().await { - CapellaReadiness::Ready => { - info!( - info = "ensure the execution endpoint is updated to the latest Capella/Shanghai release", - "Ready for Capella" - ) + ForkName::Capella => { + if !capabilities.get_payload_v2 { + missing_methods.push(ENGINE_GET_PAYLOAD_V2); + } + if !capabilities.forkchoice_updated_v2 { + missing_methods.push(ENGINE_FORKCHOICE_UPDATED_V2); + } + if !capabilities.new_payload_v2 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V2); + } } - readiness @ CapellaReadiness::ExchangeCapabilitiesFailed { error: _ } => { - error!( - hint = "the execution endpoint may be offline", - info = %readiness, - "Not ready for Capella" - ) + ForkName::Deneb => { + if !capabilities.get_payload_v3 { + missing_methods.push(ENGINE_GET_PAYLOAD_V3); + } + if !capabilities.forkchoice_updated_v3 { + missing_methods.push(ENGINE_FORKCHOICE_UPDATED_V3); + } + if !capabilities.new_payload_v3 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V3); + } } - readiness => warn!( - hint = "try updating the execution endpoint", - info = %readiness, - "Not ready for Capella" - ), - } -} - -/// Provides some helpful logging to users to indicate if their node is ready for Deneb -async fn deneb_readiness_logging( - current_slot: Slot, - beacon_chain: &BeaconChain, -) { - let deneb_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .fork_name_unchecked() - .deneb_enabled(); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if deneb_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_deneb(current_slot) - { - return; - } - - if deneb_completed && !has_execution_layer { - error!( - info = "you need a Deneb enabled execution engine to validate blocks.", - "Execution endpoint required" - ); - return; - } - - match beacon_chain.check_deneb_readiness().await { - DenebReadiness::Ready => { - info!( - info = - "ensure the execution endpoint is updated to the latest Deneb/Cancun release", - "Ready for Deneb" - ) + ForkName::Electra => { + if !capabilities.get_payload_v4 { + missing_methods.push(ENGINE_GET_PAYLOAD_V4); + } + if !capabilities.new_payload_v4 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V4); + } } - readiness @ DenebReadiness::ExchangeCapabilitiesFailed { error: _ } => { - error!( - hint = "the execution endpoint may be offline", - info = %readiness, - "Not ready for Deneb" - ) + ForkName::Fulu => { + if !capabilities.get_payload_v5 { + missing_methods.push(ENGINE_GET_PAYLOAD_V5); + } + if !capabilities.new_payload_v4 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V4); + } } - readiness => warn!( - hint = "try updating the execution endpoint", - info = %readiness, - "Not ready for Deneb" - ), - } -} -/// Provides some helpful logging to users to indicate if their node is ready for Electra. -async fn electra_readiness_logging( - current_slot: Slot, - beacon_chain: &BeaconChain, -) { - let electra_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .fork_name_unchecked() - .electra_enabled(); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if electra_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_electra(current_slot) - { - return; - } - - if electra_completed && !has_execution_layer { - // When adding a new fork, add a check for the next fork readiness here. - error!( - info = "you need a Electra enabled execution engine to validate blocks.", - "Execution endpoint required" - ); - return; - } - - match beacon_chain.check_electra_readiness().await { - ElectraReadiness::Ready => { - info!( - info = - "ensure the execution endpoint is updated to the latest Electra/Prague release", - "Ready for Electra" - ) + // TODO(EIP7805) check capabilities + ForkName::Eip7805 => { + if !capabilities.get_payload_v5 { + missing_methods.push(ENGINE_GET_PAYLOAD_V5); + } + if !capabilities.new_payload_v4 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V4); + } } - readiness @ ElectraReadiness::ExchangeCapabilitiesFailed { error: _ } => { - error!( - hint = "the execution endpoint may be offline", - info = %readiness, - "Not ready for Electra" - ) + ForkName::Gloas => { + if !capabilities.get_payload_v5 { + missing_methods.push(ENGINE_GET_PAYLOAD_V5); + } + if !capabilities.new_payload_v4 { + missing_methods.push(ENGINE_NEW_PAYLOAD_V4); + } } - readiness => warn!( - hint = "try updating the execution endpoint", - info = %readiness, - "Not ready for Electra" - ), - } -} - -/// Provides some helpful logging to users to indicate if their node is ready for Eip7805. -async fn eip7805_readiness_logging( - current_slot: Slot, - beacon_chain: &BeaconChain, -) { - let eip7805_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .fork_name_unchecked() - .eip7805_enabled(); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if eip7805_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_electra(current_slot) - { - return; - } - - if eip7805_completed && !has_execution_layer { - // When adding a new fork, add a check for the next fork readiness here. - error!( - info = "you need a Eip7805 enabled execution engine to validate blocks.", - "Execution endpoint required" - ); - return; - } - - match beacon_chain.check_eip7805_readiness().await { - Eip7805Readiness::Ready => { - info!( - info = "ensure the execution endpoint is updated to the latest eip7805 release", - "Ready for Eip7805" - ) - } - readiness @ Eip7805Readiness::ExchangeCapabilitiesFailed { error: _ } => { - error!( - hint = "the execution endpoint may be offline", - info = %readiness, - "Not ready for Eip7805Readiness" - ) - } - readiness => warn!( - hint = "try updating the execution endpoint", - info = %readiness, - "Not ready for Eip7805Readiness" - ), - } -} - -/// Provides some helpful logging to users to indicate if their node is ready for Fulu. -async fn fulu_readiness_logging( - current_slot: Slot, - beacon_chain: &BeaconChain, -) { - let fulu_completed = beacon_chain - .canonical_head - .cached_head() - .snapshot - .beacon_state - .fork_name_unchecked() - .fulu_enabled(); - - let has_execution_layer = beacon_chain.execution_layer.is_some(); - - if fulu_completed && has_execution_layer - || !beacon_chain.is_time_to_prepare_for_fulu(current_slot) - { - return; - } - - if fulu_completed && !has_execution_layer { - error!( - info = "you need a Fulu enabled execution engine to validate blocks.", - "Execution endpoint required" - ); - return; - } - - match beacon_chain.check_fulu_readiness().await { - FuluReadiness::Ready => { - info!( - info = "ensure the execution endpoint is updated to the latest Fulu release", - "Ready for Fulu" - ) - } - readiness @ FuluReadiness::ExchangeCapabilitiesFailed { error: _ } => { - error!( - hint = "the execution endpoint may be offline", - info = %readiness, - "Not ready for Fulu" - ) - } - readiness => warn!( - hint = "try updating the execution endpoint", - info = %readiness, - "Not ready for Fulu" - ), } + missing_methods } async fn genesis_execution_payload_logging(beacon_chain: &BeaconChain) { @@ -731,53 +711,6 @@ async fn genesis_execution_payload_logging(beacon_chain: &B } } -fn eth1_logging(beacon_chain: &BeaconChain) { - let current_slot_opt = beacon_chain.slot().ok(); - - // Perform some logging about the eth1 chain - if let Some(eth1_chain) = beacon_chain.eth1_chain.as_ref() { - // No need to do logging if using the dummy backend. - if eth1_chain.is_dummy_backend() { - return; - } - - if let Some(status) = eth1_chain.sync_status( - beacon_chain.genesis_time, - current_slot_opt, - &beacon_chain.spec, - ) { - debug!( - eth1_head_block = status.head_block_number, - latest_cached_block_number = status.latest_cached_block_number, - latest_cached_timestamp = status.latest_cached_block_timestamp, - voting_target_timestamp = status.voting_target_timestamp, - ready = status.lighthouse_is_cached_and_ready, - "Eth1 cache sync status" - ); - - if !status.lighthouse_is_cached_and_ready { - let voting_target_timestamp = status.voting_target_timestamp; - - let distance = status - .latest_cached_block_timestamp - .map(|latest| { - voting_target_timestamp.saturating_sub(latest) - / beacon_chain.spec.seconds_per_eth1_block - }) - .map(|distance| distance.to_string()) - .unwrap_or_else(|| "initializing deposits".to_string()); - - warn!( - est_blocks_remaining = distance, - "Syncing deposit contract block cache" - ); - } - } else { - error!("Unable to determine deposit contract sync status"); - } - } -} - /// Returns the peer count, returning something helpful if it's `usize::MAX` (effectively a /// `None` value). fn peer_count_pretty(peer_count: usize) -> String { diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml deleted file mode 100644 index fa08364251..0000000000 --- a/beacon_node/eth1/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "eth1" -version = "0.2.0" -authors = ["Paul Hauner "] -edition = { workspace = true } - -[dev-dependencies] -environment = { workspace = true } -eth1_test_rig = { workspace = true } -serde_yaml = { workspace = true } - -[dependencies] -eth2 = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } -execution_layer = { workspace = true } -futures = { workspace = true } -logging = { workspace = true } -merkle_proof = { workspace = true } -metrics = { workspace = true } -parking_lot = { workspace = true } -sensitive_url = { workspace = true } -serde = { workspace = true } -state_processing = { workspace = true } -superstruct = { workspace = true } -task_executor = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -tree_hash = { workspace = true } -types = { workspace = true } diff --git a/beacon_node/eth1/src/block_cache.rs b/beacon_node/eth1/src/block_cache.rs deleted file mode 100644 index 9c840aea21..0000000000 --- a/beacon_node/eth1/src/block_cache.rs +++ /dev/null @@ -1,303 +0,0 @@ -use ssz_derive::{Decode, Encode}; -use std::collections::HashMap; -use std::ops::RangeInclusive; - -pub use eth2::lighthouse::Eth1Block; -use eth2::types::Hash256; -use std::sync::Arc; - -#[derive(Debug, PartialEq, Clone)] -pub enum Error { - /// The timestamp of each block equal to or later than the block prior to it. - InconsistentTimestamp { parent: u64, child: u64 }, - /// Some `Eth1Block` was provided with the same block number but different data. The source - /// of eth1 data is inconsistent. - Conflicting(u64), - /// The given block was not one block number higher than the highest known block number. - NonConsecutive { given: u64, expected: u64 }, - /// Some invariant was violated, there is a likely bug in the code. - Internal(String), -} - -/// Stores block and deposit contract information and provides queries based upon the block -/// timestamp. -#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)] -pub struct BlockCache { - blocks: Vec>, - #[ssz(skip_serializing, skip_deserializing)] - by_hash: HashMap>, -} - -impl BlockCache { - /// Returns the number of blocks stored in `self`. - pub fn len(&self) -> usize { - self.blocks.len() - } - - /// True if the cache does not store any blocks. - pub fn is_empty(&self) -> bool { - self.blocks.is_empty() - } - - /// Returns the earliest (lowest timestamp) block, if any. - pub fn earliest_block(&self) -> Option<&Eth1Block> { - self.blocks.first().map(|ptr| ptr.as_ref()) - } - - /// Returns the latest (highest timestamp) block, if any. - pub fn latest_block(&self) -> Option<&Eth1Block> { - self.blocks.last().map(|ptr| ptr.as_ref()) - } - - /// Returns the timestamp of the earliest block in the cache (if any). - pub fn earliest_block_timestamp(&self) -> Option { - self.blocks.first().map(|block| block.timestamp) - } - - /// Returns the timestamp of the latest block in the cache (if any). - pub fn latest_block_timestamp(&self) -> Option { - self.blocks.last().map(|block| block.timestamp) - } - - /// Returns the lowest block number stored. - pub fn lowest_block_number(&self) -> Option { - self.blocks.first().map(|block| block.number) - } - - /// Returns the highest block number stored. - pub fn highest_block_number(&self) -> Option { - self.blocks.last().map(|block| block.number) - } - - /// Returns an iterator over all blocks. - /// - /// Blocks a guaranteed to be returned with; - /// - /// - Monotonically increasing block numbers. - /// - Non-uniformly increasing block timestamps. - pub fn iter(&self) -> impl DoubleEndedIterator + Clone { - self.blocks.iter().map(|ptr| ptr.as_ref()) - } - - /// Shortens the cache, keeping the latest (by block number) `len` blocks while dropping the - /// rest. - /// - /// If `len` is greater than the vector's current length, this has no effect. - pub fn truncate(&mut self, len: usize) { - if len < self.blocks.len() { - let remaining = self.blocks.split_off(self.blocks.len() - len); - for block in &self.blocks { - self.by_hash.remove(&block.hash); - } - self.blocks = remaining; - } - } - - /// Returns the range of block numbers stored in the block cache. All blocks in this range can - /// be accessed. - fn available_block_numbers(&self) -> Option> { - Some(self.blocks.first()?.number..=self.blocks.last()?.number) - } - - /// Returns a block with the corresponding number, if any. - pub fn block_by_number(&self, block_number: u64) -> Option<&Eth1Block> { - self.blocks - .get( - self.blocks - .as_slice() - .binary_search_by(|block| block.number.cmp(&block_number)) - .ok()?, - ) - .map(|ptr| ptr.as_ref()) - } - - /// Returns a block with the corresponding hash, if any. - pub fn block_by_hash(&self, block_hash: &Hash256) -> Option<&Eth1Block> { - self.by_hash.get(block_hash).map(|ptr| ptr.as_ref()) - } - - /// Rebuilds the by_hash map - pub fn rebuild_by_hash_map(&mut self) { - self.by_hash.clear(); - for block in self.blocks.iter() { - self.by_hash.insert(block.hash, block.clone()); - } - } - - /// Insert an `Eth1Snapshot` into `self`, allowing future queries. - /// - /// Allows inserting either: - /// - /// - The root block (i.e., any block if there are no existing blocks), or, - /// - An immediate child of the most recent (highest block number) block. - /// - /// ## Errors - /// - /// - If the cache is not empty and `item.block.block_number - 1` is not already in `self`. - /// - If `item.block.block_number` is in `self`, but is not identical to the supplied - /// `Eth1Snapshot`. - /// - If `item.block.timestamp` is prior to the parent. - pub fn insert_root_or_child(&mut self, block: Eth1Block) -> Result<(), Error> { - let expected_block_number = self - .highest_block_number() - .map(|n| n + 1) - .unwrap_or_else(|| block.number); - - // If there are already some cached blocks, check to see if the new block number is one of - // them. - // - // If the block is already known, check to see the given block is identical to it. If not, - // raise an inconsistency error. This is mostly likely caused by some fork on the eth1 - // chain. - if let Some(local) = self.available_block_numbers() { - if local.contains(&block.number) { - let known_block = self.block_by_number(block.number).ok_or_else(|| { - Error::Internal("An expected block was not present".to_string()) - })?; - - if known_block == &block { - return Ok(()); - } else { - return Err(Error::Conflicting(block.number)); - }; - } - } - - // Only permit blocks when it's either: - // - // - The first block inserted. - // - Exactly one block number higher than the highest known block number. - if block.number != expected_block_number { - return Err(Error::NonConsecutive { - given: block.number, - expected: expected_block_number, - }); - } - - // If the block is not the first block inserted, ensure that its timestamp is not higher - // than its parents. - if let Some(previous_block) = self.blocks.last() { - if previous_block.timestamp > block.timestamp { - return Err(Error::InconsistentTimestamp { - parent: previous_block.timestamp, - child: block.timestamp, - }); - } - } - - let ptr = Arc::new(block); - self.by_hash.insert(ptr.hash, ptr.clone()); - self.blocks.push(ptr); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use types::FixedBytesExtended; - - use super::*; - - fn get_block(i: u64, interval_secs: u64) -> Eth1Block { - Eth1Block { - hash: Hash256::from_low_u64_be(i), - timestamp: i * interval_secs, - number: i, - deposit_root: Some(Hash256::from_low_u64_be(i << 32)), - deposit_count: Some(i), - } - } - - fn get_blocks(n: usize, interval_secs: u64) -> Vec { - (0..n as u64).map(|i| get_block(i, interval_secs)).collect() - } - - fn insert(cache: &mut BlockCache, s: Eth1Block) -> Result<(), Error> { - cache.insert_root_or_child(s) - } - - #[test] - fn truncate() { - let n = 16; - let blocks = get_blocks(n, 10); - - let mut cache = BlockCache::default(); - - for block in blocks { - insert(&mut cache, block.clone()).expect("should add consecutive blocks"); - } - - for len in &[0, 1, 2, 3, 4, 8, 15, 16] { - let mut cache = cache.clone(); - - cache.truncate(*len); - - assert_eq!( - cache.blocks.len(), - *len, - "should truncate to length: {}", - *len - ); - } - - let mut cache_2 = cache; - cache_2.truncate(17); - assert_eq!( - cache_2.blocks.len(), - n, - "truncate to larger than n should be a no-op" - ); - } - - #[test] - fn inserts() { - let n = 16; - let blocks = get_blocks(n, 10); - - let mut cache = BlockCache::default(); - - for block in blocks { - insert(&mut cache, block.clone()).expect("should add consecutive blocks"); - } - - // No error for re-adding a block identical to one that exists. - assert!(insert(&mut cache, get_block(n as u64 - 1, 10)).is_ok()); - - // Error for re-adding a block that is different to the one that exists. - assert!(insert(&mut cache, get_block(n as u64 - 1, 11)).is_err()); - - // Error for adding non-consecutive blocks. - assert!(insert(&mut cache, get_block(n as u64 + 1, 10)).is_err()); - assert!(insert(&mut cache, get_block(n as u64 + 2, 10)).is_err()); - - // Error for adding timestamp prior to previous. - assert!(insert(&mut cache, get_block(n as u64, 1)).is_err()); - // Double check to make sure previous test was only affected by timestamp. - assert!(insert(&mut cache, get_block(n as u64, 10)).is_ok()); - } - - #[test] - fn duplicate_timestamp() { - let mut blocks = get_blocks(7, 10); - - blocks[0].timestamp = 0; - blocks[1].timestamp = 10; - blocks[2].timestamp = 10; - blocks[3].timestamp = 20; - blocks[4].timestamp = 30; - blocks[5].timestamp = 40; - blocks[6].timestamp = 40; - - let mut cache = BlockCache::default(); - - for block in &blocks { - insert(&mut cache, block.clone()) - .expect("should add consecutive blocks with duplicate timestamps"); - } - - let blocks = blocks.into_iter().map(Arc::new).collect::>(); - - assert_eq!(cache.blocks, blocks, "should have added all blocks"); - } -} diff --git a/beacon_node/eth1/src/deposit_cache.rs b/beacon_node/eth1/src/deposit_cache.rs deleted file mode 100644 index a2d4a1cf06..0000000000 --- a/beacon_node/eth1/src/deposit_cache.rs +++ /dev/null @@ -1,1090 +0,0 @@ -use crate::{DepositLog, Eth1Block}; -use ssz_derive::{Decode, Encode}; -use state_processing::common::DepositDataTree; -use std::cmp::Ordering; -use superstruct::superstruct; -use tree_hash::TreeHash; -use types::{Deposit, DepositTreeSnapshot, Hash256, DEPOSIT_TREE_DEPTH}; - -#[derive(Debug, PartialEq)] -pub enum Error { - /// A deposit log was added when a prior deposit was not already in the cache. - /// - /// Logs have to be added with monotonically-increasing block numbers. - NonConsecutive { log_index: u64, expected: usize }, - /// The eth1 event log data was unable to be parsed. - LogParse(String), - /// There are insufficient deposits in the cache to fulfil the request. - InsufficientDeposits { - known_deposits: usize, - requested: u64, - }, - /// A log with the given index is already present in the cache and it does not match the one - /// provided. - DuplicateDistinctLog(u64), - /// Attempted to insert log with given index after the log had been finalized - FinalizedLogInsert { - log_index: u64, - finalized_index: u64, - }, - /// The deposit count must always be large enough to account for the requested deposit range. - /// - /// E.g., you cannot request deposit 10 when the deposit count is 9. - DepositCountInvalid { deposit_count: u64, range_end: u64 }, - /// You can't request deposits on or before the finalized deposit - DepositRangeInvalid { - range_start: u64, - finalized_count: u64, - }, - /// You can't finalize what's already been finalized and the cache must have the logs - /// that you wish to finalize - InvalidFinalizeIndex { - requested_count: u64, - currently_finalized: u64, - deposit_count: u64, - }, - /// Error with the merkle tree for deposits. - DepositTree(merkle_proof::MerkleTreeError), - /// An unexpected condition was encountered. - Internal(String), - /// This is for errors that should never occur - PleaseNotifyTheDevs, -} - -pub type SszDepositCache = SszDepositCacheV13; - -#[superstruct( - variants(V13), - variant_attributes(derive(Encode, Decode, Clone)), - no_enum -)] -pub struct SszDepositCache { - pub logs: Vec, - pub leaves: Vec, - pub deposit_contract_deploy_block: u64, - pub finalized_deposit_count: u64, - pub finalized_block_height: u64, - pub deposit_tree_snapshot: Option, - pub deposit_roots: Vec, -} - -impl SszDepositCache { - pub fn from_deposit_cache(cache: &DepositCache) -> Self { - Self { - logs: cache.logs.clone(), - leaves: cache.leaves.clone(), - deposit_contract_deploy_block: cache.deposit_contract_deploy_block, - finalized_deposit_count: cache.finalized_deposit_count, - finalized_block_height: cache.finalized_block_height, - deposit_tree_snapshot: cache.deposit_tree.get_snapshot(), - deposit_roots: cache.deposit_roots.clone(), - } - } - - pub fn to_deposit_cache(&self) -> Result { - let deposit_tree = self - .deposit_tree_snapshot - .as_ref() - .map(|snapshot| { - let mut tree = DepositDataTree::from_snapshot(snapshot, DEPOSIT_TREE_DEPTH) - .map_err(|e| format!("Invalid SszDepositCache: {:?}", e))?; - for leaf in &self.leaves { - tree.push_leaf(*leaf).map_err(|e| { - format!("Invalid SszDepositCache: unable to push leaf: {:?}", e) - })?; - } - Ok::<_, String>(tree) - }) - .unwrap_or_else(|| { - // deposit_tree_snapshot = None (tree was never finalized) - // Create DepositDataTree from leaves - Ok(DepositDataTree::create( - &self.leaves, - self.leaves.len(), - DEPOSIT_TREE_DEPTH, - )) - })?; - - // Check for invalid SszDepositCache conditions - if self.leaves.len() != self.logs.len() { - return Err("Invalid SszDepositCache: logs and leaves should have equal length".into()); - } - // `deposit_roots` also includes the zero root - if self.leaves.len() + 1 != self.deposit_roots.len() { - return Err( - "Invalid SszDepositCache: deposit_roots length must be only one more than leaves" - .into(), - ); - } - Ok(DepositCache { - logs: self.logs.clone(), - leaves: self.leaves.clone(), - deposit_contract_deploy_block: self.deposit_contract_deploy_block, - finalized_deposit_count: self.finalized_deposit_count, - finalized_block_height: self.finalized_block_height, - deposit_tree, - deposit_roots: self.deposit_roots.clone(), - }) - } -} - -/// Mirrors the merkle tree of deposits in the eth1 deposit contract. -/// -/// Provides `Deposit` objects with merkle proofs included. -#[cfg_attr(test, derive(PartialEq))] -pub struct DepositCache { - logs: Vec, - leaves: Vec, - deposit_contract_deploy_block: u64, - finalized_deposit_count: u64, - finalized_block_height: u64, - /// An incremental merkle tree which represents the current state of the - /// deposit contract tree. - deposit_tree: DepositDataTree, - /// Vector of deposit roots. `deposit_roots[i]` denotes `deposit_root` at - /// `deposit_index` `i`. - deposit_roots: Vec, -} - -impl Default for DepositCache { - fn default() -> Self { - let deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); - let deposit_roots = vec![deposit_tree.root()]; - DepositCache { - logs: Vec::new(), - leaves: Vec::new(), - deposit_contract_deploy_block: 1, - finalized_deposit_count: 0, - finalized_block_height: 0, - deposit_tree, - deposit_roots, - } - } -} - -#[derive(Debug, PartialEq)] -pub enum DepositCacheInsertOutcome { - Inserted, - Duplicate, -} - -impl DepositCache { - /// Create new `DepositCache` given block number at which deposit - /// contract was deployed. - pub fn new(deposit_contract_deploy_block: u64) -> Self { - DepositCache { - deposit_contract_deploy_block, - finalized_block_height: deposit_contract_deploy_block.saturating_sub(1), - ..Self::default() - } - } - - pub fn from_deposit_snapshot( - deposit_contract_deploy_block: u64, - snapshot: &DepositTreeSnapshot, - ) -> Result { - let deposit_tree = DepositDataTree::from_snapshot(snapshot, DEPOSIT_TREE_DEPTH) - .map_err(|e| format!("Invalid DepositSnapshot: {:?}", e))?; - Ok(DepositCache { - logs: Vec::new(), - leaves: Vec::new(), - deposit_contract_deploy_block, - finalized_deposit_count: snapshot.deposit_count, - finalized_block_height: snapshot.execution_block_height, - deposit_tree, - deposit_roots: vec![snapshot.deposit_root], - }) - } - - /// Returns the number of deposits the cache stores - pub fn len(&self) -> usize { - self.finalized_deposit_count as usize + self.logs.len() - } - - /// True if the cache does not store any blocks. - pub fn is_empty(&self) -> bool { - self.finalized_deposit_count != 0 && self.logs.is_empty() - } - - /// Returns the block number for the most recent deposit in the cache. - pub fn latest_block_number(&self) -> u64 { - self.logs - .last() - .map(|log| log.block_number) - .unwrap_or(self.finalized_block_height) - } - - /// Returns an iterator over all the logs in `self` that aren't finalized. - pub fn iter(&self) -> impl Iterator { - self.logs.iter() - } - - /// Returns the deposit log with INDEX i. - pub fn get_log(&self, i: usize) -> Option<&DepositLog> { - let finalized_deposit_count = self.finalized_deposit_count as usize; - if i < finalized_deposit_count { - None - } else { - self.logs.get(i - finalized_deposit_count) - } - } - - /// Returns the deposit root with DEPOSIT COUNT (not index) i - pub fn get_root(&self, i: usize) -> Option<&Hash256> { - let finalized_deposit_count = self.finalized_deposit_count as usize; - if i < finalized_deposit_count { - None - } else { - self.deposit_roots.get(i - finalized_deposit_count) - } - } - - /// Returns the finalized deposit count - pub fn finalized_deposit_count(&self) -> u64 { - self.finalized_deposit_count - } - - /// Finalizes the cache up to `eth1_block.deposit_count`. - pub fn finalize(&mut self, eth1_block: Eth1Block) -> Result<(), Error> { - let deposits_to_finalize = eth1_block.deposit_count.ok_or_else(|| { - Error::Internal("Eth1Block did not contain deposit_count".to_string()) - })?; - - let currently_finalized = self.finalized_deposit_count; - if deposits_to_finalize > self.len() as u64 || deposits_to_finalize <= currently_finalized { - Err(Error::InvalidFinalizeIndex { - requested_count: deposits_to_finalize, - currently_finalized, - deposit_count: self.len() as u64, - }) - } else { - let finalized_log = self - .get_log((deposits_to_finalize - 1) as usize) - .cloned() - .ok_or(Error::PleaseNotifyTheDevs)?; - let drop = (deposits_to_finalize - currently_finalized) as usize; - self.deposit_tree - .finalize(eth1_block.into()) - .map_err(Error::DepositTree)?; - self.logs.drain(0..drop); - self.leaves.drain(0..drop); - self.deposit_roots.drain(0..drop); - self.finalized_deposit_count = deposits_to_finalize; - self.finalized_block_height = finalized_log.block_number; - - Ok(()) - } - } - - /// Returns the deposit tree snapshot (if tree is finalized) - pub fn get_deposit_snapshot(&self) -> Option { - self.deposit_tree.get_snapshot() - } - - /// Adds `log` to self. - /// - /// This function enforces that `logs` are imported one-by-one with no gaps between - /// `log.index`, starting at `log.index == 0`. - /// - /// ## Errors - /// - /// - If a log with index `log.index - 1` is not already present in `self` (ignored when empty). - /// - If a log with `log.index` is already known, but the given `log` is distinct to it. - pub fn insert_log(&mut self, log: DepositLog) -> Result { - match log.index.cmp(&(self.len() as u64)) { - Ordering::Equal => { - let deposit = log.deposit_data.tree_hash_root(); - // should push to deposit_tree first because it's fallible - self.deposit_tree - .push_leaf(deposit) - .map_err(Error::DepositTree)?; - self.leaves.push(deposit); - self.logs.push(log); - self.deposit_roots.push(self.deposit_tree.root()); - Ok(DepositCacheInsertOutcome::Inserted) - } - Ordering::Less => { - let mut compare_index = log.index as usize; - if log.index < self.finalized_deposit_count { - return Err(Error::FinalizedLogInsert { - log_index: log.index, - finalized_index: self.finalized_deposit_count - 1, - }); - } else { - compare_index -= self.finalized_deposit_count as usize; - } - if self.logs[compare_index] == log { - Ok(DepositCacheInsertOutcome::Duplicate) - } else { - Err(Error::DuplicateDistinctLog(log.index)) - } - } - Ordering::Greater => Err(Error::NonConsecutive { - log_index: log.index, - expected: self.logs.len(), - }), - } - } - - /// Returns a list of `Deposit` objects, within the given deposit index `range`. - /// - /// The `deposit_count` is used to generate the proofs for the `Deposits`. For example, if we - /// have 100 proofs, but the eth2 chain only acknowledges 50 of them, we must produce our - /// proofs with respect to a tree size of 50. - /// - /// - /// ## Errors - /// - /// - If `deposit_count` is less than `end`. - /// - There are not sufficient deposits in the tree to generate the proof. - pub fn get_deposits( - &self, - start: u64, - end: u64, - deposit_count: u64, - ) -> Result<(Hash256, Vec), Error> { - if deposit_count < end { - // It's invalid to ask for more deposits than should exist. - Err(Error::DepositCountInvalid { - deposit_count, - range_end: end, - }) - } else if end > self.len() as u64 { - // The range of requested deposits exceeds the deposits stored locally. - Err(Error::InsufficientDeposits { - requested: end, - known_deposits: self.logs.len(), - }) - } else if self.finalized_deposit_count > start { - // Can't ask for deposits before or on the finalized deposit - Err(Error::DepositRangeInvalid { - range_start: start, - finalized_count: self.finalized_deposit_count, - }) - } else { - let (start, end, deposit_count) = ( - start - self.finalized_deposit_count, - end - self.finalized_deposit_count, - deposit_count - self.finalized_deposit_count, - ); - let leaves = self - .leaves - .get(0..deposit_count as usize) - .ok_or_else(|| Error::Internal("Unable to get known leaves".into()))?; - - let tree = self - .deposit_tree - .get_snapshot() - .map(|snapshot| { - // The tree has already been finalized. So we can just start from the snapshot - // and replay the deposits up to `deposit_count` - let mut tree = DepositDataTree::from_snapshot(&snapshot, DEPOSIT_TREE_DEPTH) - .map_err(Error::DepositTree)?; - for leaf in leaves { - tree.push_leaf(*leaf).map_err(Error::DepositTree)?; - } - Ok(tree) - }) - .unwrap_or_else(|| { - // Deposit tree hasn't been finalized yet, will have to re-create the whole tree - Ok(DepositDataTree::create( - leaves, - leaves.len(), - DEPOSIT_TREE_DEPTH, - )) - })?; - - let mut deposits = vec![]; - self.logs - .get(start as usize..end as usize) - .ok_or_else(|| Error::Internal("Unable to get known log".into()))? - .iter() - .try_for_each(|deposit_log| { - let (_leaf, proof) = tree - .generate_proof(deposit_log.index as usize) - .map_err(Error::DepositTree)?; - deposits.push(Deposit { - proof: proof.into(), - data: deposit_log.deposit_data.clone(), - }); - Ok(()) - })?; - - Ok((tree.root(), deposits)) - } - } - - /// Returns the number of deposits with valid signatures that have been observed up to and - /// including the block at `block_number`. - /// - /// Returns `None` if the `block_number` is zero or prior to contract deployment. - pub fn get_valid_signature_count(&self, block_number: u64) -> Option { - if block_number == 0 || block_number < self.deposit_contract_deploy_block { - None - } else { - Some( - self.logs - .iter() - .take_while(|deposit| deposit.block_number <= block_number) - .filter(|deposit| deposit.signature_is_valid) - .count(), - ) - } - } - - /// Returns the number of deposits that have been observed up to and - /// including the block at `block_number`. - /// - /// Returns `None` if the `block_number` is zero or prior to contract deployment - /// or prior to last finalized deposit. - pub fn get_deposit_count_from_cache(&self, block_number: u64) -> Option { - if block_number == 0 - || block_number < self.deposit_contract_deploy_block - || block_number < self.finalized_block_height - { - None - } else if block_number == self.finalized_block_height { - Some(self.finalized_deposit_count) - } else { - Some( - self.finalized_deposit_count - + self - .logs - .iter() - .take_while(|deposit| deposit.block_number <= block_number) - .count() as u64, - ) - } - } - - /// Gets the deposit root at block height = block_number. - /// - /// Fetches the `deposit_count` on or just before the queried `block_number` - /// and queries the `deposit_roots` map to get the corresponding `deposit_root`. - pub fn get_deposit_root_from_cache(&self, block_number: u64) -> Option { - let count = self.get_deposit_count_from_cache(block_number)?; - self.get_root(count as usize).cloned() - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use execution_layer::http::deposit_log::Log; - use types::{EthSpec, FixedBytesExtended, MainnetEthSpec}; - - /// The data from a deposit event, using the v0.8.3 version of the deposit contract. - pub const EXAMPLE_LOG: &[u8] = &[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 1, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 167, 108, 6, 69, 88, 17, 3, 51, 6, 4, 158, 232, 82, - 248, 218, 2, 71, 219, 55, 102, 86, 125, 136, 203, 36, 77, 64, 213, 43, 52, 175, 154, 239, - 50, 142, 52, 201, 77, 54, 239, 0, 229, 22, 46, 139, 120, 62, 240, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 64, 89, 115, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 140, 74, 175, 158, 209, 20, 206, - 30, 63, 215, 238, 113, 60, 132, 216, 211, 100, 186, 202, 71, 34, 200, 160, 225, 212, 213, - 119, 88, 51, 80, 101, 74, 2, 45, 78, 153, 12, 192, 44, 51, 77, 40, 10, 72, 246, 34, 193, - 187, 22, 95, 4, 211, 245, 224, 13, 162, 21, 163, 54, 225, 22, 124, 3, 56, 14, 81, 122, 189, - 149, 250, 251, 159, 22, 77, 94, 157, 197, 196, 253, 110, 201, 88, 193, 246, 136, 226, 221, - 18, 113, 232, 105, 100, 114, 103, 237, 189, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; - - fn example_log() -> DepositLog { - let spec = MainnetEthSpec::default_spec(); - - let log = Log { - block_number: 42, - data: EXAMPLE_LOG.to_vec(), - }; - log.to_deposit_log(&spec).expect("should decode log") - } - - fn get_cache_with_deposits(n: u64) -> DepositCache { - let mut deposit_cache = DepositCache::default(); - for i in 0..n { - let mut log = example_log(); - log.index = i; - log.block_number = i; - log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i); - deposit_cache - .insert_log(log) - .expect("should add consecutive logs"); - } - assert_eq!(deposit_cache.len() as u64, n, "should have {} deposits", n); - - deposit_cache - } - - #[test] - fn insert_log_valid() { - let mut deposit_cache = DepositCache::default(); - - for i in 0..16 { - let mut log = example_log(); - log.index = i; - deposit_cache - .insert_log(log) - .expect("should add consecutive logs"); - } - } - - #[test] - fn insert_log_invalid() { - let mut deposit_cache = DepositCache::default(); - - for i in 0..4 { - let mut log = example_log(); - log.index = i; - deposit_cache - .insert_log(log) - .expect("should add consecutive logs"); - } - - // Add duplicate, when given is the same as the one known. - let mut log = example_log(); - log.index = 3; - assert_eq!( - deposit_cache.insert_log(log).unwrap(), - DepositCacheInsertOutcome::Duplicate - ); - - // Add duplicate, when given is different to the one known. - let mut log = example_log(); - log.index = 3; - log.block_number = 99; - assert!(deposit_cache.insert_log(log).is_err()); - - // Skip inserting a log. - let mut log = example_log(); - log.index = 5; - assert!(deposit_cache.insert_log(log).is_err()); - } - - #[test] - fn get_deposit_valid() { - let n = 1_024; - let deposit_cache = get_cache_with_deposits(n); - - // Get 0 deposits, with max deposit count. - let (_, deposits) = deposit_cache - .get_deposits(0, 0, n) - .expect("should get the full tree"); - assert_eq!(deposits.len(), 0, "should return no deposits"); - - // Get 0 deposits, with 0 deposit count. - let (_, deposits) = deposit_cache - .get_deposits(0, 0, 0) - .expect("should get the full tree"); - assert_eq!(deposits.len(), 0, "should return no deposits"); - - // Get all deposits, with max deposit count. - let (full_root, deposits) = deposit_cache - .get_deposits(0, n, n) - .expect("should get the full tree"); - assert_eq!(deposits.len(), n as usize, "should return all deposits"); - - // Get 4 deposits, with max deposit count. - let (root, deposits) = deposit_cache - .get_deposits(0, 4, n) - .expect("should get the four from the full tree"); - assert_eq!( - deposits.len(), - 4_usize, - "should get 4 deposits from full tree" - ); - assert_eq!( - root, full_root, - "should still return full root when getting deposit subset" - ); - - // Get half of the deposits, with half deposit count. - let half = n / 2; - let (half_root, deposits) = deposit_cache - .get_deposits(0, half, half) - .expect("should get the half tree"); - assert_eq!(deposits.len(), half as usize, "should return half deposits"); - - // Get 4 deposits, with half deposit count. - let (root, deposits) = deposit_cache - .get_deposits(0, 4, n / 2) - .expect("should get the half tree"); - assert_eq!( - deposits.len(), - 4_usize, - "should get 4 deposits from half tree" - ); - assert_eq!( - root, half_root, - "should still return half root when getting deposit subset" - ); - assert_ne!( - full_root, half_root, - "should get different root when pinning deposit count" - ); - } - - #[test] - fn get_deposit_invalid() { - let n = 16; - let mut tree = get_cache_with_deposits(n); - - // Range too high. - assert!(tree.get_deposits(0, n + 1, n).is_err()); - - // Count too high. - assert!(tree.get_deposits(0, n, n + 1).is_err()); - - // Range higher than count. - assert!(tree.get_deposits(0, 4, 2).is_err()); - - let block7 = fake_eth1_block(&tree, 7).expect("should create fake eth1 block"); - tree.finalize(block7).expect("should finalize"); - // Range starts <= finalized deposit - assert!(tree.get_deposits(6, 9, 11).is_err()); - assert!(tree.get_deposits(7, 9, 11).is_err()); - // Range start > finalized deposit should be OK - assert!(tree.get_deposits(8, 9, 11).is_ok()); - } - - // returns an eth1 block that can be used to finalize the cache at `deposit_index` - // this will ensure the `deposit_root` on the `Eth1Block` is correct - fn fake_eth1_block(deposit_cache: &DepositCache, deposit_index: usize) -> Option { - let deposit_log = deposit_cache.get_log(deposit_index)?; - Some(Eth1Block { - hash: Hash256::from_low_u64_be(deposit_log.block_number), - timestamp: 0, - number: deposit_log.block_number, - deposit_root: deposit_cache.get_root(deposit_index + 1).cloned(), - deposit_count: Some(deposit_log.index + 1), - }) - } - - #[test] - fn test_finalization_boundaries() { - let n = 8; - let half = n / 2; - - let mut deposit_cache = get_cache_with_deposits(n as u64); - - let full_root_before_finalization = deposit_cache.deposit_tree.root(); - let half_log_plus1_before_finalization = deposit_cache - .get_log(half + 1) - .expect("log should exist") - .clone(); - let half_root_plus1_before_finalization = - *deposit_cache.get_root(half + 1).expect("root should exist"); - - let (root_before_finalization, proof_before_finalization) = deposit_cache - .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) - .expect("should return 1 deposit with proof"); - - // finalize on the tree at half - let half_block = - fake_eth1_block(&deposit_cache, half).expect("fake block should be created"); - assert!( - deposit_cache.get_deposit_snapshot().is_none(), - "snapshot should not exist as tree has not been finalized" - ); - deposit_cache - .finalize(half_block) - .expect("tree should_finalize"); - - // check boundary conditions for get_log - assert!( - deposit_cache.get_log(half).is_none(), - "log at finalized deposit should NOT exist" - ); - assert_eq!( - *deposit_cache.get_log(half + 1).expect("log should exist"), - half_log_plus1_before_finalization, - "log after finalized deposit should match before finalization" - ); - // check boundary conditions for get_root - assert!( - deposit_cache.get_root(half).is_none(), - "root at finalized deposit should NOT exist" - ); - assert_eq!( - *deposit_cache.get_root(half + 1).expect("root should exist"), - half_root_plus1_before_finalization, - "root after finalized deposit should match before finalization" - ); - // full root should match before and after finalization - assert_eq!( - deposit_cache.deposit_tree.root(), - full_root_before_finalization, - "full root should match before and after finalization" - ); - // check boundary conditions for get_deposits (proof) - assert!( - deposit_cache - .get_deposits(half as u64, (half + 1) as u64, (half + 1) as u64) - .is_err(), - "cannot prove the finalized deposit" - ); - let (root_after_finalization, proof_after_finalization) = deposit_cache - .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) - .expect("should return 1 deposit with proof"); - assert_eq!( - root_before_finalization, root_after_finalization, - "roots before and after finalization should match" - ); - assert_eq!( - proof_before_finalization, proof_after_finalization, - "proof before and after finalization should match" - ); - - // recover tree from snapshot by replaying deposits - let snapshot = deposit_cache - .get_deposit_snapshot() - .expect("snapshot should exist"); - let mut recovered = DepositCache::from_deposit_snapshot(1, &snapshot) - .expect("should recover finalized tree"); - for i in half + 1..n { - let mut log = example_log(); - log.index = i as u64; - log.block_number = i as u64; - log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i as u64); - recovered - .insert_log(log) - .expect("should add consecutive logs"); - } - - // check the same boundary conditions above for the recovered tree - assert!( - recovered.get_log(half).is_none(), - "log at finalized deposit should NOT exist" - ); - assert_eq!( - *recovered.get_log(half + 1).expect("log should exist"), - half_log_plus1_before_finalization, - "log after finalized deposit should match before finalization in recovered tree" - ); - // check boundary conditions for get_root - assert!( - recovered.get_root(half).is_none(), - "root at finalized deposit should NOT exist" - ); - assert_eq!( - *recovered.get_root(half + 1).expect("root should exist"), - half_root_plus1_before_finalization, - "root after finalized deposit should match before finalization in recovered tree" - ); - // full root should match before and after finalization - assert_eq!( - recovered.deposit_tree.root(), - full_root_before_finalization, - "full root should match before and after finalization" - ); - // check boundary conditions for get_deposits (proof) - assert!( - recovered - .get_deposits(half as u64, (half + 1) as u64, (half + 1) as u64) - .is_err(), - "cannot prove the finalized deposit" - ); - let (recovered_root_after_finalization, recovered_proof_after_finalization) = recovered - .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) - .expect("should return 1 deposit with proof"); - assert_eq!( - root_before_finalization, recovered_root_after_finalization, - "recovered roots before and after finalization should match" - ); - assert_eq!( - proof_before_finalization, recovered_proof_after_finalization, - "recovered proof before and after finalization should match" - ); - } - - #[test] - fn test_finalization() { - let n = 1024; - let half = n / 2; - let quarter = half / 2; - let mut deposit_cache = get_cache_with_deposits(n); - - let full_root_before_finalization = deposit_cache.deposit_tree.root(); - let q3_root_before_finalization = deposit_cache - .get_root((half + quarter) as usize) - .cloned() - .expect("root should exist"); - let q3_log_before_finalization = deposit_cache - .get_log((half + quarter) as usize) - .cloned() - .expect("log should exist"); - // get_log(half+quarter) should return log with index `half+quarter` - assert_eq!( - q3_log_before_finalization.index, - half + quarter, - "log index should be {}", - half + quarter, - ); - - // get lower quarter of deposits with max deposit count - let (lower_quarter_root_before_finalization, lower_quarter_deposits_before_finalization) = - deposit_cache - .get_deposits(quarter, half, n) - .expect("should get lower quarter"); - assert_eq!( - lower_quarter_deposits_before_finalization.len(), - quarter as usize, - "should get {} deposits from lower quarter", - quarter, - ); - // since the lower quarter was done with full deposits, root should be the same as full_root_before_finalization - assert_eq!( - lower_quarter_root_before_finalization, full_root_before_finalization, - "should still get full root with deposit subset", - ); - - // get upper quarter of deposits with slightly reduced deposit count - let (upper_quarter_root_before_finalization, upper_quarter_deposits_before_finalization) = - deposit_cache - .get_deposits(half, half + quarter, n - 2) - .expect("should get upper quarter"); - assert_eq!( - upper_quarter_deposits_before_finalization.len(), - quarter as usize, - "should get {} deposits from upper quarter", - quarter, - ); - // since upper quarter was with subset of nodes, it should differ from full root - assert_ne!( - full_root_before_finalization, upper_quarter_root_before_finalization, - "subtree root should differ from full root", - ); - - let f0_log = deposit_cache - .get_log((quarter - 1) as usize) - .cloned() - .expect("should return log"); - let f0_block = fake_eth1_block(&deposit_cache, (quarter - 1) as usize) - .expect("fake eth1 block should be created"); - - // finalize first quarter - deposit_cache - .finalize(f0_block) - .expect("should finalize first quarter"); - // finalized count and block number should match log - assert_eq!( - deposit_cache.finalized_deposit_count, - f0_log.index + 1, - "after calling finalize(eth1block) finalized_deposit_count should equal eth1_block.deposit_count", - ); - assert_eq!( - deposit_cache.finalized_block_height, - f0_log.block_number, - "after calling finalize(eth1block) finalized_block_number should equal eth1block.block_number" - ); - // check get_log boundaries - assert!( - deposit_cache.get_log((quarter - 1) as usize).is_none(), - "get_log() should return None for index <= finalized log index", - ); - assert!( - deposit_cache.get_log(quarter as usize).is_some(), - "get_log() should return Some(log) for index >= finalized_deposit_count", - ); - - // full root should remain the same after finalization - assert_eq!( - full_root_before_finalization, - deposit_cache.deposit_tree.root(), - "root should be the same before and after finalization", - ); - // get_root should return the same root before and after finalization - assert_eq!( - q3_root_before_finalization, - deposit_cache - .get_root((half + quarter) as usize) - .cloned() - .expect("root should exist"), - "get_root should return the same root before and after finalization", - ); - // get_log should return the same log before and after finalization - assert_eq!( - q3_log_before_finalization, - deposit_cache - .get_log((half + quarter) as usize) - .cloned() - .expect("log should exist"), - "get_log should return the same log before and after finalization", - ); - - // again get lower quarter of deposits with max deposit count after finalization - let (f0_lower_quarter_root, f0_lower_quarter_deposits) = deposit_cache - .get_deposits(quarter, half, n) - .expect("should get lower quarter"); - assert_eq!( - f0_lower_quarter_deposits.len(), - quarter as usize, - "should get {} deposits from lower quarter", - quarter, - ); - // again get upper quarter of deposits with slightly reduced deposit count after finalization - let (f0_upper_quarter_root, f0_upper_quarter_deposits) = deposit_cache - .get_deposits(half, half + quarter, n - 2) - .expect("should get upper quarter"); - assert_eq!( - f0_upper_quarter_deposits.len(), - quarter as usize, - "should get {} deposits from upper quarter", - quarter, - ); - - // lower quarter root and deposits should be the same - assert_eq!( - lower_quarter_root_before_finalization, f0_lower_quarter_root, - "root should be the same before and after finalization", - ); - for i in 0..lower_quarter_deposits_before_finalization.len() { - assert_eq!( - lower_quarter_deposits_before_finalization[i], f0_lower_quarter_deposits[i], - "get_deposits() should be the same before and after finalization", - ); - } - // upper quarter root and deposits should be the same - assert_eq!( - upper_quarter_root_before_finalization, f0_upper_quarter_root, - "subtree root should be the same before and after finalization", - ); - for i in 0..upper_quarter_deposits_before_finalization.len() { - assert_eq!( - upper_quarter_deposits_before_finalization[i], f0_upper_quarter_deposits[i], - "get_deposits() should be the same before and after finalization", - ); - } - - let f1_log = deposit_cache - .get_log((half - 2) as usize) - .cloned() - .expect("should return log"); - // finalize a little less than half to test multiple finalization - let f1_block = fake_eth1_block(&deposit_cache, (half - 2) as usize) - .expect("should create fake eth1 block"); - deposit_cache - .finalize(f1_block) - .expect("should finalize a little less than half"); - // finalized count and block number should match f1_log - assert_eq!( - deposit_cache.finalized_deposit_count, - f1_log.index + 1, - "after calling finalize(eth1block) finalized_deposit_count should equal eth1_block.deposit_count", - ); - assert_eq!( - deposit_cache.finalized_block_height, - f1_log.block_number, - "after calling finalize(eth1block) finalized_block_number should equal eth1block.block_number" - ); - // check get_log boundaries - assert!( - deposit_cache.get_log((half - 2) as usize).is_none(), - "get_log() should return None for index <= finalized log index", - ); - assert!( - deposit_cache.get_log((half - 1) as usize).is_some(), - "get_log() should return Some(log) for index >= finalized_deposit_count", - ); - - // full root should still be unchanged - assert_eq!( - full_root_before_finalization, - deposit_cache.deposit_tree.root(), - "root should be the same before and after finalization", - ); - - // again get upper quarter of deposits with slightly reduced deposit count after second finalization - let (f1_upper_quarter_root, f1_upper_quarter_deposits) = deposit_cache - .get_deposits(half, half + quarter, n - 2) - .expect("should get upper quarter"); - - // upper quarter root and deposits should be the same after second finalization - assert_eq!( - f0_upper_quarter_root, f1_upper_quarter_root, - "subtree root should be the same after multiple finalization", - ); - for i in 0..f0_upper_quarter_deposits.len() { - assert_eq!( - f0_upper_quarter_deposits[i], f1_upper_quarter_deposits[i], - "get_deposits() should be the same before and after finalization", - ); - } - } - - fn verify_equality(original: &DepositCache, copy: &DepositCache) { - // verify each field individually so that if one field should - // fail to recover, this test will point right to it - assert_eq!(original.deposit_contract_deploy_block, copy.deposit_contract_deploy_block, "DepositCache: deposit_contract_deploy_block should remain the same after encoding and decoding from ssz" ); - assert_eq!( - original.leaves, copy.leaves, - "DepositCache: leaves should remain the same after encoding and decoding from ssz" - ); - assert_eq!( - original.logs, copy.logs, - "DepositCache: logs should remain the same after encoding and decoding from ssz" - ); - assert_eq!(original.finalized_deposit_count, copy.finalized_deposit_count, "DepositCache: finalized_deposit_count should remain the same after encoding and decoding from ssz"); - assert_eq!(original.finalized_block_height, copy.finalized_block_height, "DepositCache: finalized_block_height should remain the same after encoding and decoding from ssz"); - assert_eq!(original.deposit_roots, copy.deposit_roots, "DepositCache: deposit_roots should remain the same before and after encoding and decoding from ssz"); - assert!(original.deposit_tree == copy.deposit_tree, "DepositCache: deposit_tree should remain the same before and after encoding and decoding from ssz"); - // verify all together for good measure - assert!( - original == copy, - "Deposit cache should remain the same after encoding and decoding from ssz" - ); - } - - fn ssz_round_trip(original: &DepositCache) -> DepositCache { - use ssz::{Decode, Encode}; - let bytes = SszDepositCache::from_deposit_cache(original).as_ssz_bytes(); - let ssz_cache = - SszDepositCache::from_ssz_bytes(&bytes).expect("should decode from ssz bytes"); - - SszDepositCache::to_deposit_cache(&ssz_cache).expect("should recover cache") - } - - #[test] - fn ssz_encode_decode() { - let deposit_cache = get_cache_with_deposits(512); - let recovered_cache = ssz_round_trip(&deposit_cache); - - verify_equality(&deposit_cache, &recovered_cache); - } - - #[test] - fn ssz_encode_decode_with_finalization() { - let mut deposit_cache = get_cache_with_deposits(512); - let block383 = fake_eth1_block(&deposit_cache, 383).expect("should create fake eth1 block"); - deposit_cache.finalize(block383).expect("should finalize"); - let mut first_recovery = ssz_round_trip(&deposit_cache); - - verify_equality(&deposit_cache, &first_recovery); - // finalize again to verify equality after multiple finalizations - let block447 = fake_eth1_block(&deposit_cache, 447).expect("should create fake eth1 block"); - first_recovery.finalize(block447).expect("should finalize"); - - let mut second_recovery = ssz_round_trip(&first_recovery); - verify_equality(&first_recovery, &second_recovery); - - // verify equality of a tree that finalized block383, block447, block479 - // with a tree that finalized block383, block479 - let block479 = fake_eth1_block(&deposit_cache, 479).expect("should create fake eth1 block"); - second_recovery - .finalize(block479.clone()) - .expect("should finalize"); - let third_recovery = ssz_round_trip(&second_recovery); - deposit_cache.finalize(block479).expect("should finalize"); - - verify_equality(&deposit_cache, &third_recovery); - } -} diff --git a/beacon_node/eth1/src/inner.rs b/beacon_node/eth1/src/inner.rs deleted file mode 100644 index 1f45346256..0000000000 --- a/beacon_node/eth1/src/inner.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::service::endpoint_from_config; -use crate::Config; -use crate::{ - block_cache::{BlockCache, Eth1Block}, - deposit_cache::{DepositCache, SszDepositCache, SszDepositCacheV13}, -}; -use execution_layer::HttpJsonRpc; -use parking_lot::RwLock; -use ssz::four_byte_option_impl; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use std::sync::Arc; -use superstruct::superstruct; -use types::{ChainSpec, DepositTreeSnapshot, Eth1Data}; - -// Define "legacy" implementations of `Option` which use four bytes for encoding the union -// selector. -four_byte_option_impl!(four_byte_option_u64, u64); - -#[derive(Default)] -pub struct DepositUpdater { - pub cache: DepositCache, - pub last_processed_block: Option, -} - -impl DepositUpdater { - pub fn new(deposit_contract_deploy_block: u64) -> Self { - let cache = DepositCache::new(deposit_contract_deploy_block); - DepositUpdater { - cache, - last_processed_block: None, - } - } - - pub fn from_snapshot( - deposit_contract_deploy_block: u64, - snapshot: &DepositTreeSnapshot, - ) -> Result { - let last_processed_block = Some(snapshot.execution_block_height); - Ok(Self { - cache: DepositCache::from_deposit_snapshot(deposit_contract_deploy_block, snapshot)?, - last_processed_block, - }) - } -} - -pub struct Inner { - pub block_cache: RwLock, - pub deposit_cache: RwLock, - pub endpoint: HttpJsonRpc, - // this gets set to Some(Eth1Data) when the deposit finalization conditions are met - pub to_finalize: RwLock>, - pub config: RwLock, - pub remote_head_block: RwLock>, - pub spec: Arc, -} - -impl Inner { - /// Prunes the block cache to `self.target_block_cache_len`. - /// - /// Is a no-op if `self.target_block_cache_len` is `None`. - pub fn prune_blocks(&self) { - if let Some(block_cache_truncation) = self.config.read().block_cache_truncation { - self.block_cache.write().truncate(block_cache_truncation); - } - } - - /// Encode the eth1 block and deposit cache as bytes. - pub fn as_bytes(&self) -> Vec { - let ssz_eth1_cache = SszEth1Cache::from_inner(self); - ssz_eth1_cache.as_ssz_bytes() - } - - /// Recover `Inner` given byte representation of eth1 deposit and block caches. - pub fn from_bytes(bytes: &[u8], config: Config, spec: Arc) -> Result { - SszEth1Cache::from_ssz_bytes(bytes) - .map_err(|e| format!("Ssz decoding error: {:?}", e))? - .to_inner(config, spec) - .inspect(|inner| inner.block_cache.write().rebuild_by_hash_map()) - } - - /// Returns a reference to the specification. - pub fn spec(&self) -> &ChainSpec { - &self.spec - } -} - -pub type SszEth1Cache = SszEth1CacheV13; - -#[superstruct( - variants(V13), - variant_attributes(derive(Encode, Decode, Clone)), - no_enum -)] -pub struct SszEth1Cache { - pub block_cache: BlockCache, - pub deposit_cache: SszDepositCacheV13, - #[ssz(with = "four_byte_option_u64")] - pub last_processed_block: Option, -} - -impl SszEth1Cache { - pub fn from_inner(inner: &Inner) -> Self { - let deposit_updater = inner.deposit_cache.read(); - let block_cache = inner.block_cache.read(); - Self { - block_cache: (*block_cache).clone(), - deposit_cache: SszDepositCache::from_deposit_cache(&deposit_updater.cache), - last_processed_block: deposit_updater.last_processed_block, - } - } - - pub fn to_inner(&self, config: Config, spec: Arc) -> Result { - Ok(Inner { - block_cache: RwLock::new(self.block_cache.clone()), - deposit_cache: RwLock::new(DepositUpdater { - cache: self.deposit_cache.to_deposit_cache()?, - last_processed_block: self.last_processed_block, - }), - endpoint: endpoint_from_config(&config) - .map_err(|e| format!("Failed to create endpoint: {:?}", e))?, - to_finalize: RwLock::new(None), - // Set the remote head_block zero when creating a new instance. We only care about - // present and future eth1 nodes. - remote_head_block: RwLock::new(None), - config: RwLock::new(config), - spec, - }) - } -} diff --git a/beacon_node/eth1/src/lib.rs b/beacon_node/eth1/src/lib.rs deleted file mode 100644 index 9c4f9a1d8d..0000000000 --- a/beacon_node/eth1/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod block_cache; -mod deposit_cache; -mod inner; -mod metrics; -mod service; - -pub use block_cache::{BlockCache, Eth1Block}; -pub use deposit_cache::{DepositCache, SszDepositCache, SszDepositCacheV13}; -pub use execution_layer::http::deposit_log::DepositLog; -pub use inner::{SszEth1Cache, SszEth1CacheV13}; -pub use service::{ - BlockCacheUpdateOutcome, Config, DepositCacheUpdateOutcome, Error, Eth1Endpoint, Service, - DEFAULT_CHAIN_ID, -}; diff --git a/beacon_node/eth1/src/metrics.rs b/beacon_node/eth1/src/metrics.rs deleted file mode 100644 index 1df4ba0df9..0000000000 --- a/beacon_node/eth1/src/metrics.rs +++ /dev/null @@ -1,41 +0,0 @@ -pub use metrics::*; -use std::sync::LazyLock; - -/* - * Eth1 blocks - */ -pub static BLOCK_CACHE_LEN: LazyLock> = - LazyLock::new(|| try_create_int_gauge("eth1_block_cache_len", "Count of eth1 blocks in cache")); -pub static LATEST_CACHED_BLOCK_TIMESTAMP: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "eth1_latest_cached_block_timestamp", - "Timestamp of latest block in eth1 cache", - ) -}); - -/* - * Eth1 deposits - */ -pub static DEPOSIT_CACHE_LEN: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "eth1_deposit_cache_len", - "Number of deposits in the eth1 cache", - ) -}); -pub static HIGHEST_PROCESSED_DEPOSIT_BLOCK: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "eth1_highest_processed_deposit_block", - "Number of the last block checked for deposits", - ) -}); - -/* - * Eth1 rpc connection - */ - -pub static ETH1_CONNECTED: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "sync_eth1_connected", - "Set to 1 if connected to an eth1 node, otherwise set to 0", - ) -}); diff --git a/beacon_node/eth1/src/service.rs b/beacon_node/eth1/src/service.rs deleted file mode 100644 index 6b10bd2215..0000000000 --- a/beacon_node/eth1/src/service.rs +++ /dev/null @@ -1,1243 +0,0 @@ -use crate::metrics; -use crate::{ - block_cache::{BlockCache, Error as BlockCacheError, Eth1Block}, - deposit_cache::{DepositCacheInsertOutcome, Error as DepositCacheError}, - inner::{DepositUpdater, Inner}, -}; -use execution_layer::auth::Auth; -use execution_layer::http::{ - deposit_methods::{BlockQuery, Eth1Id}, - HttpJsonRpc, -}; -use futures::future::TryFutureExt; -use parking_lot::{RwLock, RwLockReadGuard}; -use sensitive_url::SensitiveUrl; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::ops::{Range, RangeInclusive}; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::time::{interval_at, Duration, Instant}; -use tracing::{debug, error, info, trace, warn}; -use types::{ChainSpec, DepositTreeSnapshot, Eth1Data, EthSpec, Unsigned}; - -/// Indicates the default eth1 chain id we use for the deposit contract. -pub const DEFAULT_CHAIN_ID: Eth1Id = Eth1Id::Mainnet; -/// Indicates the default eth1 endpoint. -pub const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545"; - -const STANDARD_TIMEOUT_MILLIS: u64 = 15_000; - -/// Timeout when doing a eth_blockNumber call. -const BLOCK_NUMBER_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS; -/// Timeout when doing an eth_getBlockByNumber call. -const GET_BLOCK_TIMEOUT_MILLIS: u64 = STANDARD_TIMEOUT_MILLIS; -/// Timeout when doing an eth_getLogs to read the deposit contract logs. -const GET_DEPOSIT_LOG_TIMEOUT_MILLIS: u64 = 60_000; - -/// Number of blocks to download if the node detects it is lagging behind due to an inaccurate -/// relationship between block-number-based follow distance and time-based follow distance. -const CATCHUP_BATCH_SIZE: u64 = 128; - -/// The absolute minimum follow distance to enforce when downloading catchup batches. -const CATCHUP_MIN_FOLLOW_DISTANCE: u64 = 64; - -/// To account for fast PoW blocks requiring more blocks in the cache than the block-based follow -/// distance would imply, we store `CACHE_FACTOR` more blocks in our cache. -const CACHE_FACTOR: u64 = 2; - -#[derive(Debug, PartialEq, Clone)] -pub enum EndpointError { - RequestFailed(String), - WrongChainId, - FarBehind, -} - -type EndpointState = Result<(), EndpointError>; - -/// Returns `Ok` if the endpoint is usable, i.e. is reachable and has a correct network id and -/// chain id. Otherwise it returns `Err`. -async fn endpoint_state(endpoint: &HttpJsonRpc, config_chain_id: &Eth1Id) -> EndpointState { - let error_connecting = |e: String| { - debug!( - %endpoint, - error = &e, - "eth1 endpoint error" - ); - warn!( - %endpoint, - "Error connecting to eth1 node endpoint" - ); - EndpointError::RequestFailed(e) - }; - - let chain_id = endpoint - .get_chain_id(Duration::from_millis(STANDARD_TIMEOUT_MILLIS)) - .await - .map_err(error_connecting)?; - // Eth1 nodes return chain_id = 0 if the node is not synced - // Handle the special case - if chain_id == Eth1Id::Custom(0) { - warn!( - %endpoint, - "Remote execution node is not synced" - ); - return Err(EndpointError::FarBehind); - } - if &chain_id != config_chain_id { - warn!( - %endpoint, - expected = ?config_chain_id, - received = ?chain_id, - "Invalid execution chain ID. Please switch to correct chain ID on endpoint" - ); - Err(EndpointError::WrongChainId) - } else { - Ok(()) - } -} - -/// Enum for the two internal (maybe different) cached heads for cached deposits and for the block -/// cache. -pub enum HeadType { - Deposit, - BlockCache, -} - -/// Returns the head block and the new block ranges relevant for deposits and the block cache -/// from the given endpoint. -async fn get_remote_head_and_new_block_ranges( - endpoint: &HttpJsonRpc, - service: &Service, - node_far_behind_seconds: u64, -) -> Result< - ( - Eth1Block, - Option>, - Option>, - ), - Error, -> { - let remote_head_block = download_eth1_block(endpoint, service.inner.clone(), None).await?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(u64::MAX); - if remote_head_block.timestamp + node_far_behind_seconds < now { - warn!( - %endpoint, - last_seen_block_unix_timestamp = remote_head_block.timestamp, - "Execution endpoint is not synced" - ); - return Err(Error::EndpointError(EndpointError::FarBehind)); - } - - let handle_remote_not_synced = |e| { - if let Error::RemoteNotSynced { .. } = e { - warn!( - %endpoint, - "Execution endpoint is not synced" - ); - } - e - }; - let new_deposit_block_numbers = service - .relevant_new_block_numbers( - remote_head_block.number, - Some(remote_head_block.timestamp), - HeadType::Deposit, - ) - .map_err(handle_remote_not_synced)?; - let new_block_cache_numbers = service - .relevant_new_block_numbers( - remote_head_block.number, - Some(remote_head_block.timestamp), - HeadType::BlockCache, - ) - .map_err(handle_remote_not_synced)?; - Ok(( - remote_head_block, - new_deposit_block_numbers, - new_block_cache_numbers, - )) -} - -/// Returns the range of new block numbers to be considered for the given head type from the given -/// endpoint. -async fn relevant_new_block_numbers_from_endpoint( - endpoint: &HttpJsonRpc, - service: &Service, - head_type: HeadType, -) -> Result>, Error> { - let remote_highest_block = endpoint - .get_block_number(Duration::from_millis(BLOCK_NUMBER_TIMEOUT_MILLIS)) - .map_err(Error::GetBlockNumberFailed) - .await?; - service.relevant_new_block_numbers(remote_highest_block, None, head_type) -} - -#[derive(Debug, PartialEq)] -pub enum Error { - /// There was an inconsistency when adding a block to the cache. - FailedToInsertEth1Block(BlockCacheError), - /// There was an inconsistency when adding a deposit to the cache. - FailedToInsertDeposit(DepositCacheError), - /// A log downloaded from the eth1 contract was not well formed. - FailedToParseDepositLog { - block_range: Range, - error: String, - }, - /// Endpoint is currently not functional. - EndpointError(EndpointError), - /// The remote node is less synced that we expect, it is not useful until has done more - /// syncing. - RemoteNotSynced { - next_required_block: u64, - remote_highest_block: u64, - cache_follow_distance: u64, - }, - /// Failed to download a block from the eth1 node. - BlockDownloadFailed(String), - /// Failed to get the current block number from the eth1 node. - GetBlockNumberFailed(String), - /// Failed to read the deposit contract root from the eth1 node. - GetDepositRootFailed(String), - /// Failed to read the deposit contract deposit count from the eth1 node. - GetDepositCountFailed(String), - /// Failed to read the deposit contract root from the eth1 node. - GetDepositLogsFailed(String), - /// There was an unexpected internal error. - Internal(String), - /// Error finalizing deposit - FailedToFinalizeDeposit(String), - /// There was a problem Initializing from deposit snapshot - FailedToInitializeFromSnapshot(String), -} - -/// The success message for an Eth1Data cache update. -#[derive(Debug, PartialEq, Clone)] -pub struct BlockCacheUpdateOutcome { - pub blocks_imported: usize, - pub head_block_number: Option, -} - -/// The success message for an Eth1 deposit cache update. -#[derive(Debug, PartialEq, Clone)] -pub struct DepositCacheUpdateOutcome { - pub logs_imported: usize, -} - -/// Supports either one authenticated jwt JSON-RPC endpoint **or** -/// multiple non-authenticated endpoints with fallback. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Eth1Endpoint { - Auth { - endpoint: SensitiveUrl, - jwt_path: PathBuf, - jwt_id: Option, - jwt_version: Option, - }, - NoAuth(SensitiveUrl), -} - -impl Eth1Endpoint { - pub fn get_endpoint(&self) -> SensitiveUrl { - match &self { - Self::Auth { endpoint, .. } => endpoint.clone(), - Self::NoAuth(endpoint) => endpoint.clone(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - /// An Eth1 node (e.g., Geth) running a HTTP JSON-RPC endpoint. - pub endpoint: Eth1Endpoint, - /// The address the `BlockCache` and `DepositCache` should assume is the canonical deposit contract. - pub deposit_contract_address: String, - /// The eth1 chain id where the deposit contract is deployed (Holesky/Mainnet). - pub chain_id: Eth1Id, - /// Defines the first block that the `DepositCache` will start searching for deposit logs. - /// - /// Setting too high can result in missed logs. Setting too low will result in unnecessary - /// calls to the Eth1 node's HTTP JSON RPC. - pub deposit_contract_deploy_block: u64, - /// Defines the lowest block number that should be downloaded and added to the `BlockCache`. - pub lowest_cached_block_number: u64, - /// Defines how far behind the Eth1 node's head we should follow. - /// - /// Note: this should be less than or equal to the specification's `ETH1_FOLLOW_DISTANCE`. - pub follow_distance: u64, - /// The follow distance to use for blocks in our cache. - /// - /// This can be set lower than the true follow distance in order to correct for poor timing - /// of eth1 blocks. - pub cache_follow_distance: Option, - /// Specifies the seconds when we consider the head of a node far behind. - /// This should be less than `ETH1_FOLLOW_DISTANCE * SECONDS_PER_ETH1_BLOCK`. - pub node_far_behind_seconds: u64, - /// Defines the number of blocks that should be retained each time the `BlockCache` calls truncate on - /// itself. - pub block_cache_truncation: Option, - /// The interval between updates when using the `auto_update` function. - pub auto_update_interval_millis: u64, - /// The span of blocks we should query for logs, per request. - pub blocks_per_log_query: usize, - /// The maximum number of log requests per update. - pub max_log_requests_per_update: Option, - /// The maximum number of log requests per update. - pub max_blocks_per_update: Option, - /// If set to true, the eth1 caches are wiped clean when the eth1 service starts. - pub purge_cache: bool, - pub execution_timeout_multiplier: u32, -} - -impl Config { - /// Sets the block cache to a length that is suitable for the given `EthSpec` and `ChainSpec`. - pub fn set_block_cache_truncation(&mut self, spec: &ChainSpec) { - // Compute the number of eth1 blocks in an eth1 voting period. - let seconds_per_voting_period = - E::SlotsPerEth1VotingPeriod::to_u64() * spec.seconds_per_slot; - let eth1_blocks_per_voting_period = seconds_per_voting_period / spec.seconds_per_eth1_block; - - // Ensure we can store two full windows of voting blocks. - let voting_windows = eth1_blocks_per_voting_period * 2; - - // Extend the cache to account for the cache follow distance. - let extra_follow_distance_blocks = self - .follow_distance - .saturating_sub(self.cache_follow_distance()); - - let length = voting_windows + extra_follow_distance_blocks; - - // Allow for more blocks to account for blocks being generated faster than expected. - // The cache expiry should really be timestamp based, but that would require a more - // extensive refactor. - let cache_size = CACHE_FACTOR * length; - - self.block_cache_truncation = Some(cache_size as usize); - } - - /// The distance at which the cache should follow the head. - /// - /// Defaults to 3/4 of `follow_distance` unless set manually. - pub fn cache_follow_distance(&self) -> u64 { - self.cache_follow_distance - .unwrap_or(3 * self.follow_distance / 4) - } -} - -impl Default for Config { - fn default() -> Self { - Self { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(DEFAULT_ETH1_ENDPOINT) - .expect("The default Eth1 endpoint must always be a valid URL."), - ), - deposit_contract_address: "0x0000000000000000000000000000000000000000".into(), - chain_id: DEFAULT_CHAIN_ID, - deposit_contract_deploy_block: 1, - lowest_cached_block_number: 1, - follow_distance: 128, - cache_follow_distance: None, - node_far_behind_seconds: 128 * 14, - block_cache_truncation: Some(4_096), - auto_update_interval_millis: 60_000, - blocks_per_log_query: 1_000, - max_log_requests_per_update: Some(5_000), - max_blocks_per_update: Some(8_192), - purge_cache: false, - execution_timeout_multiplier: 1, - } - } -} - -pub fn endpoint_from_config(config: &Config) -> Result { - match config.endpoint.clone() { - Eth1Endpoint::Auth { - endpoint, - jwt_path, - jwt_id, - jwt_version, - } => { - let auth = Auth::new_with_path(jwt_path, jwt_id, jwt_version) - .map_err(|e| format!("Failed to initialize jwt auth: {:?}", e))?; - HttpJsonRpc::new_with_auth(endpoint, auth, Some(config.execution_timeout_multiplier)) - .map_err(|e| format!("Failed to create eth1 json rpc client: {:?}", e)) - } - Eth1Endpoint::NoAuth(endpoint) => { - HttpJsonRpc::new(endpoint, Some(config.execution_timeout_multiplier)) - .map_err(|e| format!("Failed to create eth1 json rpc client: {:?}", e)) - } - } -} - -/// Provides a set of Eth1 caches and async functions to update them. -/// -/// Stores the following caches: -/// -/// - Deposit cache: stores all deposit logs from the deposit contract. -/// - Block cache: stores some number of eth1 blocks. -#[derive(Clone)] -pub struct Service { - inner: Arc, -} - -impl Service { - /// Creates a new service. Does not attempt to connect to the eth1 node. - pub fn new(config: Config, spec: Arc) -> Result { - Ok(Self { - inner: Arc::new(Inner { - block_cache: <_>::default(), - deposit_cache: RwLock::new(DepositUpdater::new( - config.deposit_contract_deploy_block, - )), - endpoint: endpoint_from_config(&config)?, - to_finalize: RwLock::new(None), - remote_head_block: RwLock::new(None), - config: RwLock::new(config), - spec, - }), - }) - } - - pub fn chain_spec(&self) -> &Arc { - &self.inner.spec - } - - pub fn client(&self) -> &HttpJsonRpc { - &self.inner.endpoint - } - - /// Creates a new service, initializing the deposit tree from a snapshot. - pub fn from_deposit_snapshot( - config: Config, - spec: Arc, - deposit_snapshot: &DepositTreeSnapshot, - ) -> Result { - let deposit_cache = - DepositUpdater::from_snapshot(config.deposit_contract_deploy_block, deposit_snapshot) - .map_err(Error::FailedToInitializeFromSnapshot)?; - - Ok(Self { - inner: Arc::new(Inner { - block_cache: <_>::default(), - deposit_cache: RwLock::new(deposit_cache), - endpoint: endpoint_from_config(&config) - .map_err(Error::FailedToInitializeFromSnapshot)?, - to_finalize: RwLock::new(None), - remote_head_block: RwLock::new(None), - config: RwLock::new(config), - spec, - }), - }) - } - - pub fn set_to_finalize(&self, eth1_data: Option) { - *(self.inner.to_finalize.write()) = eth1_data; - } - - /// Returns the follow distance that has been shortened to accommodate for differences in the - /// spacing between blocks. - pub fn cache_follow_distance(&self) -> u64 { - self.config().cache_follow_distance() - } - - /// Return byte representation of deposit and block caches. - pub fn as_bytes(&self) -> Vec { - self.inner.as_bytes() - } - - /// Recover the deposit and block caches from encoded bytes. - pub fn from_bytes(bytes: &[u8], config: Config, spec: Arc) -> Result { - let inner = Inner::from_bytes(bytes, config, spec)?; - Ok(Self { - inner: Arc::new(inner), - }) - } - - /// Provides access to the block cache. - pub fn blocks(&self) -> &RwLock { - &self.inner.block_cache - } - - /// Provides access to the deposit cache. - pub fn deposits(&self) -> &RwLock { - &self.inner.deposit_cache - } - - /// Removes all blocks from the cache, except for the latest block. - /// - /// We don't remove the latest blocks so we don't lose track of the latest block. - pub fn clear_block_cache(&self) { - self.inner.block_cache.write().truncate(1) - } - - /// Drop the block cache, replacing it with an empty one. - pub fn drop_block_cache(&self) { - *(self.inner.block_cache.write()) = BlockCache::default(); - } - - /// Returns the timestamp of the earliest block in the cache (if any). - pub fn earliest_block_timestamp(&self) -> Option { - self.inner.block_cache.read().earliest_block_timestamp() - } - - /// Returns the timestamp of the latest block in the cache (if any). - pub fn latest_block_timestamp(&self) -> Option { - self.inner.block_cache.read().latest_block_timestamp() - } - - /// Returns the latest head block returned from an Eth1 node. - /// - /// ## Note - /// - /// This is the simply the head of the Eth1 chain, with no regard to follow distance or the - /// voting period start. - pub fn head_block(&self) -> Option { - self.inner.remote_head_block.read().as_ref().cloned() - } - - /// Returns the latest cached block. - pub fn latest_cached_block(&self) -> Option { - self.inner.block_cache.read().latest_block().cloned() - } - - /// Returns the lowest block number stored. - pub fn lowest_block_number(&self) -> Option { - self.inner.block_cache.read().lowest_block_number() - } - - /// Returns the highest block that is present in both the deposit and block caches. - pub fn highest_safe_block(&self) -> Option { - let block_cache = self.blocks().read().highest_block_number()?; - let deposit_cache = self.deposits().read().last_processed_block?; - - Some(std::cmp::min(block_cache, deposit_cache)) - } - - /// Returns the number of currently cached blocks. - pub fn block_cache_len(&self) -> usize { - self.blocks().read().len() - } - - /// Returns the number deposits available in the deposit cache. - pub fn deposit_cache_len(&self) -> usize { - self.deposits().read().cache.len() - } - - /// Returns the number of deposits with valid signatures that have been observed. - pub fn get_valid_signature_count(&self) -> Option { - let highest_safe_block = self.highest_safe_block()?; - self.deposits() - .read() - .cache - .get_valid_signature_count(highest_safe_block) - } - - /// Returns the number of deposits with valid signatures that have been observed, without - /// respecting the `highest_safe_block`. - pub fn get_raw_valid_signature_count(&self) -> Option { - let deposits = self.deposits().read(); - deposits - .cache - .get_valid_signature_count(deposits.cache.latest_block_number()) - } - - /// Returns the number of deposits with valid signatures that have been observed up to and - /// including the block at `block_number`. - /// - /// Returns `None` if the `block_number` is zero or prior to contract deployment. - pub fn get_valid_signature_count_at_block(&self, block_number: u64) -> Option { - self.deposits() - .read() - .cache - .get_valid_signature_count(block_number) - } - - /// Read the service's configuration. - pub fn config(&self) -> RwLockReadGuard { - self.inner.config.read() - } - - /// Updates the configuration in `self to be `new_config`. - /// - /// Will truncate the block cache if the new configure specifies truncation. - pub fn update_config(&self, new_config: Config) -> Result<(), String> { - let mut old_config = self.inner.config.write(); - - if new_config.deposit_contract_deploy_block != old_config.deposit_contract_deploy_block { - // This may be possible, I just haven't looked into the details to ensure it's safe. - Err("Updating deposit_contract_deploy_block is not supported".to_string()) - } else { - *old_config = new_config; - - // Prevents a locking condition when calling prune_blocks. - drop(old_config); - - self.inner.prune_blocks(); - - Ok(()) - } - } - - /// Set the lowest block that the block cache will store. - /// - /// Note: this block may not always be present if truncating is enabled. - pub fn set_lowest_cached_block(&self, block_number: u64) { - self.inner.config.write().lowest_cached_block_number = block_number; - } - - /// Update the deposit and block cache, returning an error if either fail. - /// - /// ## Returns - /// - /// - Ok(_) if the update was successful (the cache may or may not have been modified). - /// - Err(_) if there is an error. - /// - /// Emits logs for debugging and errors. - pub async fn update( - &self, - ) -> Result<(DepositCacheUpdateOutcome, BlockCacheUpdateOutcome), String> { - let client = self.client(); - let chain_id = self.config().chain_id.clone(); - let node_far_behind_seconds = self.inner.config.read().node_far_behind_seconds; - - match endpoint_state(client, &chain_id).await { - Ok(()) => crate::metrics::set_gauge(&metrics::ETH1_CONNECTED, 1), - Err(e) => { - crate::metrics::set_gauge(&metrics::ETH1_CONNECTED, 0); - return Err(format!("Invalid endpoint state: {:?}", e)); - } - } - let (remote_head_block, new_block_numbers_deposit, new_block_numbers_block_cache) = - get_remote_head_and_new_block_ranges(client, self, node_far_behind_seconds) - .await - .map_err(|e| format!("Failed to get remote head and new block ranges: {:?}", e))?; - - *self.inner.remote_head_block.write() = Some(remote_head_block); - - let update_deposit_cache = async { - let outcome_result = self - .update_deposit_cache(Some(new_block_numbers_deposit)) - .await; - - // Reset the `last_procesed block` to the last valid deposit's block number. - // This will ensure that the next batch of blocks fetched is immediately after - // the last cached valid deposit allowing us to recover from scenarios where - // the deposit cache gets corrupted due to invalid responses from eth1 nodes. - if let Err(Error::FailedToInsertDeposit(DepositCacheError::NonConsecutive { - log_index: _, - expected: _, - })) = &outcome_result - { - let mut deposit_cache = self.inner.deposit_cache.write(); - debug!( - old_block_number = deposit_cache.last_processed_block, - new_block_number = deposit_cache.cache.latest_block_number(), - "Resetting last processed block" - ); - deposit_cache.last_processed_block = - Some(deposit_cache.cache.latest_block_number()); - } - - let outcome = - outcome_result.map_err(|e| format!("Failed to update deposit cache: {:?}", e))?; - - trace!( - cached_deposits = self.inner.deposit_cache.read().cache.len(), - logs_imported = outcome.logs_imported, - last_processed_execution_block = - self.inner.deposit_cache.read().last_processed_block, - "Updated deposit cache" - ); - Ok::<_, String>(outcome) - }; - - let update_block_cache = async { - let outcome = self - .update_block_cache(Some(new_block_numbers_block_cache)) - .await - .map_err(|e| format!("Failed to update deposit contract block cache: {:?}", e))?; - - trace!( - cached_blocks = self.inner.block_cache.read().len(), - blocks_imported = outcome.blocks_imported, - head_block = outcome.head_block_number, - "Updated deposit contract block cache" - ); - Ok::<_, String>(outcome) - }; - - let (deposit_outcome, block_outcome) = - futures::try_join!(update_deposit_cache, update_block_cache)?; - - Ok((deposit_outcome, block_outcome)) - } - - /// A looping future that updates the cache, then waits `config.auto_update_interval` before - /// updating it again. - /// - /// ## Returns - /// - /// - Ok(_) if the update was successful (the cache may or may not have been modified). - /// - Err(_) if there is an error. - /// - /// Emits logs for debugging and errors. - pub fn auto_update(self, handle: task_executor::TaskExecutor) { - let update_interval = Duration::from_millis(self.config().auto_update_interval_millis); - - let mut interval = interval_at(Instant::now(), update_interval); - - let update_future = async move { - loop { - interval.tick().await; - self.do_update(update_interval).await.ok(); - } - }; - - handle.spawn(update_future, "eth1"); - } - - async fn do_update(&self, update_interval: Duration) -> Result<(), ()> { - let update_result = self.update().await; - match update_result { - Err(e) => error!( - retry_millis = update_interval.as_millis(), - error = e, - "Error updating deposit contract cache" - ), - Ok((deposit, block)) => debug!( - retry_millis = update_interval.as_millis(), - ?block, - ?deposit, - "Updated deposit contract cache" - ), - }; - let optional_eth1data = self.inner.to_finalize.write().take(); - if let Some(eth1data_to_finalize) = optional_eth1data { - let already_finalized = self - .inner - .deposit_cache - .read() - .cache - .finalized_deposit_count(); - let deposit_count_to_finalize = eth1data_to_finalize.deposit_count; - if deposit_count_to_finalize > already_finalized { - match self.finalize_deposits(eth1data_to_finalize) { - Err(e) => warn!( - error = ?e, - info = "this should resolve on its own", - "Failed to finalize deposit cache" - ), - Ok(()) => info!( - finalized_deposit_count = deposit_count_to_finalize, - "Successfully finalized deposit tree" - ), - } - } else { - debug!( - %already_finalized, - %deposit_count_to_finalize, - "Deposits tree already finalized" - ); - } - } - Ok(()) - } - - /// Returns the range of new block numbers to be considered for the given head type. - fn relevant_new_block_numbers( - &self, - remote_highest_block_number: u64, - remote_highest_block_timestamp: Option, - head_type: HeadType, - ) -> Result>, Error> { - let follow_distance = self.cache_follow_distance(); - let latest_cached_block = self.latest_cached_block(); - let next_required_block = match head_type { - HeadType::Deposit => self - .deposits() - .read() - .last_processed_block - .map(|n| n + 1) - .unwrap_or_else(|| self.config().deposit_contract_deploy_block), - HeadType::BlockCache => latest_cached_block - .as_ref() - .map(|block| block.number + 1) - .unwrap_or_else(|| self.config().lowest_cached_block_number), - }; - - relevant_block_range( - remote_highest_block_number, - remote_highest_block_timestamp, - next_required_block, - follow_distance, - latest_cached_block.as_ref(), - &self.inner.spec, - ) - } - - pub fn finalize_deposits(&self, eth1_data: Eth1Data) -> Result<(), Error> { - let eth1_block = self - .inner - .block_cache - .read() - .block_by_hash(ð1_data.block_hash) - .cloned() - .ok_or_else(|| { - Error::FailedToFinalizeDeposit(format!( - "Finalized block not found in block cache: {:?}", - eth1_data.block_hash - )) - })?; - self.inner - .deposit_cache - .write() - .cache - .finalize(eth1_block) - .map_err(|e| Error::FailedToFinalizeDeposit(format!("{:?}", e))) - } - - pub fn get_deposit_snapshot(&self) -> Option { - self.inner.deposit_cache.read().cache.get_deposit_snapshot() - } - - /// Contacts the remote eth1 node and attempts to import deposit logs up to the configured - /// follow-distance block. - /// - /// Will process no more than `BLOCKS_PER_LOG_QUERY * MAX_LOG_REQUESTS_PER_UPDATE` blocks in a - /// single update. - /// - /// If `remote_highest_block_opt` is `Some`, use that value instead of querying `self.endpoint` - /// for the head of the eth1 chain. - /// - /// ## Resolves with - /// - /// - Ok(_) if the update was successful (the cache may or may not have been modified). - /// - Err(_) if there is an error. - /// - /// Emits logs for debugging and errors. - pub async fn update_deposit_cache( - &self, - new_block_numbers: Option>>, - ) -> Result { - let client = self.client(); - let deposit_contract_address = self.config().deposit_contract_address.clone(); - - let blocks_per_log_query = self.config().blocks_per_log_query; - let max_log_requests_per_update = self - .config() - .max_log_requests_per_update - .unwrap_or(usize::MAX); - - let range = { - match new_block_numbers { - Some(range) => range, - None => { - relevant_new_block_numbers_from_endpoint(client, self, HeadType::Deposit) - .await? - } - } - }; - - let block_number_chunks = if let Some(range) = range { - range - .collect::>() - .chunks(blocks_per_log_query) - .take(max_log_requests_per_update) - .map(|vec| { - let first = vec.first().cloned().unwrap_or(0); - let last = vec.last().map(|n| n + 1).unwrap_or(0); - first..last - }) - .collect::>>() - } else { - Vec::new() - }; - - let mut logs_imported: usize = 0; - let deposit_contract_address_ref: &str = &deposit_contract_address; - for block_range in block_number_chunks.into_iter() { - if block_range.is_empty() { - debug!("No new blocks to scan for logs"); - continue; - } - - /* - * Step 1. Download logs. - */ - let block_range_ref = &block_range; - let logs = client - .get_deposit_logs_in_range( - deposit_contract_address_ref, - block_range_ref.clone(), - Duration::from_millis(GET_DEPOSIT_LOG_TIMEOUT_MILLIS), - ) - .await - .map_err(Error::GetDepositLogsFailed)?; - - /* - * Step 2. Import logs to cache. - */ - let mut cache = self.deposits().write(); - logs.iter() - .map(|raw_log| { - raw_log.to_deposit_log(self.inner.spec()).map_err(|error| { - Error::FailedToParseDepositLog { - block_range: block_range.clone(), - error, - } - }) - }) - // Return early if any of the logs cannot be parsed. - // - // This costs an additional `collect`, however it enforces that no logs are - // imported if any one of them cannot be parsed. - .collect::, _>>()? - .into_iter() - // Returns if a deposit is unable to be added to the cache. - // - // If this error occurs, the cache will no longer be guaranteed to hold either - // none or all of the logs for each block (i.e., they may exist _some_ logs for - // a block, but not _all_ logs for that block). This scenario can cause the - // node to choose an invalid genesis state or propose an invalid block. - .try_for_each(|deposit_log| { - if let DepositCacheInsertOutcome::Inserted = cache - .cache - .insert_log(deposit_log) - .map_err(Error::FailedToInsertDeposit)? - { - logs_imported += 1; - } - - Ok::<_, Error>(()) - })?; - - debug!(logs = logs.len(), "Imported deposit logs chunk"); - - cache.last_processed_block = Some(block_range.end.saturating_sub(1)); - - metrics::set_gauge(&metrics::DEPOSIT_CACHE_LEN, cache.cache.len() as i64); - metrics::set_gauge( - &metrics::HIGHEST_PROCESSED_DEPOSIT_BLOCK, - cache.last_processed_block.unwrap_or(0) as i64, - ); - } - - if logs_imported > 0 { - info!( - latest_block = self.inner.deposit_cache.read().cache.latest_block_number(), - total = self.deposit_cache_len(), - new = logs_imported, - "Imported deposit log(s)" - ); - } else { - debug!( - latest_block = self.inner.deposit_cache.read().cache.latest_block_number(), - total_deposits = self.deposit_cache_len(), - "No new deposits found" - ); - } - - Ok(DepositCacheUpdateOutcome { logs_imported }) - } - - /// Contacts the remote eth1 node and attempts to import all blocks up to the configured - /// follow-distance block. - /// - /// If configured, prunes the block cache after importing new blocks. - /// - /// If `remote_highest_block_opt` is `Some`, use that value instead of querying `self.endpoint` - /// for the head of the eth1 chain. - /// - /// ## Resolves with - /// - /// - Ok(_) if the update was successful (the cache may or may not have been modified). - /// - Err(_) if there is an error. - /// - /// Emits logs for debugging and errors. - pub async fn update_block_cache( - &self, - new_block_numbers: Option>>, - ) -> Result { - let client = self.client(); - let block_cache_truncation = self.config().block_cache_truncation; - let max_blocks_per_update = self.config().max_blocks_per_update.unwrap_or(usize::MAX); - - let range = { - match new_block_numbers { - Some(range) => range, - None => { - relevant_new_block_numbers_from_endpoint(client, self, HeadType::BlockCache) - .await? - } - } - }; - - // Map the range of required blocks into a Vec. - // - // If the required range is larger than the size of the cache, drop the exiting cache - // because it's exipred and just download enough blocks to fill the cache. - let required_block_numbers = if let Some(range) = range { - if range.start() > range.end() { - // Note: this check is not strictly necessary, however it remains to safe - // guard against any regression which may cause an underflow in a following - // subtraction operation. - return Err(Error::Internal("Range was not increasing".into())); - } else { - let range_size = range.end() - range.start(); - let max_size = block_cache_truncation - .map(|n| n as u64) - .unwrap_or_else(|| u64::MAX); - if range_size > max_size { - // If the range of required blocks is larger than `max_size`, drop all - // existing blocks and download `max_size` count of blocks. - let first_block = range.end() - max_size; - (*self.inner.block_cache.write()) = BlockCache::default(); - (first_block..=*range.end()).collect::>() - } else { - range.collect::>() - } - } - } else { - Vec::new() - }; - - // This value is used to prevent the block cache from importing a block that is not yet in - // the deposit cache. - let latest_in_cache = self - .inner - .deposit_cache - .read() - .last_processed_block - .unwrap_or(0); - - let required_block_numbers = required_block_numbers - .into_iter() - .filter(|x| *x <= latest_in_cache) - .take(max_blocks_per_update) - .collect::>(); - - debug!( - first = ?required_block_numbers.first(), - last = ?required_block_numbers.last(), - "Downloading execution blocks" - ); - - // Produce a stream from the list of required block numbers and return a future that - // consumes the it. - - let mut blocks_imported = 0; - for block_number in required_block_numbers { - let eth1_block = - download_eth1_block(client, self.inner.clone(), Some(block_number)).await?; - - self.inner - .block_cache - .write() - .insert_root_or_child(eth1_block) - .map_err(Error::FailedToInsertEth1Block)?; - - metrics::set_gauge( - &metrics::BLOCK_CACHE_LEN, - self.inner.block_cache.read().len() as i64, - ); - metrics::set_gauge( - &metrics::LATEST_CACHED_BLOCK_TIMESTAMP, - self.inner - .block_cache - .read() - .latest_block_timestamp() - .unwrap_or(0) as i64, - ); - - blocks_imported += 1; - } - - // Prune the block cache, preventing it from growing too large. - self.inner.prune_blocks(); - - metrics::set_gauge( - &metrics::BLOCK_CACHE_LEN, - self.inner.block_cache.read().len() as i64, - ); - - let block_cache = self.inner.block_cache.read(); - let latest_block_mins = block_cache - .latest_block_timestamp() - .and_then(|timestamp| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|now| now.checked_sub(Duration::from_secs(timestamp))) - }) - .map(|duration| format!("{} mins", duration.as_secs() / 60)) - .unwrap_or_else(|| "n/a".into()); - - if blocks_imported > 0 { - debug!( - latest_block_age = latest_block_mins, - latest_block = block_cache.highest_block_number(), - total_cached_blocks = block_cache.len(), - new = %blocks_imported, - "Imported execution block(s)" - ); - } else { - debug!( - latest_block = block_cache.highest_block_number(), - cached_blocks = block_cache.len(), - "No new execution blocks imported" - ); - } - - Ok(BlockCacheUpdateOutcome { - blocks_imported, - head_block_number: block_cache.highest_block_number(), - }) - } -} - -/// Returns the range of blocks starting from `next_required_block` that are at least -/// `follow_distance` many blocks before `remote_highest_block`. -/// Returns an error if `next_required_block > remote_highest_block + 1` which means the remote went -/// backwards. -fn relevant_block_range( - remote_highest_block_number: u64, - remote_highest_block_timestamp: Option, - next_required_block: u64, - cache_follow_distance: u64, - latest_cached_block: Option<&Eth1Block>, - spec: &ChainSpec, -) -> Result>, Error> { - // If the latest cached block is lagging the head block by more than `cache_follow_distance` - // times the expected block time then the eth1 block time is likely quite different from what we - // assumed. - // - // In order to catch up, load batches of `CATCHUP_BATCH_SIZE` until the situation rights itself. - // Note that we need to check this condition before the regular follow distance condition - // or we will keep downloading small numbers of blocks. - if let (Some(remote_highest_block_timestamp), Some(latest_cached_block)) = - (remote_highest_block_timestamp, latest_cached_block) - { - let lagging = latest_cached_block.timestamp - + cache_follow_distance * spec.seconds_per_eth1_block - < remote_highest_block_timestamp; - let end_block = std::cmp::max( - std::cmp::min( - remote_highest_block_number.saturating_sub(CATCHUP_MIN_FOLLOW_DISTANCE), - next_required_block + CATCHUP_BATCH_SIZE, - ), - remote_highest_block_number.saturating_sub(cache_follow_distance), - ); - if lagging && next_required_block <= end_block { - return Ok(Some(next_required_block..=end_block)); - } - } - - let remote_follow_block = remote_highest_block_number.saturating_sub(cache_follow_distance); - if next_required_block <= remote_follow_block { - Ok(Some(next_required_block..=remote_follow_block)) - } else if next_required_block > remote_highest_block_number + 1 { - // If this is the case, the node must have gone "backwards" in terms of it's sync - // (i.e., it's head block is lower than it was before). - // - // We assume that the `cache_follow_distance` should be sufficient to ensure this never - // happens, otherwise it is an error. - Err(Error::RemoteNotSynced { - next_required_block, - remote_highest_block: remote_highest_block_number, - cache_follow_distance, - }) - } else { - // Return an empty range. - Ok(None) - } -} - -/// Downloads the `(block, deposit_root, deposit_count)` tuple from an eth1 node for the given -/// `block_number`. -/// -/// Set `block_number_opt = None` to get the "latest" eth1 block (i.e., the head). -/// -/// Performs three async calls to an Eth1 HTTP JSON RPC endpoint. -async fn download_eth1_block( - endpoint: &HttpJsonRpc, - cache: Arc, - block_number_opt: Option, -) -> Result { - let deposit_root = block_number_opt.and_then(|block_number| { - cache - .deposit_cache - .read() - .cache - .get_deposit_root_from_cache(block_number) - }); - - let deposit_count = block_number_opt.and_then(|block_number| { - cache - .deposit_cache - .read() - .cache - .get_deposit_count_from_cache(block_number) - }); - - // Performs a `get_blockByNumber` call to an eth1 node. - let http_block = endpoint - .get_block( - block_number_opt - .map(BlockQuery::Number) - .unwrap_or_else(|| BlockQuery::Latest), - Duration::from_millis(GET_BLOCK_TIMEOUT_MILLIS), - ) - .map_err(Error::BlockDownloadFailed) - .await?; - - Ok(Eth1Block { - hash: http_block.hash, - number: http_block.number, - timestamp: http_block.timestamp, - deposit_root, - deposit_count, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use types::MainnetEthSpec; - - #[test] - // Ensures the default config does not panic. - fn default_config() { - Config::default(); - } - - #[test] - fn serde_serialize() { - let serialized = - serde_yaml::to_string(&Config::default()).expect("Should serde encode default config"); - serde_yaml::from_str::(&serialized).expect("Should serde decode default config"); - } - - #[test] - fn block_cache_size() { - let mut config = Config::default(); - - let spec = MainnetEthSpec::default_spec(); - - config.set_block_cache_truncation::(&spec); - - let len = config.block_cache_truncation.unwrap(); - - let seconds_per_voting_period = - ::SlotsPerEth1VotingPeriod::to_u64() * spec.seconds_per_slot; - let eth1_blocks_per_voting_period = seconds_per_voting_period / spec.seconds_per_eth1_block; - let cache_follow_distance_blocks = config.follow_distance - config.cache_follow_distance(); - - let minimum_len = eth1_blocks_per_voting_period * 2 + cache_follow_distance_blocks; - - assert!(len > minimum_len as usize); - } -} diff --git a/beacon_node/eth1/tests/test.rs b/beacon_node/eth1/tests/test.rs deleted file mode 100644 index 48ed189259..0000000000 --- a/beacon_node/eth1/tests/test.rs +++ /dev/null @@ -1,836 +0,0 @@ -#![cfg(test)] -use environment::{Environment, EnvironmentBuilder}; -use eth1::{Config, Eth1Endpoint, Service}; -use eth1::{DepositCache, DEFAULT_CHAIN_ID}; -use eth1_test_rig::{AnvilEth1Instance, Http, Middleware, Provider}; -use execution_layer::http::{deposit_methods::*, HttpJsonRpc, Log}; -use logging::create_test_tracing_subscriber; -use merkle_proof::verify_merkle_proof; -use sensitive_url::SensitiveUrl; -use std::ops::Range; -use std::sync::Arc; -use std::time::Duration; -use tree_hash::TreeHash; -use types::{ - DepositData, EthSpec, FixedBytesExtended, Hash256, Keypair, MainnetEthSpec, MinimalEthSpec, - Signature, -}; - -const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32; - -pub fn new_env() -> Environment { - create_test_tracing_subscriber(); - EnvironmentBuilder::minimal() - .multi_threaded_tokio_runtime() - .expect("should start tokio runtime") - .build() - .expect("should build env") -} - -fn timeout() -> Duration { - Duration::from_secs(2) -} - -fn random_deposit_data() -> DepositData { - let keypair = Keypair::random(); - - let mut deposit = DepositData { - pubkey: keypair.pk.into(), - withdrawal_credentials: Hash256::zero(), - amount: 32_000_000_000, - signature: Signature::empty().into(), - }; - - deposit.signature = deposit.create_signature(&keypair.sk, &MainnetEthSpec::default_spec()); - - deposit -} - -/// Blocking operation to get the deposit logs from the `deposit_contract`. -async fn blocking_deposit_logs( - client: &HttpJsonRpc, - eth1: &AnvilEth1Instance, - range: Range, -) -> Vec { - client - .get_deposit_logs_in_range(ð1.deposit_contract.address(), range, timeout()) - .await - .expect("should get logs") -} - -/// Blocking operation to get the deposit root from the `deposit_contract`. -async fn blocking_deposit_root( - client: &HttpJsonRpc, - eth1: &AnvilEth1Instance, - block_number: u64, -) -> Option { - client - .get_deposit_root(ð1.deposit_contract.address(), block_number, timeout()) - .await - .expect("should get deposit root") -} - -/// Blocking operation to get the deposit count from the `deposit_contract`. -async fn blocking_deposit_count( - client: &HttpJsonRpc, - eth1: &AnvilEth1Instance, - block_number: u64, -) -> Option { - client - .get_deposit_count(ð1.deposit_contract.address(), block_number, timeout()) - .await - .expect("should get deposit count") -} - -async fn get_block_number(client: &Provider) -> u64 { - client - .get_block_number() - .await - .map(|v| v.as_u64()) - .expect("should get block number") -} - -async fn new_anvil_instance() -> Result { - AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await -} - -mod eth1_cache { - use super::*; - - #[tokio::test] - async fn simple_scenario() { - create_test_tracing_subscriber(); - async { - for follow_distance in 0..3 { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let initial_block_number = get_block_number(&anvil_client).await; - - let config = Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: initial_block_number, - follow_distance, - ..Config::default() - }; - let cache_follow_distance = config.cache_follow_distance(); - - let service = - Service::new(config, Arc::new(MainnetEthSpec::default_spec())).unwrap(); - - // Create some blocks and then consume them, performing the test `rounds` times. - for round in 0..2 { - let blocks = 4; - - let initial = if round == 0 { - initial_block_number - } else { - service - .blocks() - .read() - .highest_block_number() - .map(|n| n + cache_follow_distance) - .expect("should have a latest block after the first round") - }; - - for _ in 0..blocks { - eth1.anvil.evm_mine().await.expect("should mine block"); - } - - service - .update_deposit_cache(None) - .await - .expect("should update deposit cache"); - service - .update_block_cache(None) - .await - .expect("should update block cache"); - - service - .update_block_cache(None) - .await - .expect("should update cache when nothing has changed"); - - assert_eq!( - service - .blocks() - .read() - .highest_block_number() - .map(|n| n + cache_follow_distance), - Some(initial + blocks), - "should update {} blocks in round {} (follow {} i.e. {})", - blocks, - round, - follow_distance, - cache_follow_distance - ); - } - } - } - .await; - } - - /// Tests the case where we attempt to download more blocks than will fit in the cache. - - #[tokio::test] - async fn big_skip() { - create_test_tracing_subscriber(); - async { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let cache_len = 4; - - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&anvil_client).await, - follow_distance: 0, - block_cache_truncation: Some(cache_len), - ..Config::default() - }, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - - let blocks = cache_len * 2; - - for _ in 0..blocks { - eth1.anvil.evm_mine().await.expect("should mine block") - } - - service - .update_deposit_cache(None) - .await - .expect("should update deposit cache"); - service - .update_block_cache(None) - .await - .expect("should update block cache"); - - assert_eq!( - service.block_cache_len(), - cache_len, - "should not grow cache beyond target" - ); - } - .await; - } - - /// Tests to ensure that the cache gets pruned when doing multiple downloads smaller than the - /// cache size. - #[tokio::test] - async fn pruning() { - create_test_tracing_subscriber(); - async { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let cache_len = 4; - - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&anvil_client).await, - follow_distance: 0, - block_cache_truncation: Some(cache_len), - ..Config::default() - }, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - - for _ in 0..4u8 { - for _ in 0..cache_len / 2 { - eth1.anvil.evm_mine().await.expect("should mine block") - } - service - .update_deposit_cache(None) - .await - .expect("should update deposit cache"); - service - .update_block_cache(None) - .await - .expect("should update block cache"); - } - - assert_eq!( - service.block_cache_len(), - cache_len, - "should not grow cache beyond target" - ); - } - .await; - } - - #[tokio::test] - async fn double_update() { - create_test_tracing_subscriber(); - async { - let n = 16; - - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - lowest_cached_block_number: get_block_number(&anvil_client).await, - follow_distance: 0, - ..Config::default() - }, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - - for _ in 0..n { - eth1.anvil.evm_mine().await.expect("should mine block") - } - - futures::try_join!( - service.update_deposit_cache(None), - service.update_deposit_cache(None) - ) - .expect("should perform two simultaneous updates of deposit cache"); - futures::try_join!( - service.update_block_cache(None), - service.update_block_cache(None) - ) - .expect("should perform two simultaneous updates of block cache"); - - assert!(service.block_cache_len() >= n, "should grow the cache"); - } - .await; - } -} - -mod deposit_tree { - - use super::*; - - #[tokio::test] - async fn updating() { - create_test_tracing_subscriber(); - async { - let n = 4; - - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let start_block = get_block_number(&anvil_client).await; - - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - deposit_contract_deploy_block: start_block, - follow_distance: 0, - ..Config::default() - }, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - - for round in 0..3 { - let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - - for deposit in &deposits { - deposit_contract - .deposit(deposit.clone()) - .await - .expect("should perform a deposit"); - } - - service - .update_deposit_cache(None) - .await - .expect("should perform update"); - - service - .update_deposit_cache(None) - .await - .expect("should perform update when nothing has changed"); - - let first = n * round; - let last = n * (round + 1); - - let (_root, local_deposits) = service - .deposits() - .read() - .cache - .get_deposits(first, last, last) - .unwrap_or_else(|_| panic!("should get deposits in round {}", round)); - - assert_eq!( - local_deposits.len(), - n as usize, - "should get the right number of deposits in round {}", - round - ); - - assert_eq!( - local_deposits - .iter() - .map(|d| d.data.clone()) - .collect::>(), - deposits.to_vec(), - "obtained deposits should match those submitted in round {}", - round - ); - } - } - .await; - } - - #[tokio::test] - async fn double_update() { - create_test_tracing_subscriber(); - async { - let n = 8; - - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let start_block = get_block_number(&anvil_client).await; - - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - deposit_contract_deploy_block: start_block, - lowest_cached_block_number: start_block, - follow_distance: 0, - ..Config::default() - }, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - - let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - - for deposit in &deposits { - deposit_contract - .deposit(deposit.clone()) - .await - .expect("should perform a deposit"); - } - - futures::try_join!( - service.update_deposit_cache(None), - service.update_deposit_cache(None) - ) - .expect("should perform two updates concurrently"); - - assert_eq!(service.deposit_cache_len(), n); - } - .await; - } - - #[tokio::test] - async fn cache_consistency() { - async { - let n = 8; - - let spec = &MainnetEthSpec::default_spec(); - - let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let mut deposit_roots = vec![]; - let mut deposit_counts = vec![]; - - let client = - HttpJsonRpc::new(SensitiveUrl::parse(ð1.endpoint()).unwrap(), None).unwrap(); - - // Perform deposits to the smart contract, recording it's state along the way. - for deposit in &deposits { - deposit_contract - .deposit(deposit.clone()) - .await - .expect("should perform a deposit"); - let block_number = get_block_number(&anvil_client).await; - deposit_roots.push( - blocking_deposit_root(&client, ð1, block_number) - .await - .expect("should get root if contract exists"), - ); - deposit_counts.push( - blocking_deposit_count(&client, ð1, block_number) - .await - .expect("should get count if contract exists"), - ); - } - - let mut tree = DepositCache::default(); - - // Pull all the deposit logs from the contract. - let block_number = get_block_number(&anvil_client).await; - let logs: Vec<_> = blocking_deposit_logs(&client, ð1, 0..block_number) - .await - .iter() - .map(|raw| raw.to_deposit_log(spec).expect("should parse deposit log")) - .inspect(|log| { - tree.insert_log(log.clone()) - .expect("should add consecutive logs"); - }) - .collect(); - - // Check the logs for invariants. - for i in 0..logs.len() { - let log = &logs[i]; - assert_eq!( - log.deposit_data, deposits[i], - "log {} should have correct deposit data", - i - ); - assert_eq!(log.index, i as u64, "log {} should have correct index", i); - } - - // For each deposit test some more invariants - for i in 0..n { - // Ensure the deposit count from the smart contract was as expected. - assert_eq!( - deposit_counts[i], - i as u64 + 1, - "deposit count should be accurate" - ); - - // Ensure that the root from the deposit tree matches what the contract reported. - let (root, deposits) = tree - .get_deposits(0, i as u64, deposit_counts[i]) - .expect("should get deposits"); - assert_eq!( - root, deposit_roots[i], - "tree deposit root {} should match the contract", - i - ); - - // Ensure that the deposits all prove into the root from the smart contract. - let deposit_root = deposit_roots[i]; - for (j, deposit) in deposits.iter().enumerate() { - assert!( - verify_merkle_proof( - deposit.data.tree_hash_root(), - &deposit.proof, - DEPOSIT_CONTRACT_TREE_DEPTH + 1, - j, - deposit_root - ), - "deposit merkle proof should prove into deposit contract root" - ) - } - } - } - .await; - } -} - -/// Tests for the base HTTP requests and response handlers. -mod http { - use super::*; - - async fn get_block(client: &HttpJsonRpc, block_number: u64) -> Block { - client - .get_block(BlockQuery::Number(block_number), timeout()) - .await - .expect("should get block number") - } - - #[tokio::test] - async fn incrementing_deposits() { - async { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - let client = - HttpJsonRpc::new(SensitiveUrl::parse(ð1.endpoint()).unwrap(), None).unwrap(); - - let block_number = get_block_number(&anvil_client).await; - let logs = blocking_deposit_logs(&client, ð1, 0..block_number).await; - assert_eq!(logs.len(), 0); - - let mut old_root = blocking_deposit_root(&client, ð1, block_number).await; - let mut old_block = get_block(&client, block_number).await; - let mut old_block_number = block_number; - - assert_eq!( - blocking_deposit_count(&client, ð1, block_number).await, - Some(0), - "should have deposit count zero" - ); - - for i in 1..=8 { - eth1.anvil - .increase_time(1) - .await - .expect("should be able to increase time on anvil"); - - deposit_contract - .deposit(random_deposit_data()) - .await - .expect("should perform a deposit"); - - // Check the logs. - let block_number = get_block_number(&anvil_client).await; - let logs = blocking_deposit_logs(&client, ð1, 0..block_number).await; - assert_eq!(logs.len(), i, "the number of logs should be as expected"); - - // Check the deposit count. - assert_eq!( - blocking_deposit_count(&client, ð1, block_number).await, - Some(i as u64), - "should have a correct deposit count" - ); - - // Check the deposit root. - let new_root = blocking_deposit_root(&client, ð1, block_number).await; - assert_ne!( - new_root, old_root, - "deposit root should change with each deposit" - ); - old_root = new_root; - - // Check the block hash. - let new_block = get_block(&client, block_number).await; - assert_ne!( - new_block.hash, old_block.hash, - "block hash should change with each deposit" - ); - - // Check to ensure the timestamp is increasing - assert!( - old_block.timestamp <= new_block.timestamp, - "block timestamp should increase" - ); - - old_block = new_block.clone(); - - // Check the block number. - assert!( - block_number > old_block_number, - "block number should increase" - ); - old_block_number = block_number; - - // Check to ensure the block root is changing - assert_ne!( - new_root, - Some(new_block.hash), - "the deposit root should be different to the block hash" - ); - } - } - .await; - } -} - -mod fast { - use super::*; - - // Adds deposits into deposit cache and matches deposit_count and deposit_root - // with the deposit count and root computed from the deposit cache. - #[tokio::test] - async fn deposit_cache_query() { - create_test_tracing_subscriber(); - async { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let now = get_block_number(&anvil_client).await; - let spec = Arc::new(MainnetEthSpec::default_spec()); - let service = Service::new( - Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - deposit_contract_deploy_block: now, - lowest_cached_block_number: now, - follow_distance: 0, - block_cache_truncation: None, - ..Config::default() - }, - spec.clone(), - ) - .unwrap(); - let client = - HttpJsonRpc::new(SensitiveUrl::parse(ð1.endpoint()).unwrap(), None).unwrap(); - let n = 10; - let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - for deposit in &deposits { - deposit_contract - .deposit(deposit.clone()) - .await - .expect("should perform a deposit"); - // Mine an extra block between deposits to test for corner cases - eth1.anvil.evm_mine().await.expect("should mine block"); - } - - service - .update_deposit_cache(None) - .await - .expect("should perform update"); - - assert!( - service.deposit_cache_len() >= n, - "should have imported n deposits" - ); - - for block_num in 0..=get_block_number(&anvil_client).await { - let expected_deposit_count = - blocking_deposit_count(&client, ð1, block_num).await; - let expected_deposit_root = blocking_deposit_root(&client, ð1, block_num).await; - - let deposit_count = service - .deposits() - .read() - .cache - .get_deposit_count_from_cache(block_num); - let deposit_root = service - .deposits() - .read() - .cache - .get_deposit_root_from_cache(block_num); - assert_eq!( - expected_deposit_count, deposit_count, - "deposit count from cache should match queried" - ); - assert_eq!( - expected_deposit_root, deposit_root, - "deposit root from cache should match queried" - ); - } - } - .await; - } -} - -mod persist { - use super::*; - #[tokio::test] - async fn test_persist_caches() { - create_test_tracing_subscriber(); - async { - let eth1 = new_anvil_instance() - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let anvil_client = eth1.json_rpc_client(); - - let now = get_block_number(&anvil_client).await; - let config = Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - deposit_contract_deploy_block: now, - lowest_cached_block_number: now, - follow_distance: 0, - block_cache_truncation: None, - ..Config::default() - }; - let service = - Service::new(config.clone(), Arc::new(MainnetEthSpec::default_spec())).unwrap(); - let n = 10; - let deposits: Vec<_> = (0..n).map(|_| random_deposit_data()).collect(); - for deposit in &deposits { - deposit_contract - .deposit(deposit.clone()) - .await - .expect("should perform a deposit"); - } - - service - .update_deposit_cache(None) - .await - .expect("should perform update"); - - assert!( - service.deposit_cache_len() >= n, - "should have imported n deposits" - ); - - let deposit_count = service.deposit_cache_len(); - - service - .update_block_cache(None) - .await - .expect("should perform update"); - - assert!( - service.block_cache_len() >= n, - "should have imported n eth1 blocks" - ); - - let block_count = service.block_cache_len(); - - let eth1_bytes = service.as_bytes(); - - // Drop service and recover from bytes - drop(service); - - let recovered_service = Service::from_bytes( - ð1_bytes, - config, - Arc::new(MainnetEthSpec::default_spec()), - ) - .unwrap(); - assert_eq!( - recovered_service.block_cache_len(), - block_count, - "Should have equal cached blocks as before recovery" - ); - assert_eq!( - recovered_service.deposit_cache_len(), - deposit_count, - "Should have equal cached deposits as before recovery" - ); - } - .await; - } -} diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index f56159c7b5..c443e94574 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -8,13 +8,14 @@ edition = { workspace = true } alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } alloy-rlp = { workspace = true } +alloy-rpc-types-eth = { workspace = true } arc-swap = "1.6.0" +bls = { workspace = true } builder_client = { path = "../builder_client" } bytes = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["events", "lighthouse"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } -ethers-core = { workspace = true } fixed_bytes = { workspace = true } fork_choice = { workspace = true } hash-db = "0.15.2" @@ -48,6 +49,7 @@ tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } triehash = "0.8.4" +typenum = { workspace = true } types = { workspace = true } warp = { workspace = true } zeroize = { workspace = true } diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index d3a32c7929..e45bf477a2 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -1,6 +1,6 @@ use crate::{ json_structures::{EncodableJsonWithdrawal, JsonWithdrawal}, - keccak::{keccak256, KeccakHasher}, + keccak::{KeccakHasher, keccak256}, }; use alloy_rlp::Encodable; use keccak_hash::KECCAK_EMPTY_LIST_RLP; @@ -80,7 +80,7 @@ mod test { use super::*; use hex::FromHex; use std::str::FromStr; - use types::{Address, Hash256, Hash64, Uint256}; + use types::{Address, Hash64, Hash256, Uint256}; fn test_rlp_encoding( header: &ExecutionBlockHeader, diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 120ed7b776..2dae0d8692 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -5,7 +5,7 @@ use crate::http::{ ENGINE_GET_INCLUSION_LIST_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V1, - ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, + ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -20,15 +20,15 @@ use strum::IntoStaticStr; use superstruct::superstruct; pub use types::{ Address, BeaconBlockRef, ConsolidationRequest, EthSpec, ExecutionBlockHash, ExecutionPayload, - ExecutionPayloadHeader, ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transaction, - Transactions, Uint256, VariableList, Withdrawal, Withdrawals, + ExecutionPayloadHeader, ExecutionPayloadRef, ForkName, Hash256, Transactions, Uint256, + Withdrawal, Withdrawals, }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionRequests, - KzgProofs, + ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionRequests, KzgProofs, }; -use types::{Graffiti, GRAFFITI_BYTES_LEN}; +use types::{GRAFFITI_BYTES_LEN, Graffiti}; pub mod auth; pub mod http; @@ -38,7 +38,7 @@ mod new_payload_request; pub use new_payload_request::{ NewPayloadRequest, NewPayloadRequestBellatrix, NewPayloadRequestCapella, NewPayloadRequestDeneb, NewPayloadRequestEip7805, NewPayloadRequestElectra, - NewPayloadRequestFulu, + NewPayloadRequestFulu, NewPayloadRequestGloas, }; pub const LATEST_TAG: &str = "latest"; @@ -271,7 +271,7 @@ pub struct ProposeBlindedBlockResponse { } #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), @@ -295,12 +295,14 @@ pub struct GetPayloadResponse { pub execution_payload: ExecutionPayloadEip7805, #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] pub execution_payload: ExecutionPayloadFulu, + #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + pub execution_payload: ExecutionPayloadGloas, pub block_value: Uint256, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] pub blobs_bundle: BlobsBundle, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] pub should_override_builder: bool, - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub requests: ExecutionRequests, } @@ -380,6 +382,12 @@ impl From> Some(inner.blobs_bundle), Some(inner.requests), ), + GetPayloadResponse::Gloas(inner) => ( + ExecutionPayload::Gloas(inner.execution_payload), + inner.block_value, + Some(inner.blobs_bundle), + Some(inner.requests), + ), } } } @@ -390,7 +398,7 @@ pub enum GetPayloadResponseType { } impl GetPayloadResponse { - pub fn execution_payload_ref(&self) -> ExecutionPayloadRef { + pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<'_, E> { self.to_ref().into() } } @@ -579,7 +587,6 @@ pub struct EngineCapabilities { pub new_payload_v2: bool, pub new_payload_v3: bool, pub new_payload_v4: bool, - pub new_payload_v5: bool, pub forkchoice_updated_v1: bool, pub forkchoice_updated_v2: bool, pub forkchoice_updated_v3: bool, @@ -611,9 +618,6 @@ impl EngineCapabilities { if self.new_payload_v4 { response.push(ENGINE_NEW_PAYLOAD_V4); } - if self.new_payload_v5 { - response.push(ENGINE_NEW_PAYLOAD_V5); - } if self.forkchoice_updated_v1 { response.push(ENGINE_FORKCHOICE_UPDATED_V1); } @@ -771,14 +775,18 @@ pub struct ClientVersionV1 { } impl ClientVersionV1 { - pub fn calculate_graffiti(&self, lighthouse_commit_prefix: CommitPrefix) -> Graffiti { - let graffiti_string = format!( + pub fn calculate_graffiti( + &self, + lighthouse_commit_prefix: CommitPrefix, + validator_graffiti: Option, + ) -> Graffiti { + let append_graffiti_full = format!( "{}{}LH{}", self.code, self.commit .0 .get(..4) - .map_or_else(|| self.commit.0.as_str(), |s| s) + .unwrap_or(self.commit.0.as_str()) .to_lowercase(), lighthouse_commit_prefix .0 @@ -786,6 +794,53 @@ impl ClientVersionV1 { .unwrap_or("0000") .to_lowercase(), ); + + // Implement the special case here: + // https://hackmd.io/@wmoBhF17RAOH2NZ5bNXJVg/BJX2c9gja#SPECIAL-CASE-the-flexible-standard + let append_graffiti_one_byte = format!( + "{}{}LH{}", + self.code, + self.commit + .0 + .get(..2) + .unwrap_or(self.commit.0.as_str()) + .to_lowercase(), + lighthouse_commit_prefix + .0 + .get(..2) + .unwrap_or("00") + .to_lowercase(), + ); + + let append_graffiti_no_commit = format!("{}LH", self.code); + let append_graffiti_only_el = format!("{}", self.code); + + let graffiti_string = if let Some(graffiti) = validator_graffiti { + let graffiti_length = graffiti.as_utf8_lossy().len(); + let graffiti_str = graffiti.as_utf8_lossy(); + + // 12 characters for append_graffiti_full, plus one character for spacing + // that leaves user specified graffiti to be 32-12-1 = 19 characters max, i.e., <20 + if graffiti_length < 20 { + format!("{} {}", append_graffiti_full, graffiti_str) + // user-specified graffiti is between 20-23 characters + } else if (20..24).contains(&graffiti_length) { + format!("{} {}", append_graffiti_one_byte, graffiti_str) + // user-specified graffiti is between 24-27 characters + } else if (24..28).contains(&graffiti_length) { + format!("{} {}", append_graffiti_no_commit, graffiti_str) + // user-specified graffiti is between 28-29 characters + } else if (28..30).contains(&graffiti_length) { + format!("{} {}", append_graffiti_only_el, graffiti_str) + // if user-specified graffiti is between 30-32 characters, append nothing + } else { + return graffiti; + } + } else { + // if no validator_graffiti (user doesn't specify), use the full client version info graffiti + append_graffiti_full + }; + let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN]; let bytes_to_copy = std::cmp::min(graffiti_string.len(), GRAFFITI_BYTES_LEN); graffiti_bytes[..bytes_to_copy] diff --git a/beacon_node/execution_layer/src/engine_api/auth.rs b/beacon_node/execution_layer/src/engine_api/auth.rs index 2f4c0cd1e8..af1ca195bd 100644 --- a/beacon_node/execution_layer/src/engine_api/auth.rs +++ b/beacon_node/execution_layer/src/engine_api/auth.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use jsonwebtoken::{encode, get_current_timestamp, Algorithm, EncodingKey, Header}; +use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp}; use rand::Rng; use serde::{Deserialize, Serialize}; use zeroize::Zeroize; @@ -46,7 +46,7 @@ impl JwtKey { /// Generate a random secret. pub fn random() -> Self { - Self(rand::thread_rng().gen::<[u8; JWT_SECRET_LENGTH]>()) + Self(rand::rng().random::<[u8; JWT_SECRET_LENGTH]>()) } /// Returns a reference to the underlying byte array. diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 68ae8e9081..e11553ff86 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -35,7 +35,6 @@ pub const ENGINE_NEW_PAYLOAD_V1: &str = "engine_newPayloadV1"; pub const ENGINE_NEW_PAYLOAD_V2: &str = "engine_newPayloadV2"; pub const ENGINE_NEW_PAYLOAD_V3: &str = "engine_newPayloadV3"; pub const ENGINE_NEW_PAYLOAD_V4: &str = "engine_newPayloadV4"; -pub const ENGINE_NEW_PAYLOAD_V5: &str = "engine_newPayloadV5"; pub const ENGINE_NEW_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(8); pub const ENGINE_GET_PAYLOAD_V1: &str = "engine_getPayloadV1"; @@ -78,7 +77,6 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, - ENGINE_NEW_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, @@ -109,9 +107,10 @@ pub static LIGHTHOUSE_JSON_CLIENT_VERSION: LazyLock = /// Contains methods to convert arbitrary bytes to an ETH2 deposit contract object. pub mod deposit_log { + use bls::{PublicKeyBytes, SignatureBytes}; use ssz::Decode; use state_processing::per_block_processing::signature_sets::deposit_pubkey_signature_message; - use types::{ChainSpec, DepositData, Hash256, PublicKeyBytes, SignatureBytes}; + use types::{ChainSpec, DepositData, Hash256}; pub use eth2::lighthouse::DepositLog; @@ -230,7 +229,7 @@ pub mod deposit_methods { use super::Log; use crate::HttpJsonRpc; use serde::{Deserialize, Serialize}; - use serde_json::{json, Value}; + use serde_json::{Value, json}; use std::fmt; use std::ops::Range; use std::str::FromStr; @@ -658,7 +657,7 @@ impl HttpJsonRpc { let mut request = self .client - .post(self.url.full.clone()) + .post(self.url.expose_full().clone()) .timeout(timeout) .header(CONTENT_TYPE, "application/json") .json(&body); @@ -731,7 +730,7 @@ impl HttpJsonRpc { pub async fn get_blobs_v2( &self, versioned_hashes: Vec, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let params = json!([versioned_hashes]); self.rpc_request( @@ -790,7 +789,7 @@ impl HttpJsonRpc { &self, execution_payload: ExecutionPayload, ) -> Result { - let params = json!([JsonExecutionPayload::from(execution_payload)]); + let params = json!([JsonExecutionPayload::try_from(execution_payload)?]); let response: JsonPayloadStatusV1 = self .rpc_request( @@ -807,7 +806,7 @@ impl HttpJsonRpc { &self, execution_payload: ExecutionPayload, ) -> Result { - let params = json!([JsonExecutionPayload::from(execution_payload)]); + let params = json!([JsonExecutionPayload::try_from(execution_payload)?]); let response: JsonPayloadStatusV1 = self .rpc_request( @@ -825,7 +824,12 @@ impl HttpJsonRpc { new_payload_request_deneb: NewPayloadRequestDeneb<'_, E>, ) -> Result { let params = json!([ - JsonExecutionPayload::V3(new_payload_request_deneb.execution_payload.clone().into()), + JsonExecutionPayload::Deneb( + new_payload_request_deneb + .execution_payload + .clone() + .try_into()? + ), new_payload_request_deneb.versioned_hashes, new_payload_request_deneb.parent_beacon_block_root, ]); @@ -846,7 +850,12 @@ impl HttpJsonRpc { new_payload_request_electra: NewPayloadRequestElectra<'_, E>, ) -> Result { let params = json!([ - JsonExecutionPayload::V4(new_payload_request_electra.execution_payload.clone().into()), + JsonExecutionPayload::Electra( + new_payload_request_electra + .execution_payload + .clone() + .try_into()? + ), new_payload_request_electra.versioned_hashes, new_payload_request_electra.parent_beacon_block_root, new_payload_request_electra @@ -865,7 +874,37 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn new_payload_v5_eip7805( + pub async fn new_payload_v4_fulu( + &self, + new_payload_request_fulu: NewPayloadRequestFulu<'_, E>, + ) -> Result { + let params = json!([ + JsonExecutionPayload::Fulu( + new_payload_request_fulu + .execution_payload + .clone() + .try_into()? + ), + new_payload_request_fulu.versioned_hashes, + new_payload_request_fulu.parent_beacon_block_root, + new_payload_request_fulu + .execution_requests + .get_execution_requests_list(), + ]); + + let response: JsonPayloadStatusV1 = self + .rpc_request( + ENGINE_NEW_PAYLOAD_V4, + params, + ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response.into()) + } + + // TODO(EIP7805) fix new payload if needed + pub async fn new_payload_v4_eip7805( &self, new_payload_request_eip7805: NewPayloadRequestEip7805<'_, E>, ) -> Result { @@ -879,7 +918,12 @@ impl HttpJsonRpc { .collect(); let params = json!([ - JsonExecutionPayload::V5(new_payload_request_eip7805.execution_payload.clone().into()), + JsonExecutionPayload::Eip7805( + new_payload_request_eip7805 + .execution_payload + .clone() + .try_into()? + ), new_payload_request_eip7805.versioned_hashes, new_payload_request_eip7805.parent_beacon_block_root, new_payload_request_eip7805 @@ -888,9 +932,10 @@ impl HttpJsonRpc { il_transactions ]); + // TODO(eip7805) should be v5 i think let response: JsonPayloadStatusV1 = self .rpc_request( - ENGINE_NEW_PAYLOAD_V5, + ENGINE_NEW_PAYLOAD_V4, params, ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) @@ -899,38 +944,33 @@ impl HttpJsonRpc { Ok(response.into()) } - pub async fn new_payload_v5_fulu( + pub async fn new_payload_v4_gloas( &self, - _new_payload_request_fulu: NewPayloadRequestFulu<'_, E>, + new_payload_request_gloas: NewPayloadRequestGloas<'_, E>, ) -> Result { - unreachable!("new payload fulu"); - // // TODO(focil) clean this up? - // let mut il_transactions = vec![]; - // for transaction in new_payload_request_fulu.il_transactions { - // if let Ok(hex_tx) = String::from_utf8(transaction.into()).map(|v| format!("0x{}", v)) { - // il_transactions.push(hex_tx); - // } - // } + let params = json!([ + JsonExecutionPayload::Gloas( + new_payload_request_gloas + .execution_payload + .clone() + .try_into()? + ), + new_payload_request_gloas.versioned_hashes, + new_payload_request_gloas.parent_beacon_block_root, + new_payload_request_gloas + .execution_requests + .get_execution_requests_list(), + ]); - // let params = json!([ - // JsonExecutionPayload::V5(new_payload_request_fulu.execution_payload.clone().into()), - // new_payload_request_fulu.versioned_hashes, - // new_payload_request_fulu.parent_beacon_block_root, - // new_payload_request_fulu - // .execution_requests - // .get_execution_requests_list(), - // il_transactions - // ]); + let response: JsonPayloadStatusV1 = self + .rpc_request( + ENGINE_NEW_PAYLOAD_V4, + params, + ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; - // let response: JsonPayloadStatusV1 = self - // .rpc_request( - // ENGINE_NEW_PAYLOAD_V5, - // params, - // ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, - // ) - // .await?; - - // Ok(response.into()) + Ok(response.into()) } pub async fn get_payload_v1( @@ -939,7 +979,7 @@ impl HttpJsonRpc { ) -> Result, Error> { let params = json!([JsonPayloadIdRequest::from(payload_id)]); - let payload_v1: JsonExecutionPayloadV1 = self + let payload_v1: JsonExecutionPayloadBellatrix = self .rpc_request( ENGINE_GET_PAYLOAD_V1, params, @@ -965,26 +1005,26 @@ impl HttpJsonRpc { match fork_name { ForkName::Bellatrix => { - let response: JsonGetPayloadResponseV1 = self + let response: JsonGetPayloadResponseBellatrix = self .rpc_request( ENGINE_GET_PAYLOAD_V2, params, ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) .await?; - JsonGetPayloadResponse::V1(response) + JsonGetPayloadResponse::Bellatrix(response) .try_into() .map_err(Error::BadResponse) } ForkName::Capella => { - let response: JsonGetPayloadResponseV2 = self + let response: JsonGetPayloadResponseCapella = self .rpc_request( ENGINE_GET_PAYLOAD_V2, params, ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) .await?; - JsonGetPayloadResponse::V2(response) + JsonGetPayloadResponse::Capella(response) .try_into() .map_err(Error::BadResponse) } @@ -1004,14 +1044,14 @@ impl HttpJsonRpc { match fork_name { ForkName::Deneb => { - let response: JsonGetPayloadResponseV3 = self + let response: JsonGetPayloadResponseDeneb = self .rpc_request( ENGINE_GET_PAYLOAD_V3, params, ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) .await?; - JsonGetPayloadResponse::V3(response) + JsonGetPayloadResponse::Deneb(response) .try_into() .map_err(Error::BadResponse) } @@ -1031,19 +1071,19 @@ impl HttpJsonRpc { match fork_name { ForkName::Electra => { - let response: JsonGetPayloadResponseV4 = self + let response: JsonGetPayloadResponseElectra = self .rpc_request( ENGINE_GET_PAYLOAD_V4, params, ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) .await?; - JsonGetPayloadResponse::V4(response) + JsonGetPayloadResponse::Electra(response) .try_into() .map_err(Error::BadResponse) } ForkName::Eip7805 => { - let response: JsonGetPayloadResponseV5 = self + let response: JsonGetPayloadResponseEip7805 = self .rpc_request( ENGINE_GET_PAYLOAD_V4, params, @@ -1051,7 +1091,7 @@ impl HttpJsonRpc { ) .await?; - JsonGetPayloadResponse::V5(response) + JsonGetPayloadResponse::Eip7805(response) .try_into() .map_err(Error::BadResponse) } @@ -1070,15 +1110,39 @@ impl HttpJsonRpc { let params = json!([JsonPayloadIdRequest::from(payload_id)]); match fork_name { - ForkName::Fulu | ForkName::Eip7805 => { - let response: JsonGetPayloadResponseV5 = self + ForkName::Fulu => { + let response: JsonGetPayloadResponseFulu = self .rpc_request( ENGINE_GET_PAYLOAD_V5, params, ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, ) .await?; - JsonGetPayloadResponse::V5(response) + JsonGetPayloadResponse::Fulu(response) + .try_into() + .map_err(Error::BadResponse) + } + ForkName::Eip7805 => { + let response: JsonGetPayloadResponseEip7805 = self + .rpc_request( + ENGINE_GET_PAYLOAD_V5, + params, + ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + JsonGetPayloadResponse::Eip7805(response) + .try_into() + .map_err(Error::BadResponse) + } + ForkName::Gloas => { + let response: JsonGetPayloadResponseGloas = self + .rpc_request( + ENGINE_GET_PAYLOAD_V5, + params, + ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + JsonGetPayloadResponse::Gloas(response) .try_into() .map_err(Error::BadResponse) } @@ -1166,10 +1230,14 @@ impl HttpJsonRpc { ) .await?; - Ok(response + response .into_iter() - .map(|opt_json| opt_json.map(From::from)) - .collect()) + .map(|opt_json| { + opt_json + .map(|json| json.try_into().map_err(Error::from)) + .transpose() + }) + .collect::, _>>() } pub async fn get_payload_bodies_by_range_v1( @@ -1190,10 +1258,14 @@ impl HttpJsonRpc { ) .await?; - Ok(response + response .into_iter() - .map(|opt_json| opt_json.map(From::from)) - .collect()) + .map(|opt_json| { + opt_json + .map(|json| json.try_into().map_err(Error::from)) + .transpose() + }) + .collect::, _>>() } pub async fn exchange_capabilities(&self) -> Result { @@ -1212,7 +1284,6 @@ impl HttpJsonRpc { new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2), new_payload_v3: capabilities.contains(ENGINE_NEW_PAYLOAD_V3), new_payload_v4: capabilities.contains(ENGINE_NEW_PAYLOAD_V4), - new_payload_v5: capabilities.contains(ENGINE_NEW_PAYLOAD_V5), forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1), forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), @@ -1319,6 +1390,10 @@ impl HttpJsonRpc { } else { let engine_version = self.get_client_version_v1().await?; *lock = Some(CachedResponse::new(engine_version.clone())); + if !engine_version.is_empty() { + // reset metric gauge when there's a fresh fetch + crate::metrics::reset_execution_layer_info_gauge(); + } Ok(engine_version) } } @@ -1357,19 +1432,27 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_newPayloadV4")) } } - NewPayloadRequest::Eip7805(new_payload_request_eip7805) => { - if engine_capabilities.new_payload_v5 { - self.new_payload_v5_eip7805(new_payload_request_eip7805) - .await + NewPayloadRequest::Fulu(new_payload_request_fulu) => { + if engine_capabilities.new_payload_v4 { + self.new_payload_v4_fulu(new_payload_request_fulu).await } else { - Err(Error::RequiredMethodUnsupported("engine_newPayloadV5")) + Err(Error::RequiredMethodUnsupported("engine_newPayloadV4")) } } - NewPayloadRequest::Fulu(new_payload_request_fulu) => { - if engine_capabilities.new_payload_v5 { - self.new_payload_v5_fulu(new_payload_request_fulu).await + // TODO(EIP7805) engine capabilties should be v5? + NewPayloadRequest::Eip7805(new_payload_request_eip7805) => { + if engine_capabilities.new_payload_v4 { + self.new_payload_v4_eip7805(new_payload_request_eip7805) + .await } else { - Err(Error::RequiredMethodUnsupported("engine_newPayloadV5")) + Err(Error::RequiredMethodUnsupported("engine_newPayloadV4")) + } + } + NewPayloadRequest::Gloas(new_payload_request_gloas) => { + if engine_capabilities.new_payload_v4 { + self.new_payload_v4_gloas(new_payload_request_gloas).await + } else { + Err(Error::RequiredMethodUnsupported("engine_newPayloadV4")) } } } @@ -1421,6 +1504,13 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_getPayloadv5")) } } + ForkName::Gloas => { + if engine_capabilities.get_payload_v5 { + self.get_payload_v5(fork_name, payload_id).await + } else { + Err(Error::RequiredMethodUnsupported("engine_getPayloadv5")) + } + } ForkName::Base | ForkName::Altair => Err(Error::UnsupportedForkVariant(format!( "called get_payload with {}", fork_name @@ -1479,11 +1569,14 @@ impl HttpJsonRpc { mod test { use super::auth::JwtKey; use super::*; - use crate::test_utils::{MockServer, DEFAULT_JWT_SECRET}; + use crate::test_utils::{DEFAULT_JWT_SECRET, MockServer}; + use fixed_bytes::FixedBytesExtended; + use ssz_types::VariableList; use std::future::Future; use std::str::FromStr; use std::sync::Arc; - use types::{FixedBytesExtended, MainnetEthSpec, Unsigned}; + use typenum::Unsigned; + use types::MainnetEthSpec; struct Tester { server: MockServer, @@ -1493,8 +1586,7 @@ mod test { impl Tester { pub fn new(with_auth: bool) -> Self { - let spec = Arc::new(MainnetEthSpec::default_spec()); - let server = MockServer::unit_testing(spec); + let server = MockServer::unit_testing(); let rpc_url = SensitiveUrl::parse(&server.url()).unwrap(); let echo_url = SensitiveUrl::parse(&format!("{}/echo", server.url())).unwrap(); @@ -1590,10 +1682,11 @@ mod test { fn encode_transactions( transactions: Transactions, ) -> Result { - let ep: JsonExecutionPayload = JsonExecutionPayload::V1(JsonExecutionPayloadV1 { - transactions, - ..<_>::default() - }); + let ep: JsonExecutionPayload = + JsonExecutionPayload::Bellatrix(JsonExecutionPayloadBellatrix { + transactions, + ..<_>::default() + }); let json = serde_json::to_value(ep)?; Ok(json.get("transactions").unwrap().clone()) } @@ -1853,16 +1946,16 @@ mod test { fee_recipient: Address::repeat_byte(1), state_root: Hash256::repeat_byte(1), receipts_root: Hash256::repeat_byte(0), - logs_bloom: vec![1; 256].into(), + logs_bloom: vec![1; 256].try_into().unwrap(), prev_randao: Hash256::repeat_byte(1), block_number: 0, gas_limit: 1, gas_used: 2, timestamp: 42, - extra_data: vec![].into(), + extra_data: vec![].try_into().unwrap(), base_fee_per_gas: Uint256::from(1), block_hash: ExecutionBlockHash::repeat_byte(1), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), }, )) .await; @@ -1900,16 +1993,16 @@ mod test { fee_recipient: Address::repeat_byte(1), state_root: Hash256::repeat_byte(1), receipts_root: Hash256::repeat_byte(0), - logs_bloom: vec![1; 256].into(), + logs_bloom: vec![1; 256].try_into().unwrap(), prev_randao: Hash256::repeat_byte(1), block_number: 0, gas_limit: 1, gas_used: 2, timestamp: 42, - extra_data: vec![].into(), + extra_data: vec![].try_into().unwrap(), base_fee_per_gas: Uint256::from(1), block_hash: ExecutionBlockHash::repeat_byte(1), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), }, )) .await @@ -2110,16 +2203,16 @@ mod test { fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: Hash256::zero(), block_number: 1, gas_limit: u64::from_str_radix("1c95111",16).unwrap(), gas_used: 0, timestamp: 5, - extra_data: vec![].into(), + extra_data: vec![].try_into().unwrap(), base_fee_per_gas: Uint256::from(7), block_hash: ExecutionBlockHash::from_str("0x6359b8381a370e2f54072a5784ddd78b6ed024991558c511d4452eb4f6ac898c").unwrap(), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), }); assert_eq!(payload, expected); @@ -2135,16 +2228,16 @@ mod test { fee_recipient: Address::from_str("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(), state_root: Hash256::from_str("0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45").unwrap(), receipts_root: Hash256::from_str("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: Hash256::zero(), block_number: 1, gas_limit: u64::from_str_radix("1c9c380",16).unwrap(), gas_used: 0, timestamp: 5, - extra_data: vec![].into(), + extra_data: vec![].try_into().unwrap(), base_fee_per_gas: Uint256::from(7), block_hash: ExecutionBlockHash::from_str("0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858").unwrap(), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), })) .await; }, diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index e25c15dbed..cac5946ebe 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -1,7 +1,8 @@ use super::*; use alloy_rlp::RlpEncodable; use serde::{Deserialize, Serialize}; -use ssz::Decode; +use ssz::{Decode, TryFromIter}; +use ssz_types::{FixedVector, VariableList, typenum::Unsigned}; use strum::EnumString; use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; @@ -9,7 +10,7 @@ use types::blob_sidecar::BlobsList; use types::execution_requests::{ ConsolidationRequests, DepositRequests, RequestType, WithdrawalRequests, }; -use types::{Blob, FixedVector, KzgProof, Unsigned}; +use types::{Blob, KzgProof}; #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -65,7 +66,7 @@ pub struct JsonPayloadIdResponse { } #[superstruct( - variants(V1, V2, V3, V4, V5, V6), + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive(Debug, PartialEq, Default, Serialize, Deserialize,), serde(bound = "E: EthSpec", rename_all = "camelCase"), @@ -100,19 +101,19 @@ pub struct JsonExecutionPayload { pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, - #[superstruct(only(V2, V3, V4, V5, V6))] + #[superstruct(only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] pub withdrawals: VariableList, - #[superstruct(only(V3, V4, V5, V6))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] #[serde(with = "serde_utils::u64_hex_be")] pub blob_gas_used: u64, - #[superstruct(only(V3, V4, V5, V6))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] #[serde(with = "serde_utils::u64_hex_be")] pub excess_blob_gas: u64, } -impl From> for JsonExecutionPayloadV1 { +impl From> for JsonExecutionPayloadBellatrix { fn from(payload: ExecutionPayloadBellatrix) -> Self { - JsonExecutionPayloadV1 { + JsonExecutionPayloadBellatrix { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -130,9 +131,11 @@ impl From> for JsonExecutionPayloadV1 From> for JsonExecutionPayloadV2 { - fn from(payload: ExecutionPayloadCapella) -> Self { - JsonExecutionPayloadV2 { +impl TryFrom> for JsonExecutionPayloadCapella { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadCapella) -> Result { + Ok(JsonExecutionPayloadCapella { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -147,18 +150,15 @@ impl From> for JsonExecutionPayloadV2 base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), - } + withdrawals: withdrawals_to_json(payload.withdrawals)?, + }) } } -impl From> for JsonExecutionPayloadV3 { - fn from(payload: ExecutionPayloadDeneb) -> Self { - JsonExecutionPayloadV3 { +impl TryFrom> for JsonExecutionPayloadDeneb { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadDeneb) -> Result { + Ok(JsonExecutionPayloadDeneb { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -173,21 +173,18 @@ impl From> for JsonExecutionPayloadV3 { base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_to_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for JsonExecutionPayloadV4 { - fn from(payload: ExecutionPayloadElectra) -> Self { - JsonExecutionPayloadV4 { +impl TryFrom> for JsonExecutionPayloadElectra { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadElectra) -> Result { + Ok(JsonExecutionPayloadElectra { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -202,21 +199,18 @@ impl From> for JsonExecutionPayloadV4 base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_to_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for JsonExecutionPayloadV5 { - fn from(payload: ExecutionPayloadEip7805) -> Self { - JsonExecutionPayloadV5 { +impl TryFrom> for JsonExecutionPayloadFulu { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadFulu) -> Result { + Ok(JsonExecutionPayloadFulu { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -231,21 +225,18 @@ impl From> for JsonExecutionPayloadV5 base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_to_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for JsonExecutionPayloadV6 { - fn from(payload: ExecutionPayloadFulu) -> Self { - JsonExecutionPayloadV6 { +impl TryFrom> for JsonExecutionPayloadEip7805 { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadEip7805) -> Result { + Ok(JsonExecutionPayloadEip7805 { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -260,33 +251,69 @@ impl From> for JsonExecutionPayloadV6 { base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_to_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for JsonExecutionPayload { - fn from(execution_payload: ExecutionPayload) -> Self { +impl TryFrom> for JsonExecutionPayloadGloas { + type Error = ssz_types::Error; + + fn try_from(payload: ExecutionPayloadGloas) -> Result { + Ok(JsonExecutionPayloadGloas { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: withdrawals_to_json(payload.withdrawals)?, + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + }) + } +} + +impl TryFrom> for JsonExecutionPayload { + type Error = ssz_types::Error; + + fn try_from(execution_payload: ExecutionPayload) -> Result { match execution_payload { - ExecutionPayload::Bellatrix(payload) => JsonExecutionPayload::V1(payload.into()), - ExecutionPayload::Capella(payload) => JsonExecutionPayload::V2(payload.into()), - ExecutionPayload::Deneb(payload) => JsonExecutionPayload::V3(payload.into()), - ExecutionPayload::Electra(payload) => JsonExecutionPayload::V4(payload.into()), - ExecutionPayload::Eip7805(payload) => JsonExecutionPayload::V5(payload.into()), - ExecutionPayload::Fulu(payload) => JsonExecutionPayload::V6(payload.into()), + ExecutionPayload::Bellatrix(payload) => { + Ok(JsonExecutionPayload::Bellatrix(payload.into())) + } + ExecutionPayload::Capella(payload) => { + Ok(JsonExecutionPayload::Capella(payload.try_into()?)) + } + ExecutionPayload::Deneb(payload) => { + Ok(JsonExecutionPayload::Deneb(payload.try_into()?)) + } + ExecutionPayload::Electra(payload) => { + Ok(JsonExecutionPayload::Electra(payload.try_into()?)) + } + ExecutionPayload::Fulu(payload) => Ok(JsonExecutionPayload::Fulu(payload.try_into()?)), + ExecutionPayload::Eip7805(payload) => { + Ok(JsonExecutionPayload::Eip7805(payload.try_into()?)) + } + ExecutionPayload::Gloas(payload) => { + Ok(JsonExecutionPayload::Gloas(payload.try_into()?)) + } } } } -impl From> for ExecutionPayloadBellatrix { - fn from(payload: JsonExecutionPayloadV1) -> Self { +impl From> for ExecutionPayloadBellatrix { + fn from(payload: JsonExecutionPayloadBellatrix) -> Self { ExecutionPayloadBellatrix { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, @@ -305,9 +332,11 @@ impl From> for ExecutionPayloadBellatrix From> for ExecutionPayloadCapella { - fn from(payload: JsonExecutionPayloadV2) -> Self { - ExecutionPayloadCapella { +impl TryFrom> for ExecutionPayloadCapella { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadCapella) -> Result { + Ok(ExecutionPayloadCapella { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -322,19 +351,16 @@ impl From> for ExecutionPayloadCapella base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), - } + withdrawals: withdrawals_from_json(payload.withdrawals)?, + }) } } -impl From> for ExecutionPayloadDeneb { - fn from(payload: JsonExecutionPayloadV3) -> Self { - ExecutionPayloadDeneb { +impl TryFrom> for ExecutionPayloadDeneb { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadDeneb) -> Result { + Ok(ExecutionPayloadDeneb { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -349,21 +375,18 @@ impl From> for ExecutionPayloadDeneb { base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_from_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for ExecutionPayloadElectra { - fn from(payload: JsonExecutionPayloadV4) -> Self { - ExecutionPayloadElectra { +impl TryFrom> for ExecutionPayloadElectra { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadElectra) -> Result { + Ok(ExecutionPayloadElectra { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -378,21 +401,18 @@ impl From> for ExecutionPayloadElectra base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_from_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for ExecutionPayloadEip7805 { - fn from(payload: JsonExecutionPayloadV5) -> Self { - ExecutionPayloadEip7805 { +impl TryFrom> for ExecutionPayloadFulu { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadFulu) -> Result { + Ok(ExecutionPayloadFulu { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -407,21 +427,18 @@ impl From> for ExecutionPayloadEip7805 base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_from_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for ExecutionPayloadFulu { - fn from(payload: JsonExecutionPayloadV6) -> Self { - ExecutionPayloadFulu { +impl TryFrom> for ExecutionPayloadEip7805 { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadEip7805) -> Result { + Ok(ExecutionPayloadEip7805 { parent_hash: payload.parent_hash, fee_recipient: payload.fee_recipient, state_root: payload.state_root, @@ -436,27 +453,63 @@ impl From> for ExecutionPayloadFulu { base_fee_per_gas: payload.base_fee_per_gas, block_hash: payload.block_hash, transactions: payload.transactions, - withdrawals: payload - .withdrawals - .into_iter() - .map(Into::into) - .collect::>() - .into(), + withdrawals: withdrawals_from_json(payload.withdrawals)?, blob_gas_used: payload.blob_gas_used, excess_blob_gas: payload.excess_blob_gas, - } + }) } } -impl From> for ExecutionPayload { - fn from(json_execution_payload: JsonExecutionPayload) -> Self { +impl TryFrom> for ExecutionPayloadGloas { + type Error = ssz_types::Error; + + fn try_from(payload: JsonExecutionPayloadGloas) -> Result { + Ok(ExecutionPayloadGloas { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: withdrawals_from_json(payload.withdrawals)?, + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + }) + } +} + +impl TryFrom> for ExecutionPayload { + type Error = ssz_types::Error; + + fn try_from(json_execution_payload: JsonExecutionPayload) -> Result { match json_execution_payload { - JsonExecutionPayload::V1(payload) => ExecutionPayload::Bellatrix(payload.into()), - JsonExecutionPayload::V2(payload) => ExecutionPayload::Capella(payload.into()), - JsonExecutionPayload::V3(payload) => ExecutionPayload::Deneb(payload.into()), - JsonExecutionPayload::V4(payload) => ExecutionPayload::Electra(payload.into()), - JsonExecutionPayload::V5(payload) => ExecutionPayload::Eip7805(payload.into()), - JsonExecutionPayload::V6(payload) => ExecutionPayload::Fulu(payload.into()), + JsonExecutionPayload::Bellatrix(payload) => { + Ok(ExecutionPayload::Bellatrix(payload.into())) + } + JsonExecutionPayload::Capella(payload) => { + Ok(ExecutionPayload::Capella(payload.try_into()?)) + } + JsonExecutionPayload::Deneb(payload) => { + Ok(ExecutionPayload::Deneb(payload.try_into()?)) + } + JsonExecutionPayload::Electra(payload) => { + Ok(ExecutionPayload::Electra(payload.try_into()?)) + } + JsonExecutionPayload::Fulu(payload) => Ok(ExecutionPayload::Fulu(payload.try_into()?)), + JsonExecutionPayload::Eip7805(payload) => { + Ok(ExecutionPayload::Eip7805(payload.try_into()?)) + } + JsonExecutionPayload::Gloas(payload) => { + Ok(ExecutionPayload::Gloas(payload.try_into()?)) + } } } } @@ -500,10 +553,10 @@ impl TryFrom for ExecutionRequests { // Elements of the list **MUST** be ordered by `request_type` in ascending order let current_prefix = RequestType::from_u8(*prefix_byte) .ok_or(RequestsError::InvalidPrefix(*prefix_byte))?; - if let Some(prev) = prev_prefix { - if prev.to_u8() >= current_prefix.to_u8() { - return Err(RequestsError::InvalidOrdering); - } + if let Some(prev) = prev_prefix + && prev.to_u8() >= current_prefix.to_u8() + { + return Err(RequestsError::InvalidOrdering); } prev_prefix = Some(current_prefix); @@ -542,7 +595,7 @@ impl TryFrom for ExecutionRequests { } #[superstruct( - variants(V1, V2, V3, V4, V5, V6), + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive(Debug, PartialEq, Serialize, Deserialize), serde(bound = "E: EthSpec", rename_all = "camelCase") @@ -553,25 +606,30 @@ impl TryFrom for ExecutionRequests { #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub struct JsonGetPayloadResponse { - #[superstruct(only(V1), partial_getter(rename = "execution_payload_v1"))] - pub execution_payload: JsonExecutionPayloadV1, - #[superstruct(only(V2), partial_getter(rename = "execution_payload_v2"))] - pub execution_payload: JsonExecutionPayloadV2, - #[superstruct(only(V3), partial_getter(rename = "execution_payload_v3"))] - pub execution_payload: JsonExecutionPayloadV3, - #[superstruct(only(V4), partial_getter(rename = "execution_payload_v4"))] - pub execution_payload: JsonExecutionPayloadV4, - #[superstruct(only(V5), partial_getter(rename = "execution_payload_v5"))] - pub execution_payload: JsonExecutionPayloadV5, - #[superstruct(only(V6), partial_getter(rename = "execution_payload_v6"))] - pub execution_payload: JsonExecutionPayloadV6, + #[superstruct( + only(Bellatrix), + partial_getter(rename = "execution_payload_bellatrix") + )] + pub execution_payload: JsonExecutionPayloadBellatrix, + #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))] + pub execution_payload: JsonExecutionPayloadCapella, + #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))] + pub execution_payload: JsonExecutionPayloadDeneb, + #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] + pub execution_payload: JsonExecutionPayloadElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: JsonExecutionPayloadFulu, + #[superstruct(only(Eip7805), partial_getter(rename = "execution_payload_eip7805"))] + pub execution_payload: JsonExecutionPayloadEip7805, + #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + pub execution_payload: JsonExecutionPayloadGloas, #[serde(with = "serde_utils::u256_hex_be")] pub block_value: Uint256, - #[superstruct(only(V3, V4, V5, V6))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] pub blobs_bundle: JsonBlobsBundleV1, - #[superstruct(only(V3, V4, V5, V6))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] pub should_override_builder: bool, - #[superstruct(only(V4, V5, V6))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub execution_requests: JsonExecutionRequests, } @@ -579,56 +637,79 @@ impl TryFrom> for GetPayloadResponse { type Error = String; fn try_from(json_get_payload_response: JsonGetPayloadResponse) -> Result { match json_get_payload_response { - JsonGetPayloadResponse::V1(response) => { + JsonGetPayloadResponse::Bellatrix(response) => { Ok(GetPayloadResponse::Bellatrix(GetPayloadResponseBellatrix { execution_payload: response.execution_payload.into(), block_value: response.block_value, })) } - JsonGetPayloadResponse::V2(response) => { + JsonGetPayloadResponse::Capella(response) => { Ok(GetPayloadResponse::Capella(GetPayloadResponseCapella { - execution_payload: response.execution_payload.into(), + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, block_value: response.block_value, })) } - JsonGetPayloadResponse::V3(response) => { + JsonGetPayloadResponse::Deneb(response) => { Ok(GetPayloadResponse::Deneb(GetPayloadResponseDeneb { - execution_payload: response.execution_payload.into(), + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, })) } - JsonGetPayloadResponse::V4(response) => { + JsonGetPayloadResponse::Electra(response) => { Ok(GetPayloadResponse::Electra(GetPayloadResponseElectra { - execution_payload: response.execution_payload.into(), + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, requests: response.execution_requests.try_into().map_err(|e| { - format!("Failed to convert json to execution requests : {:?}", e) + format!("Failed to convert json to execution requests: {:?}", e) })?, })) } - JsonGetPayloadResponse::V5(response) => { - Ok(GetPayloadResponse::Eip7805(GetPayloadResponseEip7805 { - execution_payload: response.execution_payload.into(), - block_value: response.block_value, - blobs_bundle: response.blobs_bundle.into(), - should_override_builder: response.should_override_builder, - requests: response.execution_requests.try_into().map_err(|e| { - format!("Failed to convert json to execution requests {:?}", e) - })?, - })) - } - JsonGetPayloadResponse::V6(response) => { + JsonGetPayloadResponse::Fulu(response) => { Ok(GetPayloadResponse::Fulu(GetPayloadResponseFulu { - execution_payload: response.execution_payload.into(), + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, requests: response.execution_requests.try_into().map_err(|e| { - format!("Failed to convert json to execution requests {:?}", e) + format!("Failed to convert json to execution requests: {:?}", e) + })?, + })) + } + JsonGetPayloadResponse::Eip7805(response) => { + Ok(GetPayloadResponse::Eip7805(GetPayloadResponseEip7805 { + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, + block_value: response.block_value, + blobs_bundle: response.blobs_bundle.into(), + should_override_builder: response.should_override_builder, + requests: response.execution_requests.try_into().map_err(|e| { + format!("Failed to convert json to execution requests: {:?}", e) + })?, + })) + } + JsonGetPayloadResponse::Gloas(response) => { + Ok(GetPayloadResponse::Gloas(GetPayloadResponseGloas { + execution_payload: response.execution_payload.try_into().map_err(|e| { + format!("Failed to convert json to execution payload: {:?}", e) + })?, + block_value: response.block_value, + blobs_bundle: response.blobs_bundle.into(), + should_override_builder: response.should_override_builder, + requests: response.execution_requests.try_into().map_err(|e| { + format!("Failed to convert json to execution requests: {:?}", e) })?, })) } @@ -670,6 +751,26 @@ impl From for Withdrawal { } } } + +// Helper functions to convert between `VariableList` and `VariableList`. +fn withdrawals_to_json( + list: VariableList, +) -> Result, ssz_types::Error> +where + N: Unsigned, +{ + VariableList::try_from_iter(list.into_iter().map(Into::into)) +} + +fn withdrawals_from_json( + list: VariableList, +) -> Result, ssz_types::Error> +where + N: Unsigned, +{ + VariableList::try_from_iter(list.into_iter().map(Into::into)) +} + #[derive(Debug, PartialEq, Clone, RlpEncodable)] pub struct EncodableJsonWithdrawal<'a> { pub index: u64, @@ -973,30 +1074,25 @@ pub struct JsonExecutionPayloadBodyV1 { pub withdrawals: Option>, } -impl From> for ExecutionPayloadBodyV1 { - fn from(value: JsonExecutionPayloadBodyV1) -> Self { - Self { +impl TryFrom> for ExecutionPayloadBodyV1 { + type Error = ssz_types::Error; + + fn try_from(value: JsonExecutionPayloadBodyV1) -> Result { + Ok(Self { transactions: value.transactions, - withdrawals: value.withdrawals.map(|json_withdrawals| { - Withdrawals::::from( - json_withdrawals - .into_iter() - .map(Into::into) - .collect::>(), - ) - }), - } + withdrawals: value.withdrawals.map(withdrawals_from_json).transpose()?, + }) } } -impl From> for JsonExecutionPayloadBodyV1 { - fn from(value: ExecutionPayloadBodyV1) -> Self { - Self { +impl TryFrom> for JsonExecutionPayloadBodyV1 { + type Error = ssz_types::Error; + + fn try_from(value: ExecutionPayloadBodyV1) -> Result { + Ok(Self { transactions: value.transactions, - withdrawals: value.withdrawals.map(|withdrawals| { - VariableList::from(withdrawals.into_iter().map(Into::into).collect::>()) - }), - } + withdrawals: value.withdrawals.map(withdrawals_to_json).transpose()?, + }) } } @@ -1078,10 +1174,10 @@ impl TryFrom for ClientVersionV1 { #[cfg(test)] mod tests { + use bls::{PublicKeyBytes, SignatureBytes}; use ssz::Encode; use types::{ - ConsolidationRequest, DepositRequest, MainnetEthSpec, PublicKeyBytes, RequestType, - SignatureBytes, WithdrawalRequest, + ConsolidationRequest, DepositRequest, MainnetEthSpec, RequestType, WithdrawalRequest, }; use super::*; diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 53b4627b5a..b025bf7a69 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -1,4 +1,4 @@ -use crate::{block_hash::calculate_execution_block_hash, metrics, Error, Transactions}; +use crate::{Error, block_hash::calculate_execution_block_hash, metrics}; use crate::versioned_hashes::verify_versioned_hashes; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; @@ -9,11 +9,12 @@ use types::{ }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionRequests, + ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionRequests, Transactions, }; #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), @@ -39,17 +40,19 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { pub execution_payload: &'block ExecutionPayloadDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload: &'block ExecutionPayloadElectra, - #[superstruct(only(Eip7805), partial_getter(rename = "execution_payload_eip7805"))] - pub execution_payload: &'block ExecutionPayloadEip7805, #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] pub execution_payload: &'block ExecutionPayloadFulu, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Eip7805), partial_getter(rename = "execution_payload_eip7805"))] + pub execution_payload: &'block ExecutionPayloadEip7805, + #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + pub execution_payload: &'block ExecutionPayloadGloas, + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] pub versioned_hashes: Vec, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas))] pub parent_beacon_block_root: Hash256, - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub execution_requests: &'block ExecutionRequests, - #[superstruct(only(Eip7805, Fulu))] + #[superstruct(only(Eip7805, Gloas))] pub il_transactions: Transactions, } @@ -62,6 +65,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(payload) => payload.execution_payload.parent_hash, Self::Eip7805(payload) => payload.execution_payload.parent_hash, Self::Fulu(payload) => payload.execution_payload.parent_hash, + Self::Gloas(payload) => payload.execution_payload.parent_hash, } } @@ -73,6 +77,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(payload) => payload.execution_payload.block_hash, Self::Eip7805(payload) => payload.execution_payload.block_hash, Self::Fulu(payload) => payload.execution_payload.block_hash, + Self::Gloas(payload) => payload.execution_payload.block_hash, } } @@ -84,6 +89,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(payload) => payload.execution_payload.block_number, Self::Eip7805(payload) => payload.execution_payload.block_number, Self::Fulu(payload) => payload.execution_payload.block_number, + Self::Gloas(payload) => payload.execution_payload.block_number, } } @@ -95,6 +101,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload), Self::Eip7805(request) => ExecutionPayloadRef::Eip7805(request.execution_payload), Self::Fulu(request) => ExecutionPayloadRef::Fulu(request.execution_payload), + Self::Gloas(request) => ExecutionPayloadRef::Gloas(request.execution_payload), } } @@ -108,6 +115,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(request) => ExecutionPayload::Electra(request.execution_payload.clone()), Self::Eip7805(request) => ExecutionPayload::Eip7805(request.execution_payload.clone()), Self::Fulu(request) => ExecutionPayload::Fulu(request.execution_payload.clone()), + Self::Gloas(request) => ExecutionPayload::Gloas(request.execution_payload.clone()), } } @@ -212,6 +220,18 @@ impl<'a, E: EthSpec> NewPayloadRequest<'a, E> { parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, })), + + BeaconBlockRef::Fulu(block_ref) => Ok(Self::Fulu(NewPayloadRequestFulu { + execution_payload: &block_ref.body.execution_payload.execution_payload, + versioned_hashes: block_ref + .body + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + parent_beacon_block_root: block_ref.parent_root, + execution_requests: &block_ref.body.execution_requests, + })), BeaconBlockRef::Eip7805(block_ref) => Ok(Self::Eip7805(NewPayloadRequestEip7805 { execution_payload: &block_ref.body.execution_payload.execution_payload, versioned_hashes: block_ref @@ -224,22 +244,12 @@ impl<'a, E: EthSpec> NewPayloadRequest<'a, E> { execution_requests: &block_ref.body.execution_requests, il_transactions, })), - BeaconBlockRef::Fulu(block_ref) => Ok(Self::Fulu(NewPayloadRequestFulu { - execution_payload: &block_ref.body.execution_payload.execution_payload, - versioned_hashes: block_ref - .body - .blob_kzg_commitments - .iter() - .map(kzg_commitment_to_versioned_hash) - .collect(), - parent_beacon_block_root: block_ref.parent_root, - execution_requests: &block_ref.body.execution_requests, - il_transactions, - })), + _ => Err(BeaconStateError::IncorrectStateVariant), } } } +//TODO(EIP7732): Consider implementing these as methods on the NewPayloadRequest struct impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> { type Error = BeaconStateError; @@ -277,18 +287,6 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, })), - BeaconBlockRef::Eip7805(block_ref) => Ok(Self::Eip7805(NewPayloadRequestEip7805 { - execution_payload: &block_ref.body.execution_payload.execution_payload, - versioned_hashes: block_ref - .body - .blob_kzg_commitments - .iter() - .map(kzg_commitment_to_versioned_hash) - .collect(), - parent_beacon_block_root: block_ref.parent_root, - execution_requests: &block_ref.body.execution_requests, - il_transactions: vec![].into(), - })), BeaconBlockRef::Fulu(block_ref) => Ok(Self::Fulu(NewPayloadRequestFulu { execution_payload: &block_ref.body.execution_payload.execution_payload, versioned_hashes: block_ref @@ -299,8 +297,20 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .collect(), parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, - il_transactions: vec![].into(), })), + BeaconBlockRef::Eip7805(block_ref) => Ok(Self::Eip7805(NewPayloadRequestEip7805 { + execution_payload: &block_ref.body.execution_payload.execution_payload, + versioned_hashes: block_ref + .body + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + parent_beacon_block_root: block_ref.parent_root, + execution_requests: &block_ref.body.execution_requests, + il_transactions: vec![].try_into()?, + })), + BeaconBlockRef::Gloas(_) => Err(Self::Error::IncorrectStateVariant), } } } @@ -322,10 +332,15 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<' ExecutionPayloadRef::Electra(_) => Err(Self::Error::IncorrectStateVariant), ExecutionPayloadRef::Eip7805(_) => Err(Self::Error::IncorrectStateVariant), ExecutionPayloadRef::Fulu(_) => Err(Self::Error::IncorrectStateVariant), + //TODO(EIP7732): Probably time to just get rid of this + ExecutionPayloadRef::Gloas(_) => Err(Self::Error::IncorrectStateVariant), } } } +// TODO(EIP-7732) build out the following when it's needed like in Mark's branch +// impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest { + #[cfg(test)] mod test { use crate::versioned_hashes::Error as VersionedHashError; @@ -407,7 +422,7 @@ mod test { *beacon_block .body_mut() .blob_kzg_commitments_mut() - .expect("should get commitments") = commitments.into(); + .expect("should get commitments") = commitments.try_into().unwrap(); let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref()) .expect("should create new payload request"); diff --git a/beacon_node/execution_layer/src/engines.rs b/beacon_node/execution_layer/src/engines.rs index c46a94c5af..cc2bfcc7b6 100644 --- a/beacon_node/execution_layer/src/engines.rs +++ b/beacon_node/execution_layer/src/engines.rs @@ -11,11 +11,11 @@ use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; -use tokio::sync::{watch, Mutex, RwLock}; +use tokio::sync::{Mutex, RwLock, watch}; use tokio_stream::wrappers::WatchStream; use tracing::{debug, error, info, warn}; -use types::non_zero_usize::new_non_zero_usize; use types::ExecutionBlockHash; +use types::non_zero_usize::new_non_zero_usize; /// The number of payload IDs that will be stored for each `Engine`. /// diff --git a/beacon_node/execution_layer/src/keccak.rs b/beacon_node/execution_layer/src/keccak.rs index 62e354d503..609f766886 100644 --- a/beacon_node/execution_layer/src/keccak.rs +++ b/beacon_node/execution_layer/src/keccak.rs @@ -11,8 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use hash256_std_hasher::Hash256StdHasher; use hash_db::Hasher; +use hash256_std_hasher::Hash256StdHasher; use types::Hash256; pub fn keccak256(bytes: &[u8]) -> Hash256 { diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index e0a2efcc56..5a20be5dfe 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -7,28 +7,29 @@ use crate::json_structures::{BlobAndProofV1, BlobAndProofV2}; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; -use auth::{strip_prefix, Auth, JwtKey}; +use auth::{Auth, JwtKey, strip_prefix}; pub use block_hash::calculate_execution_block_hash; +use bls::{PublicKeyBytes, Signature}; use builder_client::BuilderHttpClient; pub use engine_api::EngineCapabilities; use engine_api::Error as ApiError; pub use engine_api::*; -pub use engine_api::{http, http::deposit_methods, http::HttpJsonRpc}; +pub use engine_api::{http, http::HttpJsonRpc, http::deposit_methods}; use engines::{Engine, EngineError}; pub use engines::{EngineState, ForkchoiceState}; -use eth2::types::{builder_bid::SignedBuilderBid, ForkVersionedResponse}; use eth2::types::{BlobsBundle, FullPayloadContents}; -use ethers_core::types::Transaction as EthersTransaction; +use eth2::types::{ForkVersionedResponse, builder_bid::SignedBuilderBid}; use fixed_bytes::UintExtended; use fork_choice::ForkchoiceUpdateParameters; use logging::crit; use lru::LruCache; -use payload_status::process_payload_status; pub use payload_status::PayloadStatus; +use payload_status::process_payload_status; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; -use std::collections::{hash_map::Entry, HashMap}; +use ssz_types::VariableList; +use std::collections::{HashMap, hash_map::Entry}; use std::fmt; use std::future::Future; use std::io::Write; @@ -43,7 +44,7 @@ use tokio::{ time::sleep, }; use tokio_stream::wrappers::WatchStream; -use tracing::{debug, error, info, warn}; +use tracing::{Instrument, debug, debug_span, error, info, instrument, warn}; use tree_hash::TreeHash; use types::beacon_block_body::KzgCommitments; use types::builder_bid::BuilderBid; @@ -56,7 +57,7 @@ use types::{ use types::{ BeaconStateError, BlindedPayload, ChainSpec, Epoch, ExecPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadEip7805, ExecutionPayloadElectra, - ExecutionPayloadFulu, FullPayload, ProposerPreparationData, PublicKeyBytes, Signature, Slot, + ExecutionPayloadFulu, FullPayload, ProposerPreparationData, Slot, }; mod block_hash; @@ -136,8 +137,7 @@ impl TryFrom> for ProvenancedPayload { + V1(Box>), + V2, +} + type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle>); struct Inner { @@ -757,18 +763,18 @@ impl ExecutionLayer { /// Returns the `Self::is_synced` response if unable to get latest block. pub async fn is_synced_for_notifier(&self, current_slot: Slot) -> bool { let synced = self.is_synced().await; - if synced { - if let Ok(Some(block)) = self + if synced + && let Ok(Some(block)) = self .engine() .api .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await - { - if block.block_number == 0 && current_slot > 0 { - return false; - } - } + && block.block_number == 0 + && current_slot > 0 + { + return false; } + synced } @@ -861,6 +867,7 @@ impl ExecutionLayer { } /// Returns the fee-recipient address that should be used to build a block + #[instrument(level = "debug", skip_all)] pub async fn get_suggested_fee_recipient(&self, proposer_index: u64) -> Address { if let Some(preparation_data_entry) = self.proposer_preparation_data().await.get(&proposer_index) @@ -885,6 +892,7 @@ impl ExecutionLayer { } } + #[instrument(level = "debug", skip_all)] pub async fn get_proposer_gas_limit(&self, proposer_index: u64) -> Option { self.proposer_preparation_data() .await @@ -901,6 +909,7 @@ impl ExecutionLayer { /// /// The result will be returned from the first node that returns successfully. No more nodes /// will be contacted. + #[instrument(level = "debug", skip_all)] pub async fn get_payload( &self, payload_parameters: PayloadParameters<'_>, @@ -1006,6 +1015,7 @@ impl ExecutionLayer { timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async { builder .get_builder_header::(slot, parent_hash, pubkey) + .instrument(debug_span!("get_builder_header")) .await }), timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { @@ -1247,6 +1257,7 @@ impl ExecutionLayer { .await } + #[instrument(level = "debug", skip_all)] async fn get_full_payload_with( &self, payload_parameters: PayloadParameters<'_>, @@ -1366,6 +1377,7 @@ impl ExecutionLayer { } /// Maps to the `engine_newPayload` JSON-RPC call. + /// TODO(EIP-7732) figure out how and why Mark relaxed new_payload_request param's typ to NewPayloadRequest pub async fn notify_new_payload( &self, new_payload_request: NewPayloadRequest<'_, E>, @@ -1497,17 +1509,17 @@ impl ExecutionLayer { let payload_attributes = self.payload_attributes(next_slot, head_block_root).await; // Compute the "lookahead", the time between when the payload will be produced and now. - if let Some(ref payload_attributes) = payload_attributes { - if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) { - let timestamp = Duration::from_secs(payload_attributes.timestamp()); - if let Some(lookahead) = timestamp.checked_sub(now) { - metrics::observe_duration( - &metrics::EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD, - lookahead, - ); - } else { - debug!(?timestamp, ?now, "Late payload attributes") - } + if let Some(ref payload_attributes) = payload_attributes + && let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) + { + let timestamp = Duration::from_secs(payload_attributes.timestamp()); + if let Some(lookahead) = timestamp.checked_sub(now) { + metrics::observe_duration( + &metrics::EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD, + lookahead, + ); + } else { + debug!(?timestamp, ?now, "Late payload attributes") } } @@ -1577,10 +1589,14 @@ impl ExecutionLayer { &self, age_limit: Option, ) -> Result, Error> { - self.engine() + let versions = self + .engine() .request(|engine| engine.get_engine_version(age_limit)) .await - .map_err(Into::into) + .map_err(Into::::into)?; + metrics::expose_execution_layer_info(&versions); + + Ok(versions) } /// Used during block production to determine if the merge has been triggered. @@ -1731,14 +1747,13 @@ impl ExecutionLayer { self.engine() .request(|engine| async move { - if let Some(pow_block) = self.get_pow_block(engine, block_hash).await? { - if let Some(pow_parent) = + if let Some(pow_block) = self.get_pow_block(engine, block_hash).await? + && let Some(pow_parent) = self.get_pow_block(engine, pow_block.parent_hash).await? - { - return Ok(Some( - self.is_valid_terminal_pow_block(pow_block, pow_parent, spec), - )); - } + { + return Ok(Some( + self.is_valid_terminal_pow_block(pow_block, pow_parent, spec), + )); } Ok(None) }) @@ -1839,6 +1854,9 @@ impl ExecutionLayer { ForkName::Base | ForkName::Altair => { return Err(Error::InvalidForkForPayload); } + ForkName::Gloas => { + return Err(Error::InvalidForkForPayload); + } }; return Ok(Some(payload)); } @@ -1884,7 +1902,7 @@ impl ExecutionLayer { pub async fn get_blobs_v2( &self, query: Vec, - ) -> Result>>, Error> { + ) -> Result>>, Error> { let capabilities = self.get_engine_capabilities(None).await?; if capabilities.get_blobs_v2 { @@ -1913,9 +1931,35 @@ impl ExecutionLayer { &self, block_root: Hash256, block: &SignedBlindedBeaconBlock, - ) -> Result, Error> { + spec: &ChainSpec, + ) -> Result, Error> { debug!(?block_root, "Sending block to builder"); + if spec.is_fulu_scheduled() { + let resp = self + .post_builder_blinded_blocks_v2(block_root, block) + .await + .map(|()| SubmitBlindedBlockResponse::V2); + // Fallback to v1 if v2 fails because the relay doesn't support it. + // Note: we should remove the fallback post fulu when all relays have support for v2. + if resp.is_err() { + self.post_builder_blinded_blocks_v1(block_root, block) + .await + .map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload))) + } else { + resp + } + } else { + self.post_builder_blinded_blocks_v1(block_root, block) + .await + .map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload))) + } + } + async fn post_builder_blinded_blocks_v1( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { @@ -1923,16 +1967,16 @@ impl ExecutionLayer { debug!( ?block_root, ssz = ssz_enabled, - "Calling submit_blinded_block on builder" + "Calling submit_blinded_block v1 on builder" ); if ssz_enabled { builder - .post_builder_blinded_blocks_ssz(block) + .post_builder_blinded_blocks_v1_ssz(block) .await .map_err(Error::Builder) } else { builder - .post_builder_blinded_blocks(block) + .post_builder_blinded_blocks_v1(block) .await .map_err(Error::Builder) .map(|d| d.data) @@ -1994,14 +2038,75 @@ impl ExecutionLayer { let Some(raw_transactions) = raw_transactions else { debug!(%parent_hash, "The EL sent an empty inclusion list"); - return Ok(transactions.into()); + return Ok(transactions.try_into()?); }; for raw_tx in raw_transactions { let decoded_hex_tx = VariableList::new(hex::decode(raw_tx.strip_prefix("0x").unwrap_or(&raw_tx))?)?; transactions.push(decoded_hex_tx); } - Ok(transactions.into()) + Ok(transactions.try_into()?) + } + + async fn post_builder_blinded_blocks_v2( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + if let Some(builder) = self.builder() { + let (result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { + let ssz_enabled = builder.is_ssz_available(); + debug!( + ?block_root, + ssz = ssz_enabled, + "Calling submit_blinded_block v2 on builder" + ); + if ssz_enabled { + builder + .post_builder_blinded_blocks_v2_ssz(block) + .await + .map_err(Error::Builder) + } else { + builder + .post_builder_blinded_blocks_v2(block) + .await + .map_err(Error::Builder) + } + }) + .await; + + match result { + Ok(()) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + info!( + relay_response_ms = duration.as_millis(), + ?block_root, + "Successfully submitted blinded block to the builder" + ); + + Ok(()) + } + Err(e) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::FAILURE], + ); + error!( + info = "this may result in a missed block proposal", + error = ?e, + relay_response_ms = duration.as_millis(), + ?block_root, + "Failed to submit blinded block to the builder" + ); + Err(e) + } + } + } else { + Err(Error::NoPayloadBuilder) + } } } @@ -2040,6 +2145,7 @@ enum InvalidBuilderPayload { payload: u64, expected: u64, }, + SszTypesError(ssz_types::Error), } impl fmt::Display for InvalidBuilderPayload { @@ -2081,6 +2187,7 @@ impl fmt::Display for InvalidBuilderPayload { InvalidBuilderPayload::GasLimitMismatch { payload, expected } => { write!(f, "payload gas limit was {} not {}", payload, expected) } + Self::SszTypesError(e) => write!(f, "{:?}", e), } } } @@ -2136,7 +2243,13 @@ fn verify_builder_bid( .withdrawals() .ok() .cloned() - .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); + .map(|withdrawals| { + Withdrawals::::try_from(withdrawals) + .map_err(InvalidBuilderPayload::SszTypesError) + .map(|w| w.tree_hash_root()) + }) + .transpose()?; + let payload_withdrawals_root = header.withdrawals_root().ok(); let expected_gas_limit = proposer_gas_limit .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit, target_gas_limit, spec)); @@ -2263,12 +2376,13 @@ mod test { let (mock, block_hash) = MockExecutionLayer::default_params(runtime.task_executor.clone()) .move_to_terminal_block() .produce_forked_pow_block(); - assert!(mock - .el - .is_valid_terminal_pow_block_hash(block_hash, &mock.spec) - .await - .unwrap() - .unwrap()); + assert!( + mock.el + .is_valid_terminal_pow_block_hash(block_hash, &mock.spec) + .await + .unwrap() + .unwrap() + ); } #[tokio::test] diff --git a/beacon_node/execution_layer/src/metrics.rs b/beacon_node/execution_layer/src/metrics.rs index ab1a22677f..859f33bc81 100644 --- a/beacon_node/execution_layer/src/metrics.rs +++ b/beacon_node/execution_layer/src/metrics.rs @@ -41,17 +41,17 @@ pub static EXECUTION_LAYER_REQUEST_TIMES: LazyLock> = LazyL pub static EXECUTION_LAYER_PAYLOAD_ATTRIBUTES_LOOKAHEAD: LazyLock> = LazyLock::new(|| { try_create_histogram( - "execution_layer_payload_attributes_lookahead", - "Duration between an fcU call with PayloadAttributes and when the block should be produced", - ) + "execution_layer_payload_attributes_lookahead", + "Duration between an fcU call with PayloadAttributes and when the block should be produced", + ) }); pub static EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID: LazyLock> = LazyLock::new( || { try_create_int_counter_vec( - "execution_layer_pre_prepared_payload_id", - "Indicates hits or misses for already having prepared a payload id before payload production", - &["event"] - ) + "execution_layer_pre_prepared_payload_id", + "Indicates hits or misses for already having prepared a payload id before payload production", + &["event"], + ) }, ); pub static EXECUTION_LAYER_GET_PAYLOAD_BODIES_BY_RANGE: LazyLock> = @@ -113,6 +113,32 @@ pub static EXECUTION_LAYER_PAYLOAD_BIDS: LazyLock> = LazyLoc try_create_int_gauge_vec( "execution_layer_payload_bids", "The gwei bid value of payloads received by local EEs or builders. Only shows values up to i64::MAX.", - &["source"] + &["source"], ) }); +pub static EXECUTION_LAYER_INFO: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "execution_layer_info", + "The build of the execution layer connected to lighthouse", + &["code", "name", "version", "commit"], + ) +}); + +pub fn reset_execution_layer_info_gauge() { + let _ = EXECUTION_LAYER_INFO.as_ref().map(|gauge| gauge.reset()); +} + +pub fn expose_execution_layer_info(els: &Vec) { + for el in els { + set_gauge_vec( + &EXECUTION_LAYER_INFO, + &[ + &el.code.to_string(), + &el.name, + &el.version, + &el.commit.to_string(), + ], + 1, + ); + } +} diff --git a/beacon_node/execution_layer/src/payload_status.rs b/beacon_node/execution_layer/src/payload_status.rs index bbfd30239d..efe7d2cf91 100644 --- a/beacon_node/execution_layer/src/payload_status.rs +++ b/beacon_node/execution_layer/src/payload_status.rs @@ -44,8 +44,7 @@ pub fn process_payload_status( } else { let error = format!( "new_payload: response.status = VALID but invalid latest_valid_hash. Expected({:?}) Found({:?})", - head_block_hash, - response.latest_valid_hash + head_block_hash, response.latest_valid_hash ); Err(EngineError::Api { error: ApiError::BadResponse(error), diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index e3b6279e18..3608cb7472 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -1,18 +1,21 @@ use crate::engine_api::{ + ExecutionBlock, PayloadAttributes, PayloadId, PayloadStatusV1, PayloadStatusV1Status, json_structures::{ JsonForkchoiceUpdatedV1Response, JsonPayloadStatusV1, JsonPayloadStatusV1Status, }, - ExecutionBlock, PayloadAttributes, PayloadId, PayloadStatusV1, PayloadStatusV1Status, }; use crate::engines::ForkchoiceState; -use crate::EthersTransaction; +use alloy_consensus::TxEnvelope; +use alloy_rpc_types_eth::Transaction as AlloyTransaction; use eth2::types::BlobsBundle; +use fixed_bytes::FixedBytesExtended; use kzg::{Kzg, KzgCommitment, KzgProof}; use parking_lot::Mutex; -use rand::{rngs::StdRng, Rng, SeedableRng}; +use rand::{Rng, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; use ssz::Decode; use ssz_types::VariableList; +use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; use tree_hash::TreeHash; @@ -20,7 +23,7 @@ use tree_hash_derive::TreeHash; use types::{ Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadEip7805, - ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadHeader, FixedBytesExtended, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeader, ForkName, Hash256, KzgProofs, Transaction, Transactions, Uint256, }; @@ -29,7 +32,7 @@ use super::DEFAULT_TERMINAL_BLOCK; const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); const TEST_BLOB_BUNDLE_V2: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle_v2.ssz"); -pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; const GAS_USED: u64 = DEFAULT_GAS_LIMIT - 1; #[derive(Clone, Debug, PartialEq)] @@ -39,8 +42,8 @@ pub enum Block { PoS(ExecutionPayload), } -pub fn mock_el_extra_data() -> types::VariableList { - "block gen was here".as_bytes().to_vec().into() +pub fn mock_el_extra_data() -> VariableList { + "block gen was here".as_bytes().to_vec().try_into().unwrap() } impl Block { @@ -142,21 +145,22 @@ pub struct ExecutionBlockGenerator { pub pending_payloads: HashMap>, pub next_payload_id: u64, pub payload_ids: HashMap>, + min_blobs_count: usize, /* * Post-merge fork triggers */ - pub shanghai_time: Option, // capella - pub cancun_time: Option, // deneb - pub prague_time: Option, // electra - pub eip7805_time: Option, // eip7805 - pub osaka_time: Option, // fulu + pub shanghai_time: Option, // capella + pub cancun_time: Option, // deneb + pub prague_time: Option, // electra + pub osaka_time: Option, // fulu + pub eip7805_time: Option, // eip7805 + pub amsterdam_time: Option, // gloas /* * deneb stuff */ pub blobs_bundles: HashMap>, pub kzg: Option>, rng: Arc>, - spec: Arc, } fn make_rng() -> Arc> { @@ -176,10 +180,10 @@ impl ExecutionBlockGenerator { prague_time: Option, eip7805_time: Option, osaka_time: Option, - spec: Arc, + amsterdam_time: Option, kzg: Option>, ) -> Self { - let mut gen = Self { + let mut generator = Self { head_block: <_>::default(), finalized_block_hash: <_>::default(), blocks: <_>::default(), @@ -190,20 +194,21 @@ impl ExecutionBlockGenerator { pending_payloads: <_>::default(), next_payload_id: 0, payload_ids: <_>::default(), + min_blobs_count: 0, shanghai_time, cancun_time, prague_time, eip7805_time, osaka_time, + amsterdam_time, blobs_bundles: <_>::default(), kzg, rng: make_rng(), - spec, }; - gen.insert_pow_block(0).unwrap(); + generator.insert_pow_block(0).unwrap(); - gen + generator } pub fn latest_block(&self) -> Option> { @@ -244,22 +249,24 @@ impl ExecutionBlockGenerator { } pub fn get_fork_at_timestamp(&self, timestamp: u64) -> ForkName { - match self.osaka_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Fulu, - _ => match self.eip7805_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Eip7805, - _ => match self.prague_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Electra, - _ => match self.cancun_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Deneb, - _ => match self.shanghai_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Capella, - _ => ForkName::Bellatrix, - }, - }, - }, - }, + let forks = [ + (self.amsterdam_time, ForkName::Gloas), + (self.eip7805_time, ForkName::Eip7805), + (self.osaka_time, ForkName::Fulu), + (self.prague_time, ForkName::Electra), + (self.cancun_time, ForkName::Deneb), + (self.shanghai_time, ForkName::Capella), + ]; + + for (fork_time, fork_name) in forks { + if let Some(time) = fork_time + && timestamp >= time + { + return fork_name; + } } + + ForkName::Bellatrix } pub fn execution_block_by_number(&self, number: u64) -> Option { @@ -324,6 +331,10 @@ impl ExecutionBlockGenerator { Ok(()) } + pub fn set_min_blob_count(&mut self, count: usize) { + self.min_blobs_count = count; + } + pub fn insert_pow_block(&mut self, block_number: u64) -> Result<(), String> { if let Some(finalized_block_hash) = self.finalized_block_hash { return Err(format!( @@ -509,10 +520,10 @@ impl ExecutionBlockGenerator { // This is meant to cover starting post-merge transition at genesis. Useful for // testing Capella forks and later. let head_block_hash = forkchoice_state.head_block_hash; - if let Some(genesis_pow_block) = self.block_by_number(0) { - if genesis_pow_block.block_hash() == head_block_hash { - self.terminal_block_hash = head_block_hash; - } + if let Some(genesis_pow_block) = self.block_by_number(0) + && genesis_pow_block.block_hash() == head_block_hash + { + self.terminal_block_hash = head_block_hash; } if let Some(payload) = self.pending_payloads.remove(&head_block_hash) { @@ -595,7 +606,7 @@ impl ExecutionBlockGenerator { fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, @@ -604,7 +615,7 @@ impl ExecutionBlockGenerator { extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), }), PayloadAttributes::V2(pa) => match self.get_fork_at_timestamp(pa.timestamp) { ForkName::Bellatrix => ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix { @@ -612,7 +623,7 @@ impl ExecutionBlockGenerator { fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, @@ -621,14 +632,14 @@ impl ExecutionBlockGenerator { extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), + transactions: vec![].try_into().unwrap(), }), ForkName::Capella => ExecutionPayload::Capella(ExecutionPayloadCapella { parent_hash: head_block_hash, fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, @@ -637,8 +648,8 @@ impl ExecutionBlockGenerator { extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), }), _ => unreachable!(), }, @@ -648,7 +659,7 @@ impl ExecutionBlockGenerator { fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, @@ -657,8 +668,8 @@ impl ExecutionBlockGenerator { extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), blob_gas_used: 0, excess_blob_gas: 0, }), @@ -667,7 +678,7 @@ impl ExecutionBlockGenerator { fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, @@ -676,27 +687,8 @@ impl ExecutionBlockGenerator { extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), - blob_gas_used: 0, - excess_blob_gas: 0, - }), - ForkName::Eip7805 => ExecutionPayload::Eip7805(ExecutionPayloadEip7805 { - parent_hash: head_block_hash, - fee_recipient: pa.suggested_fee_recipient, - receipts_root: Hash256::repeat_byte(42), - state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), - prev_randao: pa.prev_randao, - block_number: parent.block_number() + 1, - gas_limit: DEFAULT_GAS_LIMIT, - gas_used: GAS_USED, - timestamp: pa.timestamp, - extra_data: mock_el_extra_data::(), - base_fee_per_gas: Uint256::from(1u64), - block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), blob_gas_used: 0, excess_blob_gas: 0, }), @@ -705,17 +697,55 @@ impl ExecutionBlockGenerator { fee_recipient: pa.suggested_fee_recipient, receipts_root: Hash256::repeat_byte(42), state_root: Hash256::repeat_byte(43), - logs_bloom: vec![0; 256].into(), + logs_bloom: vec![0; 256].try_into().unwrap(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: "block gen was here".as_bytes().to_vec().try_into().unwrap(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), - transactions: vec![].into(), - withdrawals: pa.withdrawals.clone().into(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + ForkName::Eip7805 => ExecutionPayload::Eip7805(ExecutionPayloadEip7805 { + parent_hash: head_block_hash, + fee_recipient: pa.suggested_fee_recipient, + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].try_into().unwrap(), + prev_randao: pa.prev_randao, + block_number: parent.block_number() + 1, + gas_limit: DEFAULT_GAS_LIMIT, + gas_used: GAS_USED, + timestamp: pa.timestamp, + extra_data: mock_el_extra_data::(), + base_fee_per_gas: Uint256::from(1u64), + block_hash: ExecutionBlockHash::zero(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + ForkName::Gloas => ExecutionPayload::Gloas(ExecutionPayloadGloas { + parent_hash: head_block_hash, + fee_recipient: pa.suggested_fee_recipient, + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].try_into().unwrap(), + prev_randao: pa.prev_randao, + block_number: parent.block_number() + 1, + gas_limit: DEFAULT_GAS_LIMIT, + gas_used: GAS_USED, + timestamp: pa.timestamp, + extra_data: "block gen was here".as_bytes().to_vec().try_into().unwrap(), + base_fee_per_gas: Uint256::from(1u64), + block_hash: ExecutionBlockHash::zero(), + transactions: vec![].try_into().unwrap(), + withdrawals: pa.withdrawals.clone().try_into().unwrap(), blob_gas_used: 0, excess_blob_gas: 0, }), @@ -725,10 +755,11 @@ impl ExecutionBlockGenerator { let fork_name = execution_payload.fork_name(); if fork_name.deneb_enabled() { - // get random number between 0 and Max Blobs + // get random number between 0 and 1 blobs by default + // For tests that need higher blob count, consider adding a `set_max_blob_count` method let mut rng = self.rng.lock(); - let max_blobs = self.spec.max_blobs_per_block_by_fork(fork_name) as usize; - let num_blobs = rng.gen::() % (max_blobs + 1); + let max_blobs = max(1, self.min_blobs_count); + let num_blobs = rng.random_range(self.min_blobs_count..=max_blobs); let (bundle, transactions) = generate_blobs(num_blobs, fork_name)?; for tx in Vec::from(transactions) { execution_payload @@ -770,8 +801,8 @@ pub fn load_test_blobs_bundle_v1() -> Result<(KzgCommitment, KzgProo )) } -pub fn load_test_blobs_bundle_v2( -) -> Result<(KzgCommitment, KzgProofs, Blob), String> { +pub fn load_test_blobs_bundle_v2() +-> Result<(KzgCommitment, KzgProofs, Blob), String> { let BlobsBundle:: { commitments, proofs, @@ -804,29 +835,30 @@ pub fn generate_blobs( let bundle = if fork_name.fulu_enabled() { let (kzg_commitment, kzg_proofs, blob) = load_test_blobs_bundle_v2::()?; BlobsBundle { - commitments: vec![kzg_commitment; n_blobs].into(), + commitments: vec![kzg_commitment; n_blobs].try_into().unwrap(), proofs: vec![kzg_proofs.to_vec(); n_blobs] .into_iter() .flatten() .collect::>() - .into(), - blobs: vec![blob; n_blobs].into(), + .try_into() + .unwrap(), + blobs: vec![blob; n_blobs].try_into().unwrap(), } } else { let (kzg_commitment, kzg_proof, blob) = load_test_blobs_bundle_v1::()?; BlobsBundle { - commitments: vec![kzg_commitment; n_blobs].into(), - proofs: vec![kzg_proof; n_blobs].into(), - blobs: vec![blob; n_blobs].into(), + commitments: vec![kzg_commitment; n_blobs].try_into().unwrap(), + proofs: vec![kzg_proof; n_blobs].try_into().unwrap(), + blobs: vec![blob; n_blobs].try_into().unwrap(), } }; - Ok((bundle, transactions.into())) + Ok((bundle, transactions.try_into().unwrap())) } pub fn static_valid_tx() -> Result, String> { // This is a real transaction hex encoded, but we don't care about the contents of the transaction. - let transaction: EthersTransaction = serde_json::from_str( + let transaction: AlloyTransaction = serde_json::from_str( r#"{ "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", "blockNumber":"0x5daf3b", @@ -845,7 +877,8 @@ pub fn static_valid_tx() -> Result(transaction.into()).to_vec()) .map_err(|e| format!("Failed to convert transaction to SSZ: {:?}", e)) } @@ -905,6 +938,8 @@ pub fn generate_genesis_header( *header.transactions_root_mut() = empty_transactions_root; Some(header) } + // TODO(EIP-7732): need to look into this + ForkName::Gloas => None, } } @@ -960,7 +995,7 @@ pub fn generate_pow_block( #[cfg(test)] mod test { use super::*; - use kzg::{trusted_setup::get_trusted_setup, Bytes48, CellRef, KzgBlobRef, TrustedSetup}; + use kzg::{Bytes48, CellRef, KzgBlobRef, trusted_setup::get_trusted_setup}; use types::{MainnetEthSpec, MinimalEthSpec}; #[test] @@ -968,7 +1003,6 @@ mod test { const TERMINAL_DIFFICULTY: u64 = 10; const TERMINAL_BLOCK: u64 = 10; const DIFFICULTY_INCREMENT: u64 = 1; - let spec = Arc::new(MainnetEthSpec::default_spec()); let mut generator: ExecutionBlockGenerator = ExecutionBlockGenerator::new( Uint256::from(TERMINAL_DIFFICULTY), @@ -979,7 +1013,7 @@ mod test { None, None, None, - spec, + None, None, ); @@ -1080,10 +1114,7 @@ mod test { } fn load_kzg() -> Result { - let trusted_setup: TrustedSetup = - serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| format!("Unable to read trusted setup file: {e:?}"))?; - Kzg::new_from_trusted_setup(trusted_setup) + Kzg::new_from_trusted_setup(&get_trusted_setup()) .map_err(|e| format!("Failed to load trusted setup: {e:?}")) } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 24c83f3cf9..2bc4599582 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -2,11 +2,10 @@ use super::Context; use crate::engine_api::{http::*, *}; use crate::json_structures::*; use crate::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI}; -use crate::EthersTransaction; -use crate::Transactions; -use serde::{de::DeserializeOwned, Deserialize}; +use serde::{Deserialize, de::DeserializeOwned}; use serde_json::Value as JsonValue; use std::sync::Arc; +use types::Transaction; pub const GENERIC_ERROR_CODE: i64 = -1234; pub const BAD_PARAMS_ERROR_CODE: i64 = -32602; @@ -101,35 +100,37 @@ pub async fn handle_rpc( ENGINE_NEW_PAYLOAD_V1 | ENGINE_NEW_PAYLOAD_V2 | ENGINE_NEW_PAYLOAD_V3 - | ENGINE_NEW_PAYLOAD_V4 - | ENGINE_NEW_PAYLOAD_V5 => { + | ENGINE_NEW_PAYLOAD_V4 => { let request = match method { - ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::V1( - get_param::>(params, 0) + ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::Bellatrix( + get_param::>(params, 0) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, ), - ENGINE_NEW_PAYLOAD_V2 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V2(jep)) + ENGINE_NEW_PAYLOAD_V2 => get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Capella(jep)) .or_else(|_| { - get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V1(jep)) + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Bellatrix(jep)) }) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, - // From v3 onwards, we use the newPayload version only for the corresponding - // ExecutionPayload version. So we return an error instead of falling back to - // older versions of newPayload - ENGINE_NEW_PAYLOAD_V3 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V3(jep)) + ENGINE_NEW_PAYLOAD_V3 => get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Deneb(jep)) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, - ENGINE_NEW_PAYLOAD_V4 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V4(jep)) - .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, - ENGINE_NEW_PAYLOAD_V5 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V5(jep)) - .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, - ENGINE_NEW_PAYLOAD_V5 => get_param::>(params, 0) - .map(|jep| JsonExecutionPayload::V5(jep)) + ENGINE_NEW_PAYLOAD_V4 => get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Gloas(jep)) + .or_else(|_| { + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Fulu(jep)) + }) + .or_else(|_| { + get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::Electra(jep)) + }) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, + // TODO(EIP7805) fix + // ENGINE_NEW_PAYLOAD_V5 => get_param::>(params, 0) + // .map(|jep| JsonExecutionPayload::V5(jep)) + // .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, _ => unreachable!(), }; @@ -140,10 +141,10 @@ pub async fn handle_rpc( // validate method called correctly according to fork time match fork { ForkName::Bellatrix => { - if matches!(request, JsonExecutionPayload::V2(_)) { + if matches!(request, JsonExecutionPayload::Capella(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV2` before Capella fork!", + "{} called with `ExecutionPayloadCapella` before Capella fork!", method ), GENERIC_ERROR_CODE, @@ -157,10 +158,10 @@ pub async fn handle_rpc( GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V1(_)) { + if matches!(request, JsonExecutionPayload::Bellatrix(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV1` after Capella fork!", + "{} called with `ExecutionPayloadBellatrix` after Capella fork!", method ), GENERIC_ERROR_CODE, @@ -174,7 +175,7 @@ pub async fn handle_rpc( GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V1(_)) { + if matches!(request, JsonExecutionPayload::Bellatrix(_)) { return Err(( format!( "{} called with `ExecutionPayloadV1` after Deneb fork!", @@ -183,7 +184,7 @@ pub async fn handle_rpc( GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V2(_)) { + if matches!(request, JsonExecutionPayload::Capella(_)) { return Err(( format!( "{} called with `ExecutionPayloadV2` after Deneb fork!", @@ -193,7 +194,7 @@ pub async fn handle_rpc( )); } } - ForkName::Electra => { + ForkName::Electra | ForkName::Fulu | ForkName::Eip7805 | ForkName::Gloas => { if method == ENGINE_NEW_PAYLOAD_V1 || method == ENGINE_NEW_PAYLOAD_V2 || method == ENGINE_NEW_PAYLOAD_V3 @@ -203,122 +204,34 @@ pub async fn handle_rpc( GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V1(_)) { + if matches!(request, JsonExecutionPayload::Bellatrix(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV1` after Electra fork!", + "{} called with `ExecutionPayloadBellatrix after Electra fork!", method ), GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V2(_)) { + if matches!(request, JsonExecutionPayload::Capella(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV2` after Electra fork!", + "{} called with `ExecutionPayloadCapella` after Electra fork!", method ), GENERIC_ERROR_CODE, )); } - if matches!(request, JsonExecutionPayload::V3(_)) { + if matches!(request, JsonExecutionPayload::Deneb(_)) { return Err(( format!( - "{} called with `ExecutionPayloadV3` after Electra fork!", + "{} called with `ExecutionPayloadDeneb` after Electra fork!", method ), GENERIC_ERROR_CODE, )); } } - ForkName::Eip7805 => { - if method == ENGINE_NEW_PAYLOAD_V1 - || method == ENGINE_NEW_PAYLOAD_V2 - || method == ENGINE_NEW_PAYLOAD_V3 - { - return Err(( - format!("{} called after Electra fork!", method), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V1(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV1` after Electra fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V2(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV2` after Eip7805 fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V3(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV3` after Eip7805 fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - } - ForkName::Fulu => { - if method == ENGINE_NEW_PAYLOAD_V1 - || method == ENGINE_NEW_PAYLOAD_V2 - || method == ENGINE_NEW_PAYLOAD_V3 - // TODO(fulu): Uncomment this once v5 method is ready for Fulu - // || method == ENGINE_NEW_PAYLOAD_V4 - { - return Err(( - format!("{} called after Fulu fork!", method), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V1(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV1` after Fulu fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V2(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV2` after Fulu fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - if matches!(request, JsonExecutionPayload::V3(_)) { - return Err(( - format!( - "{} called with `ExecutionPayloadV3` after Fulu fork!", - method - ), - GENERIC_ERROR_CODE, - )); - } - // TODO(fulu): remove once we switch to v5 - // if matches!(request, JsonExecutionPayload::V4(_)) { - // return Err(( - // format!( - // "{} called with `ExecutionPayloadV4` after Fulu fork!", - // method - // ), - // GENERIC_ERROR_CODE, - // )); - // } - } _ => unreachable!(), }; @@ -344,7 +257,7 @@ pub async fn handle_rpc( Some( ctx.execution_block_generator .write() - .new_payload(request.into()), + .new_payload(request.try_into().unwrap()), ) } else { None @@ -417,23 +330,7 @@ pub async fn handle_rpc( )); } - // validate method called correctly according to prague fork time - if ctx - .execution_block_generator - .read() - .get_fork_at_timestamp(response.timestamp()) - == ForkName::Eip7805 - && (method == ENGINE_GET_PAYLOAD_V1 - || method == ENGINE_GET_PAYLOAD_V2 - || method == ENGINE_GET_PAYLOAD_V3) - { - return Err(( - format!("{} called after EIP7805 fork!", method), - FORK_REQUEST_MISMATCH_ERROR_CODE, - )); - } - - // validate method called correctly according to fulu fork time + // validate method called correctly according to osaka fork time if ctx .execution_block_generator .read() @@ -449,98 +346,141 @@ pub async fn handle_rpc( )); } + // validate method called correctly according to amsterdam fork time + if ctx + .execution_block_generator + .read() + .get_fork_at_timestamp(response.timestamp()) + == ForkName::Gloas + && (method == ENGINE_GET_PAYLOAD_V1 + || method == ENGINE_GET_PAYLOAD_V2 + || method == ENGINE_GET_PAYLOAD_V3 + || method == ENGINE_GET_PAYLOAD_V4) + { + return Err(( + format!("{} called after Gloas fork!", method), + FORK_REQUEST_MISMATCH_ERROR_CODE, + )); + } + match method { - ENGINE_GET_PAYLOAD_V1 => { - Ok(serde_json::to_value(JsonExecutionPayload::from(response)).unwrap()) + ENGINE_GET_PAYLOAD_V1 => Ok(serde_json::to_value( + JsonExecutionPayload::try_from(response).unwrap(), + ) + .unwrap()), + ENGINE_GET_PAYLOAD_V2 => { + Ok(match JsonExecutionPayload::try_from(response).unwrap() { + JsonExecutionPayload::Bellatrix(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseBellatrix { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + }) + .unwrap() + } + JsonExecutionPayload::Capella(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseCapella { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + }) + .unwrap() + } + _ => unreachable!(), + }) } - ENGINE_GET_PAYLOAD_V2 => Ok(match JsonExecutionPayload::from(response) { - JsonExecutionPayload::V1(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV1 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - }) - .unwrap() - } - JsonExecutionPayload::V2(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV2 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - }) - .unwrap() - } - _ => unreachable!(), - }), // From v3 onwards, we use the getPayload version only for the corresponding // ExecutionPayload version. So we return an error if the ExecutionPayload version // we get does not correspond to the getPayload version. - ENGINE_GET_PAYLOAD_V3 => Ok(match JsonExecutionPayload::from(response) { - JsonExecutionPayload::V3(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV3 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - blobs_bundle: maybe_blobs - .ok_or(( - "No blobs returned despite V3 Payload".to_string(), - GENERIC_ERROR_CODE, - ))? - .into(), - should_override_builder: false, - }) - .unwrap() - } - _ => unreachable!(), - }), - ENGINE_GET_PAYLOAD_V4 => Ok(match JsonExecutionPayload::from(response) { - JsonExecutionPayload::V4(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV4 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - blobs_bundle: maybe_blobs - .ok_or(( - "No blobs returned despite V4 Payload".to_string(), - GENERIC_ERROR_CODE, - ))? - .into(), - should_override_builder: false, - // TODO(electra): add EL requests in mock el - execution_requests: Default::default(), - }) - .unwrap() - } - JsonExecutionPayload::V5(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV5 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - blobs_bundle: maybe_blobs - .ok_or(( - "No blobs returned despite V5 Payload".to_string(), - GENERIC_ERROR_CODE, - ))? - .into(), - should_override_builder: false, - // TODO(electra): add EL requests in mock el - execution_requests: Default::default(), - }) - .unwrap() - } - JsonExecutionPayload::V6(execution_payload) => { - serde_json::to_value(JsonGetPayloadResponseV6 { - execution_payload, - block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), - blobs_bundle: maybe_blobs - .ok_or(( - "No blobs returned despite V5 Payload".to_string(), - GENERIC_ERROR_CODE, - ))? - .into(), - should_override_builder: false, - // TODO(electra): add EL requests in mock el - execution_requests: Default::default(), - }) - .unwrap() - } - _ => unreachable!(), - }), + ENGINE_GET_PAYLOAD_V3 => { + Ok(match JsonExecutionPayload::try_from(response).unwrap() { + JsonExecutionPayload::Deneb(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseDeneb { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V3 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + }) + .unwrap() + } + _ => unreachable!(), + }) + } + ENGINE_GET_PAYLOAD_V4 => { + Ok(match JsonExecutionPayload::try_from(response).unwrap() { + JsonExecutionPayload::Electra(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseElectra { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V4 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + // TODO(electra): add EL requests in mock el + execution_requests: Default::default(), + }) + .unwrap() + } + _ => unreachable!(), + }) + } + /* TODO(EIP7805) new payload handling + ENGINE_GET_PAYLOAD_V5 => { + Ok(match JsonExecutionPayload::try_from(response).unwrap() { + JsonExecutionPayload::Fulu(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseFulu { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V5 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + execution_requests: Default::default(), + }) + .unwrap() + } + JsonExecutionPayload::Eip7805(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseEip7805 { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V5 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + execution_requests: Default::default(), + }) + .unwrap() + } + JsonExecutionPayload::Gloas(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseGloas { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V5 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + execution_requests: Default::default(), + }) + .unwrap() + } + _ => unreachable!(), + }) + }*/ _ => unreachable!(), } } @@ -575,7 +515,8 @@ pub async fn handle_rpc( ForkName::Capella | ForkName::Deneb | ForkName::Electra - | ForkName::Fulu => { + | ForkName::Fulu + | ForkName::Gloas => { get_param::>(params, 1) .map(|opt| opt.map(JsonPayloadAttributes::V2)) .transpose() @@ -639,7 +580,11 @@ pub async fn handle_rpc( )); } } - ForkName::Deneb | ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu + | ForkName::Eip7805 + | ForkName::Gloas => { if method == ENGINE_FORKCHOICE_UPDATED_V1 { return Err(( format!("{} called after Deneb fork!", method), @@ -731,7 +676,8 @@ pub async fn handle_rpc( transactions: payload.transactions().clone(), withdrawals: payload.withdrawals().ok().cloned(), }; - let json_payload_body = JsonExecutionPayloadBodyV1::from(payload_body); + let json_payload_body: JsonExecutionPayloadBodyV1 = + payload_body.try_into().unwrap(); response.push(Some(json_payload_body)); } None => response.push(None), @@ -742,8 +688,7 @@ pub async fn handle_rpc( } ENGINE_GET_INCLUSION_LIST_V1 => { // This is a real transaction hex encoded, but we don't care about the contents of the transaction. - let transaction: EthersTransaction = serde_json::from_str( - r#"{ + let transaction: Transaction<::MaxBytesPerTransaction> = serde_json::from_str(r#"{ "blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2", "blockNumber":"0x5daf3b", "from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d", @@ -761,7 +706,7 @@ pub async fn handle_rpc( }"#, ) .unwrap(); - let tx_list: Transactions = vec![transaction.rlp().to_vec().into()].into(); + let tx_list: Transactions = vec![transaction].try_into().unwrap(); Ok(serde_json::to_value(tx_list).unwrap()) } diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 3c62034744..53c484c474 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,19 +1,22 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; +use bls::{PublicKeyBytes, SecretKey, Signature}; use bytes::Bytes; +use eth2::beacon_response::ForkVersionedResponse; use eth2::types::PublishBlockRequest; use eth2::types::{ - BlobsBundle, BlockId, BroadcastValidation, EventKind, EventTopic, FullPayloadContents, - ProposerData, StateId, ValidatorId, + BlobsBundle, BlockId, BroadcastValidation, EndpointVersion, EventKind, EventTopic, + FullPayloadContents, ProposerData, StateId, ValidatorId, }; use eth2::{ - BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, + BeaconNodeHttpClient, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER, + Timeouts, }; use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use ssz::Encode; +use ssz_types::VariableList; use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; @@ -25,22 +28,21 @@ use tempfile::NamedTempFile; use tokio_stream::StreamExt; use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; +use types::ExecutionBlockHash; use types::builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidEip7805, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, }; use types::{ Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, - ForkVersionedResponse, Hash256, PublicKeyBytes, Signature, SignedBlindedBeaconBlock, - SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, Hash256, + SignedBlindedBeaconBlock, SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, }; -use types::{ExecutionBlockHash, SecretKey}; use warp::reply::{self, Reply}; use warp::{Filter, Rejection}; pub const DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); -pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; pub const DEFAULT_BUILDER_PRIVATE_KEY: &str = "607a11b45a7219cc61a3d9c5fd08c7eebd602a6a19a977f8d3771d5711a550f2"; @@ -71,8 +73,8 @@ impl Operation { } } -pub fn mock_builder_extra_data() -> types::VariableList { - "mock_builder".as_bytes().to_vec().into() +pub fn mock_builder_extra_data() -> VariableList { + "mock_builder".as_bytes().to_vec().try_into().unwrap() } #[derive(Debug)] @@ -332,6 +334,10 @@ pub struct MockBuilder { payload_id_cache: Arc>>, /// If set to `true`, sets the bid returned by `get_header` to Uint256::MAX max_bid: bool, + /// Broadcast the full block with payload to the attached beacon node (simulating the relay). + /// + /// Turning this off is useful for testing. + broadcast_to_bn: bool, /// A cache that stores the proposers index for a given epoch proposers_cache: Arc>>>, } @@ -340,6 +346,9 @@ impl MockBuilder { pub fn new_for_testing( mock_el_url: SensitiveUrl, beacon_url: SensitiveUrl, + validate_pubkey: bool, + apply_operations: bool, + broadcast_to_bn: bool, spec: Arc, executor: TaskExecutor, ) -> (Self, (SocketAddr, impl Future)) { @@ -357,12 +366,15 @@ impl MockBuilder { let el = ExecutionLayer::from_config(config, executor.clone()).unwrap(); + let max_bid = false; + let builder = MockBuilder::new( el, BeaconNodeHttpClient::new(beacon_url, Timeouts::set_all(Duration::from_secs(1))), - true, - true, - false, + validate_pubkey, + apply_operations, + broadcast_to_bn, + max_bid, spec, None, ); @@ -378,6 +390,7 @@ impl MockBuilder { beacon_client: BeaconNodeHttpClient, validate_pubkey: bool, apply_operations: bool, + broadcast_to_bn: bool, max_bid: bool, spec: Arc, sk: Option<&[u8]>, @@ -407,6 +420,7 @@ impl MockBuilder { proposers_cache: Arc::new(RwLock::new(HashMap::new())), apply_operations, max_bid, + broadcast_to_bn, genesis_time: None, } } @@ -485,15 +499,25 @@ impl MockBuilder { SignedBlindedBeaconBlock::Fulu(block) => { block.message.body.execution_payload.tree_hash_root() } + SignedBlindedBeaconBlock::Gloas(_) => { + // TODO(EIP7732) Check if this is how we want to do error handling for gloas + return Err("invalid fork".to_string()); + } }; + let block_hash = block + .message() + .body() + .execution_payload() + .unwrap() + .block_hash(); info!( - block_hash = %root, + execution_payload_root = %root, + ?block_hash, "Submitting blinded beacon block to builder" ); - let payload = self - .el - .get_payload_by_root(&root) - .ok_or_else(|| "missing payload for tx root".to_string())?; + let payload = self.el.get_payload_by_root(&root).ok_or_else(|| { + format!("missing payload for root: {root:?}, block_hash: {block_hash:?}",) + })?; let (payload, blobs) = payload.deconstruct(); let full_block = block @@ -502,16 +526,28 @@ impl MockBuilder { debug!( txs_count = payload.transactions().len(), blob_count = blobs.as_ref().map(|b| b.commitments.len()), - "Got full payload, sending to local beacon node for propagation" + "Got full payload" ); - let publish_block_request = PublishBlockRequest::new( - Arc::new(full_block), - blobs.clone().map(|b| (b.proofs, b.blobs)), - ); - self.beacon_client - .post_beacon_blocks_v2(&publish_block_request, Some(BroadcastValidation::Gossip)) - .await - .map_err(|e| format!("Failed to post blinded block {:?}", e))?; + if self.broadcast_to_bn { + debug!( + block_hash = ?payload.block_hash(), + "Broadcasting builder block to BN" + ); + let publish_block_request = PublishBlockRequest::new( + Arc::new(full_block), + blobs.clone().map(|b| (b.proofs, b.blobs)), + ); + self.beacon_client + .post_beacon_blocks_v2( + &publish_block_request, + Some(BroadcastValidation::ConsensusAndEquivocation), + ) + .await + .map_err(|e| { + // XXX: this should really be a 400 but warp makes that annoyingly difficult + format!("Failed to post blinded block {e:?}") + })?; + } Ok(FullPayloadContents::new(payload, blobs)) } @@ -542,16 +578,29 @@ impl MockBuilder { info!("Got payload params"); let fork = self.fork_name_at_slot(slot); + let payload_response_type = self .el - .get_full_payload_caching(PayloadParameters { - parent_hash: payload_parameters.parent_hash, - parent_gas_limit: payload_parameters.parent_gas_limit, - proposer_gas_limit: payload_parameters.proposer_gas_limit, - payload_attributes: &payload_parameters.payload_attributes, - forkchoice_update_params: &payload_parameters.forkchoice_update_params, - current_fork: payload_parameters.current_fork, - }) + .get_full_payload_with( + PayloadParameters { + parent_hash: payload_parameters.parent_hash, + parent_gas_limit: payload_parameters.parent_gas_limit, + proposer_gas_limit: payload_parameters.proposer_gas_limit, + payload_attributes: &payload_parameters.payload_attributes, + forkchoice_update_params: &payload_parameters.forkchoice_update_params, + current_fork: payload_parameters.current_fork, + }, + // If apply_operations is set, do NOT cache the payload at this point, we are about + // to mutate it and it would be incorrect to cache the unmutated payload. + // + // This is a flaw in apply_operations generally, if you want the mock builder to + // actually return payloads then this option should be turned off. + if self.apply_operations { + |_, _| None + } else { + ExecutionLayer::cache_payload + }, + ) .await .map_err(|e| format!("couldn't get payload {:?}", e))?; @@ -568,6 +617,10 @@ impl MockBuilder { ) = payload_response.into(); match fork { + ForkName::Gloas => { + // TODO(EIP7732) Check if this is how we want to do error handling for gloas + return Err("invalid fork".to_string()); + } ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { header: payload .as_fulu() @@ -799,7 +852,7 @@ impl MockBuilder { .beacon_client .get_beacon_blocks::(BlockId::Finalized) .await - .map_err(|_| "couldn't get finalized block".to_string())? + .map_err(|e| format!("couldn't get finalized block: {e:?}"))? .ok_or_else(|| "missing finalized block".to_string())? .data() .message() @@ -812,7 +865,7 @@ impl MockBuilder { .beacon_client .get_beacon_blocks::(BlockId::Justified) .await - .map_err(|_| "couldn't get justified block".to_string())? + .map_err(|e| format!("couldn't get justified block: {e:?}"))? .ok_or_else(|| "missing justified block".to_string())? .data() .message() @@ -885,15 +938,17 @@ impl MockBuilder { expected_withdrawals, None, ), - ForkName::Deneb | ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { - PayloadAttributes::new( - timestamp, - *prev_randao, - fee_recipient, - expected_withdrawals, - Some(head_block_root), - ) - } + ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu + | ForkName::Eip7805 + | ForkName::Gloas => PayloadAttributes::new( + timestamp, + *prev_randao, + fee_recipient, + expected_withdrawals, + Some(head_block_root), + ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); } @@ -958,11 +1013,21 @@ pub fn serve( let inner_ctx = builder.clone(); let ctx_filter = warp::any().map(move || inner_ctx.clone()); - let prefix = warp::path("eth") + let prefix_v1 = warp::path("eth") .and(warp::path("v1")) .and(warp::path("builder")); - let validators = prefix + let prefix_either = warp::path("eth") + .and( + warp::path::param::().or_else(|_| async move { + Err(warp::reject::custom(Custom( + "Invalid EndpointVersion".to_string(), + ))) + }), + ) + .and(warp::path("builder")); + + let validators = prefix_v1 .and(warp::path("validators")) .and(warp::body::json()) .and(warp::path::end()) @@ -974,61 +1039,89 @@ pub fn serve( .register_validators(registrations) .await .map_err(|e| warp::reject::custom(Custom(e)))?; - Ok::<_, Rejection>(warp::reply()) - }, - ) - .boxed(); - - let blinded_block_ssz = prefix - .and(warp::path("blinded_blocks")) - .and(warp::body::bytes()) - .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) - .and(warp::path::end()) - .and(ctx_filter.clone()) - .and_then( - |block_bytes: Bytes, fork_name: ForkName, builder: MockBuilder| async move { - let block = - SignedBlindedBeaconBlock::::from_ssz_bytes_by_fork(&block_bytes, fork_name) - .map_err(|e| warp::reject::custom(Custom(format!("{:?}", e))))?; - let payload = builder - .submit_blinded_block(block) - .await - .map_err(|e| warp::reject::custom(Custom(e)))?; - - Ok::<_, warp::reject::Rejection>( - warp::http::Response::builder() - .status(200) - .body(payload.as_ssz_bytes()) - .map(add_ssz_content_type_header) - .map(|res| add_consensus_version_header(res, fork_name)) - .unwrap(), - ) + Ok::<_, Rejection>(warp::reply().into_response()) }, ); - let blinded_block = - prefix + let blinded_block_ssz = + prefix_either .and(warp::path("blinded_blocks")) - .and(warp::body::json()) + .and(warp::body::bytes()) .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) .and(warp::path::end()) .and(ctx_filter.clone()) .and_then( - |block: SignedBlindedBeaconBlock, + |endpoint_version, + block_bytes: Bytes, fork_name: ForkName, builder: MockBuilder| async move { + if endpoint_version != EndpointVersion(1) + && endpoint_version != EndpointVersion(2) + { + return Err(warp::reject::custom(Custom(format!( + "Unsupported version: {endpoint_version}" + )))); + } + let block = SignedBlindedBeaconBlock::::from_ssz_bytes_by_fork( + &block_bytes, + fork_name, + ) + .map_err(|e| warp::reject::custom(Custom(format!("{:?}", e))))?; let payload = builder .submit_blinded_block(block) .await .map_err(|e| warp::reject::custom(Custom(e)))?; - let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: fork_name, - metadata: Default::default(), - data: payload, - }; - let json_payload = serde_json::to_string(&resp) - .map_err(|_| reject("coudn't serialize response"))?; + if endpoint_version == EndpointVersion(1) { + Ok::<_, warp::reject::Rejection>( + warp::http::Response::builder() + .status(200) + .body(payload.as_ssz_bytes()) + .map(add_ssz_content_type_header) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap(), + ) + } else { + Ok(warp::http::Response::builder() + .status(202) + .body(&[] as &'static [u8]) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap()) + } + }, + ); + + let blinded_block = prefix_either + .and(warp::path("blinded_blocks")) + .and(warp::body::json()) + .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) + .and(warp::path::end()) + .and(ctx_filter.clone()) + .and_then( + |endpoint_version, + block: SignedBlindedBeaconBlock, + fork_name: ForkName, + builder: MockBuilder| async move { + if endpoint_version != EndpointVersion(1) && endpoint_version != EndpointVersion(2) + { + return Err(warp::reject::custom(Custom(format!( + "Unsupported version: {endpoint_version}" + )))); + } + let payload = builder + .submit_blinded_block(block) + .await + .map_err(|e| warp::reject::custom(Custom(e)))?; + let resp: ForkVersionedResponse<_> = ForkVersionedResponse { + version: fork_name, + metadata: Default::default(), + data: payload, + }; + + let json_payload = serde_json::to_string(&resp) + .map_err(|_| reject("coudn't serialize response"))?; + + if endpoint_version == EndpointVersion(1) { Ok::<_, warp::reject::Rejection>( warp::http::Response::builder() .status(200) @@ -1036,16 +1129,24 @@ pub fn serve( serde_json::to_string(&json_payload) .map_err(|_| reject("invalid JSON"))?, ) + .map(|res| add_consensus_version_header(res, fork_name)) .unwrap(), ) - }, - ); + } else { + Ok(warp::http::Response::builder() + .status(202) + .body("".to_string()) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap()) + } + }, + ); - let status = prefix + let status = prefix_v1 .and(warp::path("status")) - .then(|| async { warp::reply() }); + .then(|| async { warp::reply().into_response() }); - let header = prefix + let header = prefix_v1 .and(warp::path("header")) .and(warp::path::param::().or_else(|_| async { Err(reject("Invalid slot")) })) .and( diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index ed27cff9ab..8fda95b97c 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -1,13 +1,14 @@ use crate::{ test_utils::{ - MockServer, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, + DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, DEFAULT_TERMINAL_DIFFICULTY, MockServer, }, *, }; use alloy_primitives::B256 as H256; +use fixed_bytes::FixedBytesExtended; use kzg::Kzg; use tempfile::NamedTempFile; -use types::{FixedBytesExtended, MainnetEthSpec}; +use types::MainnetEthSpec; pub struct MockExecutionLayer { pub server: MockServer, @@ -30,6 +31,7 @@ impl MockExecutionLayer { None, None, None, + None, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), Arc::new(spec), None, @@ -45,6 +47,7 @@ impl MockExecutionLayer { prague_time: Option, eip7805_time: Option, osaka_time: Option, + amsterdam_time: Option, jwt_key: Option, spec: Arc, kzg: Option>, @@ -63,7 +66,7 @@ impl MockExecutionLayer { prague_time, eip7805_time, osaka_time, - spec.clone(), + amsterdam_time, kzg, ); @@ -171,10 +174,11 @@ impl MockExecutionLayer { assert_eq!(payload.prev_randao(), prev_randao); // Ensure the payload cache is empty. - assert!(self - .el - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.el + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); let builder_params = BuilderParams { pubkey: PublicKeyBytes::empty(), slot, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 54aa7ec4ef..7e036a13c2 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -2,7 +2,7 @@ use crate::engine_api::auth::JwtKey; use crate::engine_api::{ - auth::Auth, http::JSONRPC_VERSION, ExecutionBlock, PayloadStatusV1, PayloadStatusV1Status, + ExecutionBlock, PayloadStatusV1, PayloadStatusV1Status, auth::Auth, http::JSONRPC_VERSION, }; use crate::json_structures::JsonClientVersionV1; use bytes::Bytes; @@ -22,17 +22,17 @@ use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::{Arc, LazyLock}; use tokio::{runtime, sync::oneshot}; use tracing::info; -use types::{ChainSpec, EthSpec, ExecutionBlockHash, Uint256}; -use warp::{http::StatusCode, Filter, Rejection}; +use types::{EthSpec, ExecutionBlockHash, Uint256}; +use warp::{Filter, Rejection, http::StatusCode}; use crate::EngineCapabilities; pub use execution_block_generator::DEFAULT_GAS_LIMIT; pub use execution_block_generator::{ - generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, - mock_el_extra_data, static_valid_tx, Block, ExecutionBlockGenerator, + Block, ExecutionBlockGenerator, generate_blobs, generate_genesis_block, + generate_genesis_header, generate_pow_block, mock_el_extra_data, static_valid_tx, }; pub use hook::Hook; -pub use mock_builder::{mock_builder_extra_data, MockBuilder, Operation}; +pub use mock_builder::{MockBuilder, Operation, mock_builder_extra_data}; pub use mock_execution_layer::MockExecutionLayer; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; @@ -45,7 +45,6 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { new_payload_v2: true, new_payload_v3: true, new_payload_v4: true, - new_payload_v5: true, forkchoice_updated_v1: true, forkchoice_updated_v2: true, forkchoice_updated_v3: true, @@ -89,6 +88,7 @@ pub struct MockExecutionConfig { pub prague_time: Option, pub eip7805_time: Option, pub osaka_time: Option, + pub amsterdam_time: Option, } impl Default for MockExecutionConfig { @@ -104,6 +104,7 @@ impl Default for MockExecutionConfig { prague_time: None, eip7805_time: None, osaka_time: None, + amsterdam_time: None, } } } @@ -116,7 +117,7 @@ pub struct MockServer { } impl MockServer { - pub fn unit_testing(chain_spec: Arc) -> Self { + pub fn unit_testing() -> Self { Self::new( &runtime::Handle::current(), JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(), @@ -128,7 +129,7 @@ impl MockServer { None, // FIXME(electra): should this be the default? None, None, // FIXME(fulu): should this be the default? - chain_spec, + None, // FIXME(gloas): should this be the default? None, ) } @@ -136,7 +137,6 @@ impl MockServer { pub fn new_with_config( handle: &runtime::Handle, config: MockExecutionConfig, - spec: Arc, kzg: Option>, ) -> Self { create_test_tracing_subscriber(); @@ -151,6 +151,7 @@ impl MockServer { prague_time, eip7805_time, osaka_time, + amsterdam_time, } = config; let last_echo_request = Arc::new(RwLock::new(None)); let preloaded_responses = Arc::new(Mutex::new(vec![])); @@ -163,7 +164,7 @@ impl MockServer { prague_time, eip7805_time, osaka_time, - spec, + amsterdam_time, kzg, ); @@ -228,7 +229,7 @@ impl MockServer { prague_time: Option, eip7805_time: Option, osaka_time: Option, - spec: Arc, + amsterdam_time: Option, kzg: Option>, ) -> Self { Self::new_with_config( @@ -244,8 +245,8 @@ impl MockServer { prague_time, eip7805_time, osaka_time, + amsterdam_time, }, - spec, kzg, ) } diff --git a/beacon_node/execution_layer/src/versioned_hashes.rs b/beacon_node/execution_layer/src/versioned_hashes.rs index 97c3100de9..21cfd5a322 100644 --- a/beacon_node/execution_layer/src/versioned_hashes.rs +++ b/beacon_node/execution_layer/src/versioned_hashes.rs @@ -1,6 +1,7 @@ use alloy_consensus::TxEnvelope; use alloy_rlp::Decodable; -use types::{EthSpec, ExecutionPayloadRef, Hash256, Unsigned, VersionedHash}; +use typenum::Unsigned; +use types::{EthSpec, ExecutionPayloadRef, Hash256, VersionedHash}; #[derive(Debug)] pub enum Error { diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index 6ba8998a01..124231a57e 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -4,22 +4,14 @@ version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } -[dev-dependencies] -eth1_test_rig = { workspace = true } -logging = { workspace = true } -sensitive_url = { workspace = true } - [dependencies] -environment = { workspace = true } -eth1 = { workspace = true } +bls = { workspace = true } ethereum_hashing = { workspace = true } ethereum_ssz = { workspace = true } -futures = { workspace = true } int_to_bytes = { workspace = true } merkle_proof = { workspace = true } rayon = { workspace = true } state_processing = { workspace = true } -tokio = { workspace = true } tracing = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } diff --git a/beacon_node/genesis/src/common.rs b/beacon_node/genesis/src/common.rs index e48fa36204..88a88810d8 100644 --- a/beacon_node/genesis/src/common.rs +++ b/beacon_node/genesis/src/common.rs @@ -37,10 +37,17 @@ pub fn genesis_deposits( proofs.push(proof); } - Ok(deposit_data + deposit_data .into_iter() .zip(proofs) - .map(|(data, proof)| (data, proof.into())) - .map(|(data, proof)| Deposit { proof, data }) - .collect()) + .map(|(data, proof)| { + let converted_proof = proof + .try_into() + .map_err(|e| format!("Error converting proof: {:?}", e))?; + Ok(Deposit { + proof: converted_proof, + data, + }) + }) + .collect() } diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs deleted file mode 100644 index dede96512c..0000000000 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ /dev/null @@ -1,461 +0,0 @@ -pub use crate::common::genesis_deposits; -pub use eth1::Config as Eth1Config; - -use eth1::{DepositLog, Eth1Block, Service as Eth1Service}; -use state_processing::{ - eth2_genesis_time, initialize_beacon_state_from_eth1, is_valid_genesis_state, - per_block_processing::process_operations::apply_deposit, process_activations, -}; -use std::sync::{ - atomic::{AtomicU64, AtomicUsize, Ordering}, - Arc, -}; -use std::time::Duration; -use tokio::time::sleep; -use tracing::{debug, error, info, trace}; -use types::{BeaconState, ChainSpec, Deposit, Eth1Data, EthSpec, FixedBytesExtended, Hash256}; - -/// The number of blocks that are pulled per request whilst waiting for genesis. -const BLOCKS_PER_GENESIS_POLL: usize = 99; - -/// Stats about the eth1 genesis process. -pub struct Statistics { - highest_processed_block: AtomicU64, - active_validator_count: AtomicUsize, - total_deposit_count: AtomicUsize, - latest_timestamp: AtomicU64, -} - -/// Provides a service that connects to some Eth1 HTTP JSON-RPC endpoint and maintains a cache of -/// eth1 blocks and deposits, listening for the eth1 block that triggers eth2 genesis and returning -/// the genesis `BeaconState`. -/// -/// Is a wrapper around the `Service` struct of the `eth1` crate. -#[derive(Clone)] -pub struct Eth1GenesisService { - /// The underlying service. Access to this object is only required for testing and diagnosis. - pub eth1_service: Eth1Service, - /// Statistics about genesis progress. - stats: Arc, -} - -impl Eth1GenesisService { - /// Creates a new service. Does not attempt to connect to the Eth1 node. - /// - /// Modifies the given `config` to make it more suitable to the task of listening to genesis. - pub fn new(config: Eth1Config, spec: Arc) -> Result { - let config = Eth1Config { - // Truncating the block cache makes searching for genesis more - // complicated. - block_cache_truncation: None, - // Scan large ranges of blocks when awaiting genesis. - blocks_per_log_query: 1_000, - // Only perform a few log requests each time the eth1 node is polled. - // - // For small testnets this makes finding genesis much faster, - // as it usually happens within 1,000 blocks. - max_log_requests_per_update: Some(5), - // Only perform a few logs requests each time the eth1 node is polled. - // - // For small testnets, this is much faster as they do not have - // a `MIN_GENESIS_SECONDS`, so after `MIN_GENESIS_VALIDATOR_COUNT` - // has been reached only a single block needs to be read. - max_blocks_per_update: Some(BLOCKS_PER_GENESIS_POLL), - ..config - }; - - Ok(Self { - eth1_service: Eth1Service::new(config, spec) - .map_err(|e| format!("Failed to create eth1 service: {:?}", e))?, - stats: Arc::new(Statistics { - highest_processed_block: AtomicU64::new(0), - active_validator_count: AtomicUsize::new(0), - total_deposit_count: AtomicUsize::new(0), - latest_timestamp: AtomicU64::new(0), - }), - }) - } - - /// Returns the first eth1 block that has enough deposits that it's a (potentially invalid) - /// candidate for genesis. - fn first_candidate_eth1_block(&self, min_genesis_active_validator_count: usize) -> Option { - if self.eth1_service.deposit_cache_len() < min_genesis_active_validator_count { - None - } else { - self.eth1_service - .deposits() - .read() - .cache - .get_log(min_genesis_active_validator_count.saturating_sub(1)) - .map(|log| log.block_number) - } - } - - /// Scans the Eth1 chain, returning a genesis state once it has been discovered. - /// - /// ## Returns - /// - /// - `Ok(state)` once the canonical eth2 genesis state has been discovered. - /// - `Err(e)` if there is some internal error during updates. - pub async fn wait_for_genesis_state( - &self, - update_interval: Duration, - ) -> Result, String> { - let eth1_service = &self.eth1_service; - let spec = eth1_service.chain_spec(); - - let mut sync_blocks = false; - let mut highest_processed_block = None; - - info!("Importing eth1 deposit logs"); - - loop { - let update_result = eth1_service - .update_deposit_cache(None) - .await - .map_err(|e| format!("{:?}", e)); - - if let Err(e) = update_result { - error!(error = e, "Failed to update eth1 deposit cache") - } - - self.stats - .total_deposit_count - .store(eth1_service.deposit_cache_len(), Ordering::Relaxed); - - if !sync_blocks { - if let Some(viable_eth1_block) = self - .first_candidate_eth1_block(spec.min_genesis_active_validator_count as usize) - { - info!("Importing eth1 blocks"); - self.eth1_service.set_lowest_cached_block(viable_eth1_block); - sync_blocks = true - } else { - info!( - min_genesis_active_validators = spec.min_genesis_active_validator_count, - total_deposits = eth1_service.deposit_cache_len(), - valid_deposits = eth1_service.get_raw_valid_signature_count(), - "Waiting for more deposits" - ); - - sleep(update_interval).await; - - continue; - } - } - - // Download new eth1 blocks into the cache. - let blocks_imported = match eth1_service.update_block_cache(None).await { - Ok(outcome) => { - debug!( - latest_block_timestamp = eth1_service.latest_block_timestamp(), - cache_head = eth1_service.highest_safe_block(), - count = outcome.blocks_imported, - "Imported eth1 blocks" - ); - outcome.blocks_imported - } - Err(e) => { - error!( - error = ?e, - "Failed to update eth1 block cache" - ); - 0 - } - }; - - // Scan the new eth1 blocks, searching for genesis. - if let Some(genesis_state) = - self.scan_new_blocks::(&mut highest_processed_block, spec)? - { - info!( - genesis_validators = genesis_state - .get_active_validator_indices(E::genesis_epoch(), spec) - .map_err(|e| format!("Genesis validators error: {:?}", e))? - .len(), - genesis_time = genesis_state.genesis_time(), - "Genesis ceremony complete" - ); - break Ok(genesis_state); - } - - // Drop all the scanned blocks as they are no longer required. - eth1_service.clear_block_cache(); - - // Load some statistics from the atomics. - let active_validator_count = self.stats.active_validator_count.load(Ordering::Relaxed); - let total_deposit_count = self.stats.total_deposit_count.load(Ordering::Relaxed); - let latest_timestamp = self.stats.latest_timestamp.load(Ordering::Relaxed); - - // Perform some logging. - if timestamp_can_trigger_genesis(latest_timestamp, spec)? { - // Indicate that we are awaiting adequate active validators. - if (active_validator_count as u64) < spec.min_genesis_active_validator_count { - info!( - min_genesis_active_validators = spec.min_genesis_active_validator_count, - active_validators = active_validator_count, - total_deposits = total_deposit_count, - valid_deposits = eth1_service.get_valid_signature_count().unwrap_or(0), - "Waiting for more validators" - ); - } - } else { - info!( - genesis_delay = spec.genesis_delay, - genesis_time = spec.min_genesis_time, - latest_eth1_timestamp = latest_timestamp, - "Waiting for adequate eth1 timestamp" - ); - } - - // If we imported the full number of blocks, poll again in a short amount of time. - // - // We assume that if we imported a large chunk of blocks then we're some distance from - // the head and we should sync faster. - if blocks_imported >= BLOCKS_PER_GENESIS_POLL { - sleep(Duration::from_millis(50)).await; - } else { - sleep(update_interval).await; - } - } - } - - /// Processes any new blocks that have appeared since this function was last run. - /// - /// Blocks are always tested in increasing order, starting with the lowest unknown block - /// number in the cache. - /// - /// ## Returns - /// - /// - `Ok(Some(eth1_block))` if a previously-unprocessed block would trigger Eth2 genesis. - /// - `Ok(None)` if none of the new blocks would trigger genesis, or there were no new blocks. - /// - `Err(_)` if there was some internal error. - fn scan_new_blocks( - &self, - highest_processed_block: &mut Option, - spec: &ChainSpec, - ) -> Result>, String> { - let eth1_service = &self.eth1_service; - - for block in eth1_service.blocks().read().iter() { - // It's possible that the block and deposit caches aren't synced. Ignore any blocks - // which are not safe for both caches. - // - // Don't update the highest processed block since we want to come back and process this - // again later. - if eth1_service - .highest_safe_block() - .is_none_or(|n| block.number > n) - { - continue; - } - - // Ignore any block that has already been processed or update the highest processed - // block. - if highest_processed_block.is_some_and(|highest| highest >= block.number) { - continue; - } else { - self.stats - .highest_processed_block - .store(block.number, Ordering::Relaxed); - self.stats - .latest_timestamp - .store(block.timestamp, Ordering::Relaxed); - - *highest_processed_block = Some(block.number) - } - - // Ignore any block with an insufficient timestamp. - if !timestamp_can_trigger_genesis(block.timestamp, spec)? { - trace!( - genesis_delay = spec.genesis_delay, - min_genesis_time = spec.min_genesis_time, - eth1_block_timestamp = block.timestamp, - eth1_block_number = block.number, - "Insufficient block timestamp" - ); - continue; - } - - let valid_signature_count = eth1_service - .get_valid_signature_count_at_block(block.number) - .unwrap_or(0); - if (valid_signature_count as u64) < spec.min_genesis_active_validator_count { - trace!( - genesis_delay = spec.genesis_delay, - valid_signature_count = valid_signature_count, - min_validator_count = spec.min_genesis_active_validator_count, - eth1_block_number = block.number, - "Insufficient valid signatures" - ); - continue; - } - - // Generate a potential beacon state for this eth1 block. - // - // Note: this state is fully valid, some fields have been bypassed to make verification - // faster. - let state = self.cheap_state_at_eth1_block::(block, spec)?; - let active_validator_count = state - .get_active_validator_indices(E::genesis_epoch(), spec) - .map_err(|e| format!("Genesis validators error: {:?}", e))? - .len(); - - self.stats - .active_validator_count - .store(active_validator_count, Ordering::Relaxed); - - if is_valid_genesis_state(&state, spec) { - let genesis_state = self - .genesis_from_eth1_block(block.clone(), spec) - .map_err(|e| format!("Failed to generate valid genesis state : {}", e))?; - - return Ok(Some(genesis_state)); - } else { - trace!( - min_genesis_active_validator_count = - format!("{}", spec.min_genesis_active_validator_count), - active_validators = active_validator_count, - eth1_block_number = block.number, - "Insufficient active validators" - ); - } - } - - Ok(None) - } - - /// Produces an eth2 genesis `BeaconState` from the given `eth1_block`. The caller should have - /// verified that `eth1_block` produces a valid genesis state. - /// - /// ## Returns - /// - /// - `Ok(genesis_state)`: if all went well. - /// - `Err(e)`: if the given `eth1_block` was not a viable block to trigger genesis or there was - /// an internal error. - fn genesis_from_eth1_block( - &self, - eth1_block: Eth1Block, - spec: &ChainSpec, - ) -> Result, String> { - let deposit_logs = self - .eth1_service - .deposits() - .read() - .cache - .iter() - .take_while(|log| log.block_number <= eth1_block.number) - .map(|log| log.deposit_data.clone()) - .collect::>(); - - let genesis_state = initialize_beacon_state_from_eth1( - eth1_block.hash, - eth1_block.timestamp, - genesis_deposits(deposit_logs, spec)?, - None, - spec, - ) - .map_err(|e| format!("Unable to initialize genesis state: {:?}", e))?; - - if is_valid_genesis_state(&genesis_state, spec) { - Ok(genesis_state) - } else { - Err("Generated state was not valid.".to_string()) - } - } - - /// Generates an incomplete `BeaconState` for some `eth1_block` that can be used for checking - /// to see if that `eth1_block` triggers eth2 genesis. - /// - /// ## Notes - /// - /// The returned `BeaconState` should **not** be used as the genesis state, it is - /// incomplete. - fn cheap_state_at_eth1_block( - &self, - eth1_block: &Eth1Block, - spec: &ChainSpec, - ) -> Result, String> { - let genesis_time = eth2_genesis_time(eth1_block.timestamp, spec) - .map_err(|e| format!("Unable to set genesis time: {:?}", e))?; - - let mut state: BeaconState = BeaconState::new( - genesis_time, - Eth1Data { - block_hash: Hash256::zero(), - deposit_root: Hash256::zero(), - deposit_count: 0, - }, - spec, - ); - - self.deposit_logs_at_block(eth1_block.number) - .iter() - .map(|deposit_log| Deposit { - // Generate a bogus proof. - // - // The deposits are coming directly from our own deposit tree to there's no need to - // make proofs about their inclusion in it. - proof: vec![Hash256::zero(); spec.deposit_contract_tree_depth as usize].into(), - data: deposit_log.deposit_data.clone(), - }) - .try_for_each(|deposit| { - // Skip proof verification (see comment about bogus proof generation). - const PROOF_VERIFICATION: bool = false; - - // Note: presently all the signatures are verified each time this function is - // run. - // - // It would be more efficient to pre-verify signatures, filter out the invalid - // ones and disable verification for `process_deposit`. - // - // Such an optimization would only be useful in a scenario where `MIN_GENESIS_TIME` - // is reached _prior_ to `MIN_ACTIVE_VALIDATOR_COUNT`. I suspect this won't be the - // case for mainnet, so we defer this optimization. - let Deposit { proof, data } = deposit; - let proof = if PROOF_VERIFICATION { - Some(proof) - } else { - None - }; - - apply_deposit(&mut state, data, proof, true, spec) - .map_err(|e| format!("Error whilst processing deposit: {:?}", e)) - })?; - - process_activations(&mut state, spec) - .map_err(|e| format!("Error whilst processing activations: {:?}", e))?; - - Ok(state) - } - - /// Returns all deposit logs included in `block_number` and all prior blocks. - fn deposit_logs_at_block(&self, block_number: u64) -> Vec { - self.eth1_service - .deposits() - .read() - .cache - .iter() - .take_while(|log| log.block_number <= block_number) - .cloned() - .collect() - } - - /// Returns statistics about eth1 genesis. - pub fn statistics(&self) -> &Statistics { - &self.stats - } - - /// Returns the `Service` contained in `self`. - pub fn into_core_service(self) -> Eth1Service { - self.eth1_service - } -} - -/// Returns `false` for a timestamp that would result in a genesis time that is earlier than -/// `MIN_GENESIS_TIME`. -fn timestamp_can_trigger_genesis(timestamp: u64, spec: &ChainSpec) -> Result { - eth2_genesis_time(timestamp, spec) - .map(|t| t >= spec.min_genesis_time) - .map_err(|e| format!("Arith error when during genesis calculation: {:?}", e)) -} diff --git a/beacon_node/genesis/src/interop.rs b/beacon_node/genesis/src/interop.rs index 4fccc0393b..349b8f19c8 100644 --- a/beacon_node/genesis/src/interop.rs +++ b/beacon_node/genesis/src/interop.rs @@ -1,12 +1,10 @@ use crate::common::genesis_deposits; +use bls::{Keypair, PublicKey, Signature}; use ethereum_hashing::hash; use rayon::prelude::*; use ssz::Encode; use state_processing::initialize_beacon_state_from_eth1; -use types::{ - BeaconState, ChainSpec, DepositData, EthSpec, ExecutionPayloadHeader, Hash256, Keypair, - PublicKey, Signature, -}; +use types::{BeaconState, ChainSpec, DepositData, EthSpec, ExecutionPayloadHeader, Hash256}; pub const DEFAULT_ETH1_BLOCK_HASH: &[u8] = &[0x42; 32]; @@ -169,7 +167,7 @@ fn alternating_eth1_withdrawal_credentials_fn<'a>( pubkey: &'a PublicKey, spec: &'a ChainSpec, ) -> Hash256 { - if index % 2usize == 0usize { + if index.is_multiple_of(2) { bls_withdrawal_credentials(pubkey, spec) } else { eth1_withdrawal_credentials(pubkey, spec) @@ -194,7 +192,7 @@ pub fn interop_genesis_state_with_eth1( #[cfg(test)] mod test { use super::*; - use types::{test_utils::generate_deterministic_keypairs, MinimalEthSpec}; + use types::{MinimalEthSpec, test_utils::generate_deterministic_keypairs}; type TestEthSpec = MinimalEthSpec; diff --git a/beacon_node/genesis/src/lib.rs b/beacon_node/genesis/src/lib.rs index 1fba64aafb..08af792415 100644 --- a/beacon_node/genesis/src/lib.rs +++ b/beacon_node/genesis/src/lib.rs @@ -1,12 +1,8 @@ mod common; -mod eth1_genesis_service; mod interop; -pub use eth1::Config as Eth1Config; -pub use eth1::Eth1Endpoint; -pub use eth1_genesis_service::{Eth1GenesisService, Statistics}; pub use interop::{ - bls_withdrawal_credentials, interop_genesis_state, interop_genesis_state_with_eth1, - InteropGenesisBuilder, DEFAULT_ETH1_BLOCK_HASH, + DEFAULT_ETH1_BLOCK_HASH, InteropGenesisBuilder, bls_withdrawal_credentials, + interop_genesis_state, interop_genesis_state_with_eth1, }; pub use types::test_utils::generate_deterministic_keypairs; diff --git a/beacon_node/genesis/tests/tests.rs b/beacon_node/genesis/tests/tests.rs deleted file mode 100644 index b5710e50fd..0000000000 --- a/beacon_node/genesis/tests/tests.rs +++ /dev/null @@ -1,107 +0,0 @@ -#![cfg(test)] -use environment::{Environment, EnvironmentBuilder}; -use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; -use eth1_test_rig::{AnvilEth1Instance, DelayThenDeposit, Middleware}; -use genesis::{Eth1Config, Eth1GenesisService}; -use logging::create_test_tracing_subscriber; -use sensitive_url::SensitiveUrl; -use state_processing::is_valid_genesis_state; -use std::sync::Arc; -use std::time::Duration; -use types::{ - test_utils::generate_deterministic_keypair, FixedBytesExtended, Hash256, MinimalEthSpec, -}; - -pub fn new_env() -> Environment { - create_test_tracing_subscriber(); - EnvironmentBuilder::minimal() - .multi_threaded_tokio_runtime() - .expect("should start tokio runtime") - .build() - .expect("should build env") -} - -#[test] -fn basic() { - let env = new_env(); - let mut spec = (*env.eth2_config().spec).clone(); - spec.min_genesis_time = 0; - spec.min_genesis_active_validator_count = 8; - let spec = Arc::new(spec); - - env.runtime().block_on(async { - let eth1 = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()) - .await - .expect("should start eth1 environment"); - let deposit_contract = ð1.deposit_contract; - let client = eth1.json_rpc_client(); - - let now = client - .get_block_number() - .await - .map(|v| v.as_u64()) - .expect("should get block number"); - - let service = Eth1GenesisService::new( - Eth1Config { - endpoint: Eth1Endpoint::NoAuth( - SensitiveUrl::parse(eth1.endpoint().as_str()).unwrap(), - ), - deposit_contract_address: deposit_contract.address(), - deposit_contract_deploy_block: now, - lowest_cached_block_number: now, - follow_distance: 0, - block_cache_truncation: None, - ..Eth1Config::default() - }, - spec.clone(), - ) - .unwrap(); - - // NOTE: this test is sensitive to the response speed of the external web3 server. If - // you're experiencing failures, try increasing the update_interval. - let update_interval = Duration::from_millis(500); - - let deposits = (0..spec.min_genesis_active_validator_count + 2) - .map(|i| { - deposit_contract.deposit_helper::( - generate_deterministic_keypair(i as usize), - Hash256::from_low_u64_le(i), - 32_000_000_000, - ) - }) - .map(|deposit| DelayThenDeposit { - delay: Duration::from_secs(0), - deposit, - }) - .collect::>(); - - let deposit_future = deposit_contract.deposit_multiple(deposits); - - let wait_future = service.wait_for_genesis_state::(update_interval); - - let state = futures::try_join!(deposit_future, wait_future) - .map(|(_, state)| state) - .expect("should finish waiting for genesis"); - - // Note: using anvil these deposits are 1-per-block, therefore we know there should only be - // the minimum number of validators. - assert_eq!( - state.validators().len(), - spec.min_genesis_active_validator_count as usize, - "should have expected validator count" - ); - - assert!(state.genesis_time() > 0, "should have some genesis time"); - - assert!( - is_valid_genesis_state(&state, &spec), - "should be valid genesis state" - ); - - assert!( - is_valid_genesis_state(&state, &spec), - "should be valid genesis state" - ); - }); -} diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index afc68ad96d..571dab1027 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -8,24 +8,28 @@ autotests = false # using a single test binary com [dependencies] beacon_chain = { workspace = true } beacon_processor = { workspace = true } +bls = { workspace = true } bs58 = "0.4.0" bytes = { workspace = true } +context_deserialize = { workspace = true } directory = { workspace = true } either = { workspace = true } -eth1 = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["lighthouse"] } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } +fixed_bytes = { workspace = true } futures = { workspace = true } health_metrics = { workspace = true } hex = { workspace = true } lighthouse_network = { workspace = true } +lighthouse_tracing = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } lru = { workspace = true } metrics = { workspace = true } network = { workspace = true } +network_utils = { workspace = true } operation_pool = { workspace = true } parking_lot = { workspace = true } proto_array = { workspace = true } diff --git a/beacon_node/http_api/src/aggregate_attestation.rs b/beacon_node/http_api/src/aggregate_attestation.rs index 809f381139..183d29df22 100644 --- a/beacon_node/http_api/src/aggregate_attestation.rs +++ b/beacon_node/http_api/src/aggregate_attestation.rs @@ -1,6 +1,6 @@ use crate::api_types::GenericResponse; use crate::unsupported_version_rejection; -use crate::version::{add_consensus_version_header, V1, V2}; +use crate::version::{V1, V2, add_consensus_version_header}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::{self, EndpointVersion, Hash256, Slot}; use std::sync::Arc; @@ -63,6 +63,6 @@ pub fn get_aggregate_attestation( } else if endpoint_version == V1 { Ok(warp::reply::json(&GenericResponse::from(aggregate_attestation)).into_response()) } else { - return Err(unsupported_version_rejection(endpoint_version)); + Err(unsupported_version_rejection(endpoint_version)) } } diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs index 23ab5e3752..6e285829d2 100644 --- a/beacon_node/http_api/src/attestation_performance.rs +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -3,7 +3,7 @@ use eth2::lighthouse::{ AttestationPerformance, AttestationPerformanceQuery, AttestationPerformanceStatistics, }; use state_processing::{ - per_epoch_processing::EpochProcessingSummary, BlockReplayError, BlockReplayer, + BlockReplayError, BlockReplayer, per_epoch_processing::EpochProcessingSummary, }; use std::sync::Arc; use types::{BeaconState, BeaconStateError, EthSpec, Hash256}; diff --git a/beacon_node/http_api/src/attester_duties.rs b/beacon_node/http_api/src/attester_duties.rs index 8905b24cde..b42e474b5c 100644 --- a/beacon_node/http_api/src/attester_duties.rs +++ b/beacon_node/http_api/src/attester_duties.rs @@ -16,7 +16,12 @@ pub fn attester_duties( request_indices: &[u64], chain: &BeaconChain, ) -> Result { - let current_epoch = chain.epoch().map_err(warp_utils::reject::unhandled_error)?; + let current_epoch = chain + .slot_clock + .now_or_genesis() + .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) + .ok_or(BeaconChainError::UnableToReadSlot) + .map_err(warp_utils::reject::unhandled_error)?; // Determine what the current epoch would be if we fast-forward our system clock by // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. @@ -24,11 +29,17 @@ pub fn attester_duties( // Most of the time, `tolerant_current_epoch` will be equal to `current_epoch`. However, during // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerant_current_epoch` // will equal `current_epoch + 1` - let tolerant_current_epoch = chain - .slot_clock - .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or_else(|| warp_utils::reject::custom_server_error("unable to read slot clock".into()))? - .epoch(T::EthSpec::slots_per_epoch()); + let tolerant_current_epoch = if chain.slot_clock.is_prior_to_genesis().unwrap_or(true) { + current_epoch + } else { + chain + .slot_clock + .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) + .ok_or_else(|| { + warp_utils::reject::custom_server_error("unable to read slot clock".into()) + })? + .epoch(T::EthSpec::slots_per_epoch()) + }; if request_epoch == current_epoch || request_epoch == current_epoch + 1 diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs new file mode 100644 index 0000000000..df5e6eee5c --- /dev/null +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -0,0 +1,2 @@ +pub mod pool; +pub mod states; diff --git a/beacon_node/http_api/src/beacon/pool.rs b/beacon_node/http_api/src/beacon/pool.rs new file mode 100644 index 0000000000..059573c317 --- /dev/null +++ b/beacon_node/http_api/src/beacon/pool.rs @@ -0,0 +1,522 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{NetworkTxFilter, OptionalConsensusVersionHeaderFilter, ResponseFilter}; +use crate::version::{ + ResponseIncludesVersion, V1, V2, add_consensus_version_header, beacon_response, + unsupported_version_rejection, +}; +use crate::{sync_committees, utils}; +use beacon_chain::observed_operations::ObservationOutcome; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::{AttestationPoolQuery, EndpointVersion, Failure, GenericResponse}; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use operation_pool::ReceivedPreCapella; +use slot_clock::SlotClock; +use std::collections::HashSet; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, info, warn}; +use types::{ + Attestation, AttestationData, AttesterSlashing, ForkName, ProposerSlashing, + SignedBlsToExecutionChange, SignedVoluntaryExit, SingleAttestation, SyncCommitteeMessage, +}; +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; +use warp_utils::reject::convert_rejection; + +pub type BeaconPoolPathFilter = BoxedFilter<( + TaskSpawner<::EthSpec>, + Arc>, +)>; +pub type BeaconPoolPathV2Filter = BoxedFilter<( + TaskSpawner<::EthSpec>, + Arc>, +)>; +pub type BeaconPoolPathAnyFilter = BoxedFilter<( + EndpointVersion, + TaskSpawner<::EthSpec>, + Arc>, +)>; + +/// POST beacon/pool/bls_to_execution_changes +pub fn post_beacon_pool_bls_to_execution_changes( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("bls_to_execution_changes")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + address_changes: Vec, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let mut failures = vec![]; + + for (index, address_change) in address_changes.into_iter().enumerate() { + let validator_index = address_change.message.validator_index; + + match chain.verify_bls_to_execution_change_for_http_api(address_change) { + Ok(ObservationOutcome::New(verified_address_change)) => { + let validator_index = + verified_address_change.as_inner().message.validator_index; + let address = verified_address_change + .as_inner() + .message + .to_execution_address; + + // New to P2P *and* op pool, gossip immediately if post-Capella. + let received_pre_capella = + if chain.current_slot_is_post_capella().unwrap_or(false) { + ReceivedPreCapella::No + } else { + ReceivedPreCapella::Yes + }; + if matches!(received_pre_capella, ReceivedPreCapella::No) { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::BlsToExecutionChange(Box::new( + verified_address_change.as_inner().clone(), + )), + )?; + } + + // Import to op pool (may return `false` if there's a race). + let imported = chain.import_bls_to_execution_change( + verified_address_change, + received_pre_capella, + ); + + info!( + %validator_index, + ?address, + published = + matches!(received_pre_capella, ReceivedPreCapella::No), + imported, + "Processed BLS to execution change" + ); + } + Ok(ObservationOutcome::AlreadyKnown) => { + debug!(%validator_index, "BLS to execution change already known"); + } + Err(e) => { + warn!( + validator_index, + reason = ?e, + source = "HTTP", + "Invalid BLS to execution change" + ); + failures.push(Failure::new(index, format!("invalid: {e:?}"))); + } + } + } + + if failures.is_empty() { + Ok(()) + } else { + Err(warp_utils::reject::indexed_bad_request( + "some BLS to execution changes failed to verify".into(), + failures, + )) + } + }) + }, + ) + .boxed() +} + +/// GET beacon/pool/bls_to_execution_changes +pub fn get_beacon_pool_bls_to_execution_changes( + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("bls_to_execution_changes")) + .and(warp::path::end()) + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let address_changes = chain.op_pool.get_all_bls_to_execution_changes(); + Ok(GenericResponse::from(address_changes)) + }) + }, + ) + .boxed() +} + +/// POST beacon/pool/sync_committees +pub fn post_beacon_pool_sync_committees( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("sync_committees")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + signatures: Vec, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + sync_committees::process_sync_committee_signatures( + signatures, network_tx, &chain, + )?; + Ok(GenericResponse::from(())) + }) + }, + ) + .boxed() +} + +/// GET beacon/pool/voluntary_exits +pub fn get_beacon_pool_voluntary_exits( + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("voluntary_exits")) + .and(warp::path::end()) + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let attestations = chain.op_pool.get_all_voluntary_exits(); + Ok(GenericResponse::from(attestations)) + }) + }, + ) + .boxed() +} + +/// POST beacon/pool/voluntary_exits +pub fn post_beacon_pool_voluntary_exits( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("voluntary_exits")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + exit: SignedVoluntaryExit, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let outcome = chain + .verify_voluntary_exit_for_gossip(exit.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + // Notify the validator monitor. + chain + .validator_monitor + .read() + .register_api_voluntary_exit(&exit.message); + + if let ObservationOutcome::New(exit) = outcome { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::VoluntaryExit(Box::new(exit.clone().into_inner())), + )?; + + chain.import_voluntary_exit(exit); + } + + Ok(()) + }) + }, + ) + .boxed() +} + +/// GET beacon/pool/proposer_slashings +pub fn get_beacon_pool_proposer_slashings( + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("proposer_slashings")) + .and(warp::path::end()) + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let attestations = chain.op_pool.get_all_proposer_slashings(); + Ok(GenericResponse::from(attestations)) + }) + }, + ) + .boxed() +} + +/// POST beacon/pool/proposer_slashings +pub fn post_beacon_pool_proposer_slashings( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path: &BeaconPoolPathFilter, +) -> ResponseFilter { + beacon_pool_path + .clone() + .and(warp::path("proposer_slashings")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + slashing: ProposerSlashing, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let outcome = chain + .verify_proposer_slashing_for_gossip(slashing.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + // Notify the validator monitor. + chain + .validator_monitor + .read() + .register_api_proposer_slashing(&slashing); + + if let ObservationOutcome::New(slashing) = outcome { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::ProposerSlashing(Box::new( + slashing.clone().into_inner(), + )), + )?; + + chain.import_proposer_slashing(slashing); + } + + Ok(()) + }) + }, + ) + .boxed() +} + +/// GET beacon/pool/attester_slashings +pub fn get_beacon_pool_attester_slashings( + beacon_pool_path_any: &BeaconPoolPathAnyFilter, +) -> ResponseFilter { + beacon_pool_path_any + .clone() + .and(warp::path("attester_slashings")) + .and(warp::path::end()) + .then( + |endpoint_version: EndpointVersion, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + let slashings = chain.op_pool.get_all_attester_slashings(); + + // Use the current slot to find the fork version, and convert all messages to the + // current fork's format. This is to ensure consistent message types matching + // `Eth-Consensus-Version`. + let current_slot = + chain + .slot_clock + .now() + .ok_or(warp_utils::reject::custom_server_error( + "unable to read slot clock".to_string(), + ))?; + let fork_name = chain.spec.fork_name_at_slot::(current_slot); + let slashings = slashings + .into_iter() + .filter(|slashing| { + (fork_name.electra_enabled() + && matches!(slashing, AttesterSlashing::Electra(_))) + || (!fork_name.electra_enabled() + && matches!(slashing, AttesterSlashing::Base(_))) + }) + .collect::>(); + + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + + let res = beacon_response(require_version, &slashings); + Ok(add_consensus_version_header( + warp::reply::json(&res).into_response(), + fork_name, + )) + }) + }, + ) + .boxed() +} + +// POST beacon/pool/attester_slashings +pub fn post_beacon_pool_attester_slashings( + network_tx_filter: &NetworkTxFilter, + beacon_pool_path_any: &BeaconPoolPathAnyFilter, +) -> ResponseFilter { + beacon_pool_path_any + .clone() + .and(warp::path("attester_slashings")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + // V1 and V2 are identical except V2 has a consensus version header in the request. + // We only require this header for SSZ deserialization, which isn't supported for + // this endpoint presently. + |_endpoint_version: EndpointVersion, + task_spawner: TaskSpawner, + chain: Arc>, + slashing: AttesterSlashing, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let outcome = chain + .verify_attester_slashing_for_gossip(slashing.clone()) + .map_err(|e| { + warp_utils::reject::object_invalid(format!( + "gossip verification failed: {:?}", + e + )) + })?; + + // Notify the validator monitor. + chain + .validator_monitor + .read() + .register_api_attester_slashing(slashing.to_ref()); + + if let ObservationOutcome::New(slashing) = outcome { + utils::publish_pubsub_message( + &network_tx, + PubsubMessage::AttesterSlashing(Box::new( + slashing.clone().into_inner(), + )), + )?; + + chain.import_attester_slashing(slashing); + } + + Ok(()) + }) + }, + ) + .boxed() +} + +/// GET beacon/pool/attestations?committee_index,slot +pub fn get_beacon_pool_attestations( + beacon_pool_path_any: &BeaconPoolPathAnyFilter, +) -> ResponseFilter { + beacon_pool_path_any + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and(warp::query::()) + .then( + |endpoint_version: EndpointVersion, + task_spawner: TaskSpawner, + chain: Arc>, + query: AttestationPoolQuery| { + task_spawner.blocking_response_task(Priority::P1, move || { + let query_filter = |data: &AttestationData, committee_indices: HashSet| { + query.slot.is_none_or(|slot| slot == data.slot) + && query + .committee_index + .is_none_or(|index| committee_indices.contains(&index)) + }; + + let mut attestations = chain.op_pool.get_filtered_attestations(query_filter); + attestations.extend( + chain + .naive_aggregation_pool + .read() + .iter() + .filter(|&att| { + query_filter(att.data(), att.get_committee_indices_map()) + }) + .cloned(), + ); + // Use the current slot to find the fork version, and convert all messages to the + // current fork's format. This is to ensure consistent message types matching + // `Eth-Consensus-Version`. + let current_slot = + chain + .slot_clock + .now() + .ok_or(warp_utils::reject::custom_server_error( + "unable to read slot clock".to_string(), + ))?; + let fork_name = chain.spec.fork_name_at_slot::(current_slot); + let attestations = attestations + .into_iter() + .filter(|att| { + (fork_name.electra_enabled() && matches!(att, Attestation::Electra(_))) + || (!fork_name.electra_enabled() + && matches!(att, Attestation::Base(_))) + }) + .collect::>(); + + let require_version = match endpoint_version { + V1 => ResponseIncludesVersion::No, + V2 => ResponseIncludesVersion::Yes(fork_name), + _ => return Err(unsupported_version_rejection(endpoint_version)), + }; + + let res = beacon_response(require_version, &attestations); + Ok(add_consensus_version_header( + warp::reply::json(&res).into_response(), + fork_name, + )) + }) + }, + ) + .boxed() +} + +pub fn post_beacon_pool_attestations_v2( + network_tx_filter: &NetworkTxFilter, + optional_consensus_version_header_filter: OptionalConsensusVersionHeaderFilter, + beacon_pool_path_v2: &BeaconPoolPathV2Filter, +) -> ResponseFilter { + beacon_pool_path_v2 + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and(warp_utils::json::json::>()) + .and(optional_consensus_version_header_filter) + .and(network_tx_filter.clone()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + attestations: Vec, + _fork_name: Option, + network_tx: UnboundedSender>| async move { + let result = crate::publish_attestations::publish_attestations( + task_spawner, + chain, + attestations, + network_tx, + true, + ) + .await + .map(|()| warp::reply::json(&())); + convert_rejection(result).await + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs new file mode 100644 index 0000000000..6d06bcc77d --- /dev/null +++ b/beacon_node/http_api/src/beacon/states.rs @@ -0,0 +1,787 @@ +use crate::StateId; +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::ResponseFilter; +use crate::validator::pubkey_to_validator_index; +use crate::version::{ + ResponseIncludesVersion, add_consensus_version_header, + execution_optimistic_finalized_beacon_response, +}; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; +use eth2::types::{ + ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, + ValidatorsRequestBody, +}; +use std::sync::Arc; +use types::{ + AttestationShufflingId, CommitteeCache, Error as BeaconStateError, EthSpec, RelativeEpoch, +}; +use warp::filters::BoxedFilter; +use warp::{Filter, Reply}; +use warp_utils::query::multi_key_query; + +type BeaconStatesPath = BoxedFilter<( + StateId, + TaskSpawner<::EthSpec>, + Arc>, +)>; + +// GET beacon/states/{state_id}/pending_consolidations +pub fn get_beacon_state_pending_consolidations( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("pending_consolidations")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let Ok(consolidations) = state.pending_consolidations() else { + return Err(warp_utils::reject::custom_bad_request( + "Pending consolidations not found".to_string(), + )); + }; + + Ok(( + consolidations.clone(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) + }, + )?; + + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + data, + ) + .map(|res| warp::reply::json(&res).into_response()) + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/pending_partial_withdrawals +pub fn get_beacon_state_pending_partial_withdrawals( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("pending_partial_withdrawals")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let Ok(withdrawals) = state.pending_partial_withdrawals() else { + return Err(warp_utils::reject::custom_bad_request( + "Pending withdrawals not found".to_string(), + )); + }; + + Ok(( + withdrawals.clone(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) + }, + )?; + + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + data, + ) + .map(|res| warp::reply::json(&res).into_response()) + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/pending_deposits +pub fn get_beacon_state_pending_deposits( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("pending_deposits")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (data, execution_optimistic, finalized, fork_name) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let Ok(deposits) = state.pending_deposits() else { + return Err(warp_utils::reject::custom_bad_request( + "Pending deposits not found".to_string(), + )); + }; + + Ok(( + deposits.clone(), + execution_optimistic, + finalized, + state.fork_name_unchecked(), + )) + }, + )?; + + execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + data, + ) + .map(|res| warp::reply::json(&res).into_response()) + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/randao?epoch +pub fn get_beacon_state_randao( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("randao")) + .and(warp::query::()) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: eth2::types::RandaoQuery| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (randao, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let epoch = query.epoch.unwrap_or_else(|| state.current_epoch()); + let randao = *state.get_randao_mix(epoch).map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "epoch out of range: {e:?}" + )) + })?; + Ok((randao, execution_optimistic, finalized)) + }, + )?; + + Ok( + eth2::types::GenericResponse::from(eth2::types::RandaoMix { randao }) + .add_execution_optimistic_finalized(execution_optimistic, finalized), + ) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/sync_committees?epoch +pub fn get_beacon_state_sync_committees( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("sync_committees")) + .and(warp::query::()) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: eth2::types::SyncCommitteesQuery| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (sync_committee, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let current_epoch = state.current_epoch(); + let epoch = query.epoch.unwrap_or(current_epoch); + Ok(( + state + .get_built_sync_committee(epoch, &chain.spec) + .cloned() + .map_err(|e| match e { + BeaconStateError::SyncCommitteeNotKnown { .. } => { + warp_utils::reject::custom_bad_request(format!( + "state at epoch {} has no \ + sync committee for epoch {}", + current_epoch, epoch + )) + } + BeaconStateError::IncorrectStateVariant => { + warp_utils::reject::custom_bad_request(format!( + "state at epoch {} is not activated for Altair", + current_epoch, + )) + } + e => warp_utils::reject::beacon_state_error(e), + })?, + execution_optimistic, + finalized, + )) + }, + )?; + + let validators = chain + .validator_indices(sync_committee.pubkeys.iter()) + .map_err(warp_utils::reject::unhandled_error)?; + + let validator_aggregates = validators + .chunks_exact(T::EthSpec::sync_subcommittee_size()) + .map(|indices| eth2::types::SyncSubcommittee { + indices: indices.to_vec(), + }) + .collect(); + + let response = eth2::types::SyncCommitteeByValidatorIndices { + validators, + validator_aggregates, + }; + + Ok(eth2::types::GenericResponse::from(response) + .add_execution_optimistic_finalized(execution_optimistic, finalized)) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/committees?slot,index,epoch +pub fn get_beacon_state_committees( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("committees")) + .and(warp::query::()) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: eth2::types::CommitteesQuery| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let current_epoch = state.current_epoch(); + let epoch = query.epoch.unwrap_or(current_epoch); + + // Attempt to obtain the committee_cache from the beacon chain + let decision_slot = (epoch.saturating_sub(2u64)) + .end_slot(T::EthSpec::slots_per_epoch()); + // Find the decision block and skip to another method on any kind + // of failure + let shuffling_id = if let Ok(Some(shuffling_decision_block)) = + chain.block_root_at_slot(decision_slot, WhenSlotSkipped::Prev) + { + Some(AttestationShufflingId { + shuffling_epoch: epoch, + shuffling_decision_block, + }) + } else { + None + }; + + // Attempt to read from the chain cache if there exists a + // shuffling_id + let maybe_cached_shuffling = if let Some(shuffling_id) = + shuffling_id.as_ref() + { + chain + .shuffling_cache + .try_write_for(std::time::Duration::from_secs(1)) + .and_then(|mut cache_write| cache_write.get(shuffling_id)) + .and_then(|cache_item| cache_item.wait().ok()) + } else { + None + }; + + let committee_cache = + if let Some(shuffling) = maybe_cached_shuffling { + shuffling + } else { + let possibly_built_cache = + match RelativeEpoch::from_epoch(current_epoch, epoch) { + Ok(relative_epoch) + if state.committee_cache_is_initialized( + relative_epoch, + ) => + { + state.committee_cache(relative_epoch).cloned() + } + _ => CommitteeCache::initialized( + state, + epoch, + &chain.spec, + ), + } + .map_err( + |e| match e { + BeaconStateError::EpochOutOfBounds => { + let max_sprp = + T::EthSpec::slots_per_historical_root() + as u64; + let first_subsequent_restore_point_slot = + ((epoch.start_slot( + T::EthSpec::slots_per_epoch(), + ) / max_sprp) + + 1) + * max_sprp; + if epoch < current_epoch { + warp_utils::reject::custom_bad_request( + format!( + "epoch out of bounds, \ + try state at slot {}", + first_subsequent_restore_point_slot, + ), + ) + } else { + warp_utils::reject::custom_bad_request( + "epoch out of bounds, \ + too far in future" + .into(), + ) + } + } + _ => warp_utils::reject::unhandled_error( + BeaconChainError::from(e), + ), + }, + )?; + + // Attempt to write to the beacon cache (only if the cache + // size is not the default value). + if chain.config.shuffling_cache_size + != beacon_chain::shuffling_cache::DEFAULT_CACHE_SIZE + && let Some(shuffling_id) = shuffling_id + && let Some(mut cache_write) = chain + .shuffling_cache + .try_write_for(std::time::Duration::from_secs(1)) + { + cache_write.insert_committee_cache( + shuffling_id, + &possibly_built_cache, + ); + } + + possibly_built_cache + }; + + // Use either the supplied slot or all slots in the epoch. + let slots = + query.slot.map(|slot| vec![slot]).unwrap_or_else(|| { + epoch.slot_iter(T::EthSpec::slots_per_epoch()).collect() + }); + + // Use either the supplied committee index or all available indices. + let indices = + query.index.map(|index| vec![index]).unwrap_or_else(|| { + (0..committee_cache.committees_per_slot()).collect() + }); + + let mut response = Vec::with_capacity(slots.len() * indices.len()); + + for slot in slots { + // It is not acceptable to query with a slot that is not within the + // specified epoch. + if slot.epoch(T::EthSpec::slots_per_epoch()) != epoch { + return Err(warp_utils::reject::custom_bad_request( + format!("{} is not in epoch {}", slot, epoch), + )); + } + + for &index in &indices { + let committee = committee_cache + .get_beacon_committee(slot, index) + .ok_or_else(|| { + warp_utils::reject::custom_bad_request(format!( + "committee index {} does not exist in epoch {}", + index, epoch + )) + })?; + + response.push(eth2::types::CommitteeData { + index, + slot, + validators: committee + .committee + .iter() + .map(|i| *i as u64) + .collect(), + }); + } + } + + Ok((response, execution_optimistic, finalized)) + }, + )?; + Ok(eth2::types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/validators/{validator_id} +pub fn get_beacon_state_validators_id( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validators")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid validator ID".to_string(), + )) + })) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + validator_id: ValidatorId| { + // Prioritise requests for validators at the head. These should be fast to service + // and could be required by the validator client. + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let index_opt = match &validator_id { + ValidatorId::PublicKey(pubkey) => pubkey_to_validator_index( + &chain, state, pubkey, + ) + .map_err(|e| { + warp_utils::reject::custom_not_found(format!( + "unable to access pubkey cache: {e:?}", + )) + })?, + ValidatorId::Index(index) => Some(*index as usize), + }; + + Ok(( + index_opt + .and_then(|index| { + let validator = state.validators().get(index)?; + let balance = *state.balances().get(index)?; + let epoch = state.current_epoch(); + let far_future_epoch = chain.spec.far_future_epoch; + + Some(eth2::types::ValidatorData { + index: index as u64, + balance, + status: + eth2::types::ValidatorStatus::from_validator( + validator, + epoch, + far_future_epoch, + ), + validator: validator.clone(), + }) + }) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "unknown validator: {}", + validator_id + )) + })?, + execution_optimistic, + finalized, + )) + }, + )?; + + Ok(eth2::types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) + }) + }, + ) + .boxed() +} + +// POST beacon/states/{state_id}/validators +pub fn post_beacon_state_validators( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validators")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: ValidatorsRequestBody| { + // Prioritise requests for validators at the head. These should be fast to service + // and could be required by the validator client. + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + crate::validators::get_beacon_state_validators( + state_id, + chain, + &query.ids, + &query.statuses, + ) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/validators?id,status +pub fn get_beacon_state_validators( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validators")) + .and(warp::path::end()) + .and(multi_key_query::()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query_res: Result| { + // Prioritise requests for validators at the head. These should be fast to service + // and could be required by the validator client. + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + let query = query_res?; + crate::validators::get_beacon_state_validators( + state_id, + chain, + &query.id, + &query.status, + ) + }) + }, + ) + .boxed() +} + +// POST beacon/states/{state_id}/validator_identities +pub fn post_beacon_state_validator_identities( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validator_identities")) + .and(warp::path::end()) + .and(warp_utils::json::json_no_body()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: ValidatorIdentitiesRequestBody| { + // Prioritise requests for validators at the head. These should be fast to service + // and could be required by the validator client. + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + crate::validators::get_beacon_state_validator_identities( + state_id, + chain, + Some(&query.ids), + ) + }) + }, + ) + .boxed() +} + +// POST beacon/states/{state_id}/validator_balances +pub fn post_beacon_state_validator_balances( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validator_balances")) + .and(warp::path::end()) + .and(warp_utils::json::json_no_body()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: ValidatorBalancesRequestBody| { + task_spawner.blocking_json_task(Priority::P1, move || { + crate::validators::get_beacon_state_validator_balances( + state_id, + chain, + Some(&query.ids), + ) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/validator_balances?id +pub fn get_beacon_state_validator_balances( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("validator_balances")) + .and(warp::path::end()) + .and(multi_key_query::()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query_res: Result| { + task_spawner.blocking_json_task(Priority::P1, move || { + let query = query_res?; + crate::validators::get_beacon_state_validator_balances( + state_id, + chain, + query.id.as_deref(), + ) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/finality_checkpoints +pub fn get_beacon_state_finality_checkpoints( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("finality_checkpoints")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + Ok(( + eth2::types::FinalityCheckpointsData { + previous_justified: state.previous_justified_checkpoint(), + current_justified: state.current_justified_checkpoint(), + finalized: state.finalized_checkpoint(), + }, + execution_optimistic, + finalized, + )) + }, + )?; + + Ok(eth2::types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/fork +pub fn get_beacon_state_fork( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .clone() + .and(warp::path("fork")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (fork, execution_optimistic, finalized) = + state_id.fork_and_execution_optimistic_and_finalized(&chain)?; + Ok(eth2::types::ExecutionOptimisticFinalizedResponse { + data: fork, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) + }) + }, + ) + .boxed() +} + +// GET beacon/states/{state_id}/root +pub fn get_beacon_state_root( + beacon_states_path: BeaconStatesPath, +) -> ResponseFilter { + beacon_states_path + .and(warp::path("root")) + .and(warp::path::end()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (root, execution_optimistic, finalized) = state_id.root(&chain)?; + Ok(eth2::types::GenericResponse::from( + eth2::types::RootData::from(root), + )) + .map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index cdef1521ec..ea8b47f91e 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -1,13 +1,17 @@ -use crate::{state_id::checkpoint_slot_and_execution_optimistic, ExecutionOptimistic}; +use crate::version::inconsistent_fork_rejection; +use crate::{ExecutionOptimistic, state_id::checkpoint_slot_and_execution_optimistic}; use beacon_chain::kzg_utils::reconstruct_blobs; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; -use eth2::types::BlobIndicesQuery; +use eth2::beacon_response::{ExecutionOptimisticFinalizedMetadata, UnversionedResponse}; use eth2::types::BlockId as CoreBlockId; +use eth2::types::DataColumnIndicesQuery; +use eth2::types::{BlobIndicesQuery, BlobWrapper, BlobsVersionedHashesQuery}; +use fixed_bytes::FixedBytesExtended; use std::fmt; use std::str::FromStr; use std::sync::Arc; use types::{ - BlobSidecarList, EthSpec, FixedBytesExtended, Hash256, SignedBeaconBlock, + BlobSidecarList, DataColumnSidecarList, EthSpec, ForkName, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, }; use warp::Rejection; @@ -19,6 +23,13 @@ pub struct BlockId(pub CoreBlockId); type Finalized = bool; +type DataColumnsResponse = ( + DataColumnSidecarList<::EthSpec>, + ForkName, + ExecutionOptimistic, + Finalized, +); + impl BlockId { pub fn from_slot(slot: Slot) -> Self { Self(CoreBlockId::Slot(slot)) @@ -260,6 +271,47 @@ impl BlockId { } } + pub fn get_data_columns( + &self, + query: DataColumnIndicesQuery, + chain: &BeaconChain, + ) -> Result, Rejection> { + let (root, execution_optimistic, finalized) = self.root(chain)?; + let block = BlockId::blinded_block_by_root(&root, chain)?.ok_or_else(|| { + warp_utils::reject::custom_not_found(format!("beacon block with root {}", root)) + })?; + + if !chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + return Err(warp_utils::reject::custom_bad_request( + "block is pre-Fulu and has no data columns".to_string(), + )); + } + + let data_column_sidecars = if let Some(indices) = query.indices { + indices + .iter() + .filter_map(|index| chain.get_data_column(&root, index).transpose()) + .collect::, _>>() + .map_err(warp_utils::reject::unhandled_error)? + } else { + chain + .get_data_columns(&root) + .map_err(warp_utils::reject::unhandled_error)? + .unwrap_or_default() + }; + + let fork_name = block + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; + + Ok(( + data_column_sidecars, + fork_name, + execution_optimistic, + finalized, + )) + } + #[allow(clippy::type_complexity)] pub fn get_blinded_block_and_blob_list_filtered( &self, @@ -302,6 +354,68 @@ impl BlockId { Ok((block, blob_sidecar_list, execution_optimistic, finalized)) } + #[allow(clippy::type_complexity)] + pub fn get_blobs_by_versioned_hashes( + &self, + query: BlobsVersionedHashesQuery, + chain: &BeaconChain, + ) -> Result< + UnversionedResponse>, ExecutionOptimisticFinalizedMetadata>, + warp::Rejection, + > { + let (root, execution_optimistic, finalized) = self.root(chain)?; + let block = BlockId::blinded_block_by_root(&root, chain)?.ok_or_else(|| { + warp_utils::reject::custom_not_found(format!("beacon block with root {}", root)) + })?; + + // Error if the block is pre-Deneb and lacks blobs. + let blob_kzg_commitments = block.message().body().blob_kzg_commitments().map_err(|_| { + warp_utils::reject::custom_bad_request( + "block is pre-Deneb and has no blobs".to_string(), + ) + })?; + + let blob_indices_opt = query.versioned_hashes.map(|versioned_hashes| { + versioned_hashes + .iter() + .flat_map(|versioned_hash| { + blob_kzg_commitments.iter().position(|commitment| { + let computed_hash = commitment.calculate_versioned_hash(); + computed_hash == *versioned_hash + }) + }) + .map(|index| index as u64) + .collect::>() + }); + + let max_blobs_per_block = chain.spec.max_blobs_per_block(block.epoch()) as usize; + let blob_sidecar_list = if !blob_kzg_commitments.is_empty() { + if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + Self::get_blobs_from_data_columns(chain, root, blob_indices_opt, &block)? + } else { + Self::get_blobs(chain, root, blob_indices_opt, max_blobs_per_block)? + } + } else { + BlobSidecarList::new(vec![], max_blobs_per_block) + .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? + }; + + let blobs = blob_sidecar_list + .into_iter() + .map(|sidecar| BlobWrapper:: { + blob: sidecar.blob.clone(), + }) + .collect(); + + Ok(UnversionedResponse { + metadata: ExecutionOptimisticFinalizedMetadata { + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }, + data: blobs, + }) + } + fn get_blobs( chain: &BeaconChain, root: Hash256, @@ -319,9 +433,9 @@ impl BlockId { let blob_sidecar_list_filtered = match indices { Some(vec) => { - let list: Vec<_> = blob_sidecar_list + let list: Vec<_> = vec .into_iter() - .filter(|blob_sidecar| vec.contains(&blob_sidecar.index)) + .flat_map(|index| blob_sidecar_list.get(index as usize).cloned()) .collect(); BlobSidecarList::new(list, max_blobs_per_block) @@ -346,8 +460,8 @@ impl BlockId { })?; let num_found_column_keys = column_indices.len(); - let num_required_columns = chain.spec.number_of_columns / 2; - let is_blob_available = num_found_column_keys >= num_required_columns as usize; + let num_required_columns = T::EthSpec::number_of_columns() / 2; + let is_blob_available = num_found_column_keys >= num_required_columns; if is_blob_available { let data_columns = column_indices @@ -361,7 +475,7 @@ impl BlockId { ) .collect::, _>>()?; - reconstruct_blobs(&chain.kzg, &data_columns, blob_indices, block, &chain.spec).map_err( + reconstruct_blobs(&chain.kzg, data_columns, blob_indices, block, &chain.spec).map_err( |e| { warp_utils::reject::custom_server_error(format!( "Error reconstructing data columns: {e:?}" @@ -369,9 +483,9 @@ impl BlockId { }, ) } else { - Err(warp_utils::reject::custom_server_error( - format!("Insufficient data columns to reconstruct blobs: required {num_required_columns}, but only {num_found_column_keys} were found.") - )) + Err(warp_utils::reject::custom_server_error(format!( + "Insufficient data columns to reconstruct blobs: required {num_required_columns}, but only {num_found_column_keys} were found." + ))) } } } diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index 249a6732dc..3772470b28 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -4,7 +4,7 @@ use eth2::lighthouse::{ }; use parking_lot::Mutex; use state_processing::{ - per_epoch_processing::EpochProcessingSummary, BlockReplayError, BlockReplayer, + BlockReplayError, BlockReplayer, per_epoch_processing::EpochProcessingSummary, }; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 40b3815736..7c05dd00d2 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -21,15 +21,14 @@ pub fn get_next_withdrawals( // advance the state to the epoch of the proposal slot. let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); let (state_root, _, _) = state_id.root(chain)?; - if proposal_epoch != state.current_epoch() { - if let Err(e) = + if proposal_epoch != state.current_epoch() + && let Err(e) = partial_state_advance(&mut state, Some(state_root), proposal_slot, &chain.spec) - { - return Err(warp_utils::reject::custom_server_error(format!( - "failed to advance to the epoch of the proposal slot: {:?}", - e - ))); - } + { + return Err(warp_utils::reject::custom_server_error(format!( + "failed to advance to the epoch of the proposal slot: {:?}", + e + ))); } match get_expected_withdrawals(&state, &chain.spec) { diff --git a/beacon_node/http_api/src/custody.rs b/beacon_node/http_api/src/custody.rs new file mode 100644 index 0000000000..a43b55ceca --- /dev/null +++ b/beacon_node/http_api/src/custody.rs @@ -0,0 +1,53 @@ +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::lighthouse::CustodyInfo; +use std::sync::Arc; +use types::EthSpec; +use warp_utils::reject::{custom_bad_request, custom_server_error}; + +pub fn info( + chain: Arc>, +) -> Result { + if !chain.spec.is_fulu_scheduled() { + return Err(custom_bad_request("Fulu is not scheduled".to_string())); + } + + let opt_data_column_custody_info = chain + .store + .get_data_column_custody_info() + .map_err(|e| custom_server_error(format!("error reading DataColumnCustodyInfo: {e:?}")))?; + + let column_data_availability_boundary = chain + .column_data_availability_boundary() + .ok_or_else(|| custom_server_error("unreachable: Fulu should be enabled".to_string()))?; + + let earliest_custodied_data_column_slot = opt_data_column_custody_info + .and_then(|info| info.earliest_data_column_slot) + .unwrap_or_else(|| { + // If there's no data column custody info/earliest data column slot, it means *column* + // backfill is not running. Block backfill could still be running, so our earliest + // available column is either the oldest block slot or the DA boundary, whichever is + // more recent. + let oldest_block_slot = chain.store.get_anchor_info().oldest_block_slot; + column_data_availability_boundary + .start_slot(T::EthSpec::slots_per_epoch()) + .max(oldest_block_slot) + }); + let earliest_custodied_data_column_epoch = + earliest_custodied_data_column_slot.epoch(T::EthSpec::slots_per_epoch()); + + // Compute the custody columns and the CGC *at the earliest custodied slot*. The node might + // have some columns prior to this, but this value is the most up-to-date view of the data the + // node is custodying. + let custody_context = chain.data_availability_checker.custody_context(); + let custody_columns = custody_context + .custody_columns_for_epoch(Some(earliest_custodied_data_column_epoch), &chain.spec) + .to_vec(); + let custody_group_count = custody_context + .custody_group_count_at_epoch(earliest_custodied_data_column_epoch, &chain.spec); + + Ok(CustodyInfo { + earliest_custodied_data_column_slot, + custody_group_count, + custody_columns, + }) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 5fd19c20c5..59e21ad428 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -8,11 +8,13 @@ mod aggregate_attestation; mod attestation_performance; mod attester_duties; +mod beacon; mod block_id; mod block_packing_efficiency; mod block_rewards; mod build_block_contents; mod builder_states; +mod custody; mod database; mod inclusion_list_duties; mod light_client; @@ -30,47 +32,49 @@ mod sync_committees; mod task_spawner; pub mod test_utils; mod ui; +mod utils; mod validator; mod validator_inclusion; mod validators; mod version; + +use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::utils::{AnyVersionFilter, EthV1Filter}; +use crate::validator::post_validator_liveness_epoch; +use crate::validator::*; use crate::version::beacon_response; -use beacon_chain::{ - attestation_verification::VerifiedAttestation, observed_operations::ObservationOutcome, - validator_monitor::timestamp_now, AttestationError as AttnError, BeaconChain, BeaconChainError, - BeaconChainTypes, WhenSlotSkipped, -}; -use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; +use beacon::states; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; +use beacon_processor::BeaconProcessorSend; pub use block_id::BlockId; use builder_states::get_next_withdrawals; use bytes::Bytes; +use context_deserialize::ContextDeserialize; use directory::DEFAULT_ROOT_DIR; -use either::Either; +use eth2::StatusCode; +use eth2::lighthouse::sync_state::SyncState; use eth2::types::{ - self as api_types, BroadcastValidation, ContextDeserialize, EndpointVersion, ForkChoice, - ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, ValidatorBalancesRequestBody, - ValidatorId, ValidatorStatus, ValidatorsRequestBody, + self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceExtraData, + ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, ValidatorId, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use health_metrics::observe::Observe; -use lighthouse_network::rpc::methods::MetaData; -use lighthouse_network::{types::SyncState, Enr, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; +use lighthouse_network::Enr; +use lighthouse_network::NetworkGlobals; +use lighthouse_network::PeerId; use lighthouse_version::version_with_platform; -use logging::{crit, SSELoggingComponents}; -use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage}; -use operation_pool::ReceivedPreCapella; +use logging::{SSELoggingComponents, crit}; +use network::{NetworkMessage, NetworkSenders}; +use network_utils::enr_ext::EnrExt; use parking_lot::RwLock; pub use publish_blocks::{ - publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock, + ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block, }; use serde::{Deserialize, Serialize}; -use serde_json::Value; use slot_clock::SlotClock; use ssz::Encode; pub use state_id::StateId; -use std::collections::HashSet; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; @@ -80,36 +84,26 @@ use std::sync::Arc; use sysinfo::{System, SystemExt}; use system_health::{observe_nat, observe_system_health_bn}; use task_spawner::{Priority, TaskSpawner}; -use tokio::sync::{ - mpsc::{Sender, UnboundedSender}, - oneshot, -}; +use tokio::sync::mpsc::UnboundedSender; use tokio_stream::{ - wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}, StreamExt, + wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}, }; -use tracing::{debug, error, info, warn}; -use types::AttestationData; +use tracing::{debug, info, warn}; use types::{ - Attestation, AttestationShufflingId, AttesterSlashing, BeaconStateError, ChainSpec, Checkpoint, - CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, ProposerPreparationData, - ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBlindedBeaconBlock, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedInclusionList, - SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncCommitteeMessage, - SyncContributionData, + BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, + SignedBlindedBeaconBlock, Slot, }; -use validator::pubkey_to_validator_index; use version::{ - add_consensus_version_header, add_ssz_content_type_header, + ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, - unsupported_version_rejection, ResponseIncludesVersion, V1, V2, V3, + unsupported_version_rejection, }; -use warp::http::StatusCode; +use warp::Reply; use warp::hyper::Body; use warp::sse::Event; -use warp::Reply; -use warp::{http::Response, Filter, Rejection}; -use warp_utils::{query::multi_key_query, reject::convert_rejection, uor::UnifyingOrFilter}; +use warp::{Filter, Rejection, http::Response}; +use warp_utils::{query::multi_key_query, uor::UnifyingOrFilter}; const API_PREFIX: &str = "eth"; @@ -135,8 +129,6 @@ pub struct Context { pub network_senders: Option>, pub network_globals: Option>>, pub beacon_processor_send: Option>, - pub beacon_processor_reprocess_send: Option>, - pub eth1_service: Option, pub sse_logging_components: Option, } @@ -217,10 +209,12 @@ pub fn prometheus_metrics() -> warp::filters::log::Log warp::filters::log::Log( } // Create a filter that extracts the endpoint version. - let any_version = warp::path(API_PREFIX).and(warp::path::param::().or_else( - |_| async move { - Err(warp_utils::reject::custom_bad_request( - "Invalid version identifier".to_string(), - )) - }, - )); + let any_version = warp::path(API_PREFIX) + .and( + warp::path::param::().or_else(|_| async move { + Err(warp_utils::reject::custom_bad_request( + "Invalid version identifier".to_string(), + )) + }), + ) + .boxed(); // Filter that enforces a single endpoint version and then discards the `EndpointVersion`. - let single_version = |reqd: EndpointVersion| { + fn single_version(any_version: AnyVersionFilter, reqd: EndpointVersion) -> EthV1Filter { any_version .and_then(move |version| async move { if version == reqd { @@ -384,10 +377,11 @@ pub fn serve( } }) .untuple_one() - }; + .boxed() + } - let eth_v1 = single_version(V1); - let eth_v2 = single_version(V2); + let eth_v1 = single_version(any_version.clone(), V1); + let eth_v2 = single_version(any_version.clone(), V2); // Create a `warp` filter that provides access to the network globals. let inner_network_globals = ctx.network_globals.clone(); @@ -408,34 +402,34 @@ pub fn serve( // Create a `warp` filter that provides access to the beacon chain. let inner_ctx = ctx.clone(); - let chain_filter = - warp::any() - .map(move || inner_ctx.chain.clone()) - .and_then(|chain| async move { - match chain { - Some(chain) => Ok(chain), - None => Err(warp_utils::reject::custom_not_found( - "Beacon chain genesis has not yet been observed.".to_string(), - )), - } - }); + let chain_filter = warp::any() + .map(move || inner_ctx.chain.clone()) + .and_then(|chain| async move { + match chain { + Some(chain) => Ok(chain), + None => Err(warp_utils::reject::custom_not_found( + "Beacon chain genesis has not yet been observed.".to_string(), + )), + } + }) + .boxed(); // Create a `warp` filter that provides access to the network sender channel. let network_tx = ctx .network_senders .as_ref() .map(|senders| senders.network_send()); - let network_tx_filter = - warp::any() - .map(move || network_tx.clone()) - .and_then(|network_tx| async move { - match network_tx { - Some(network_tx) => Ok(network_tx), - None => Err(warp_utils::reject::custom_not_found( - "The networking stack has not yet started (network_tx).".to_string(), - )), - } - }); + let network_tx_filter = warp::any() + .map(move || network_tx.clone()) + .and_then(|network_tx| async move { + match network_tx { + Some(network_tx) => Ok(network_tx), + None => Err(warp_utils::reject::custom_not_found( + "The networking stack has not yet started (network_tx).".to_string(), + )), + } + }) + .boxed(); // Create a `warp` filter that provides access to the network attestation subscription channel. let validator_subscriptions_tx = ctx @@ -452,20 +446,8 @@ pub fn serve( .to_string(), )), } - }); - - // Create a `warp` filter that provides access to the Eth1 service. - let inner_ctx = ctx.clone(); - let eth1_service_filter = warp::any() - .map(move || inner_ctx.eth1_service.clone()) - .and_then(|eth1_service| async move { - match eth1_service { - Some(eth1_service) => Ok(eth1_service), - None => Err(warp_utils::reject::custom_not_found( - "The Eth1 service is not started. Use --eth1 on the CLI.".to_string(), - )), - } - }); + }) + .boxed(); // Create a `warp` filter that rejects requests whilst the node is syncing. let not_while_syncing_filter = @@ -476,7 +458,7 @@ pub fn serve( move |network_globals: Arc>, chain: Arc>| async move { match *network_globals.sync_state.read() { - SyncState::SyncingFinalized { .. } => { + SyncState::SyncingFinalized { .. } | SyncState::SyncingHead { .. } => { let head_slot = chain.canonical_head.cached_head().head_slot(); let current_slot = @@ -498,14 +480,15 @@ pub fn serve( ))) } } - SyncState::SyncingHead { .. } - | SyncState::SyncTransition - | SyncState::BackFillSyncing { .. } => Ok(()), + SyncState::SyncTransition + | SyncState::BackFillSyncing { .. } + | SyncState::CustodyBackFillSyncing { .. } => Ok(()), SyncState::Synced => Ok(()), SyncState::Stalled => Ok(()), } }, - ); + ) + .boxed(); // Create a `warp` filter that returns 404s if the light client server is disabled. let light_client_server_filter = @@ -558,13 +541,9 @@ pub fn serve( .beacon_processor_send .clone() .filter(|_| config.enable_beacon_processor); - let task_spawner_filter = - warp::any().map(move || TaskSpawner::new(beacon_processor_send.clone())); - let beacon_processor_reprocess_send = ctx - .beacon_processor_reprocess_send - .clone() - .filter(|_| config.enable_beacon_processor); - let reprocess_send_filter = warp::any().map(move || beacon_processor_reprocess_send.clone()); + let task_spawner_filter = warp::any() + .map(move || TaskSpawner::new(beacon_processor_send.clone())) + .boxed(); let duplicate_block_status_code = ctx.config.duplicate_block_status_code; @@ -576,6 +555,7 @@ pub fn serve( // GET beacon/genesis let get_beacon_genesis = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("genesis")) .and(warp::path::end()) @@ -599,6 +579,7 @@ pub fn serve( */ let beacon_states_path = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("states")) .and(warp::path::param::().or_else(|_| async { @@ -607,660 +588,65 @@ pub fn serve( )) })) .and(task_spawner_filter.clone()) - .and(chain_filter.clone()); + .and(chain_filter.clone()) + .boxed(); // GET beacon/states/{state_id}/root - let get_beacon_state_root = beacon_states_path - .clone() - .and(warp::path("root")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (root, execution_optimistic, finalized) = state_id.root(&chain)?; - Ok(api_types::GenericResponse::from(api_types::RootData::from( - root, - ))) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) - }) - }, - ); + let get_beacon_state_root = states::get_beacon_state_root(beacon_states_path.clone()); // GET beacon/states/{state_id}/fork - let get_beacon_state_fork = beacon_states_path - .clone() - .and(warp::path("fork")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (fork, execution_optimistic, finalized) = - state_id.fork_and_execution_optimistic_and_finalized(&chain)?; - Ok(api_types::ExecutionOptimisticFinalizedResponse { - data: fork, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) - }) - }, - ); + let get_beacon_state_fork = states::get_beacon_state_fork(beacon_states_path.clone()); // GET beacon/states/{state_id}/finality_checkpoints - let get_beacon_state_finality_checkpoints = beacon_states_path - .clone() - .and(warp::path("finality_checkpoints")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (data, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - Ok(( - api_types::FinalityCheckpointsData { - previous_justified: state.previous_justified_checkpoint(), - current_justified: state.current_justified_checkpoint(), - finalized: state.finalized_checkpoint(), - }, - execution_optimistic, - finalized, - )) - }, - )?; - - Ok(api_types::ExecutionOptimisticFinalizedResponse { - data, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) - }) - }, - ); + let get_beacon_state_finality_checkpoints = + states::get_beacon_state_finality_checkpoints(beacon_states_path.clone()); // GET beacon/states/{state_id}/validator_balances?id - let get_beacon_state_validator_balances = beacon_states_path - .clone() - .and(warp::path("validator_balances")) - .and(warp::path::end()) - .and(multi_key_query::()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query_res: Result| { - task_spawner.blocking_json_task(Priority::P1, move || { - let query = query_res?; - crate::validators::get_beacon_state_validator_balances( - state_id, - chain, - query.id.as_deref(), - ) - }) - }, - ); + let get_beacon_state_validator_balances = + states::get_beacon_state_validator_balances(beacon_states_path.clone()); // POST beacon/states/{state_id}/validator_balances - let post_beacon_state_validator_balances = beacon_states_path - .clone() - .and(warp::path("validator_balances")) - .and(warp::path::end()) - .and(warp_utils::json::json_no_body()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query: ValidatorBalancesRequestBody| { - task_spawner.blocking_json_task(Priority::P1, move || { - crate::validators::get_beacon_state_validator_balances( - state_id, - chain, - Some(&query.ids), - ) - }) - }, - ); + let post_beacon_state_validator_balances = + states::post_beacon_state_validator_balances(beacon_states_path.clone()); + + // POST beacon/states/{state_id}/validator_identities + let post_beacon_state_validator_identities = + states::post_beacon_state_validator_identities(beacon_states_path.clone()); // GET beacon/states/{state_id}/validators?id,status - let get_beacon_state_validators = beacon_states_path - .clone() - .and(warp::path("validators")) - .and(warp::path::end()) - .and(multi_key_query::()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query_res: Result| { - // Prioritise requests for validators at the head. These should be fast to service - // and could be required by the validator client. - let priority = if let StateId(eth2::types::StateId::Head) = state_id { - Priority::P0 - } else { - Priority::P1 - }; - task_spawner.blocking_json_task(priority, move || { - let query = query_res?; - crate::validators::get_beacon_state_validators( - state_id, - chain, - &query.id, - &query.status, - ) - }) - }, - ); + let get_beacon_state_validators = + states::get_beacon_state_validators(beacon_states_path.clone()); // POST beacon/states/{state_id}/validators - let post_beacon_state_validators = beacon_states_path - .clone() - .and(warp::path("validators")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query: ValidatorsRequestBody| { - // Prioritise requests for validators at the head. These should be fast to service - // and could be required by the validator client. - let priority = if let StateId(eth2::types::StateId::Head) = state_id { - Priority::P0 - } else { - Priority::P1 - }; - task_spawner.blocking_json_task(priority, move || { - crate::validators::get_beacon_state_validators( - state_id, - chain, - &query.ids, - &query.statuses, - ) - }) - }, - ); + let post_beacon_state_validators = + states::post_beacon_state_validators(beacon_states_path.clone()); // GET beacon/states/{state_id}/validators/{validator_id} - let get_beacon_state_validators_id = beacon_states_path - .clone() - .and(warp::path("validators")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid validator ID".to_string(), - )) - })) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - validator_id: ValidatorId| { - // Prioritise requests for validators at the head. These should be fast to service - // and could be required by the validator client. - let priority = if let StateId(eth2::types::StateId::Head) = state_id { - Priority::P0 - } else { - Priority::P1 - }; - task_spawner.blocking_json_task(priority, move || { - let (data, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let index_opt = match &validator_id { - ValidatorId::PublicKey(pubkey) => pubkey_to_validator_index( - &chain, state, pubkey, - ) - .map_err(|e| { - warp_utils::reject::custom_not_found(format!( - "unable to access pubkey cache: {e:?}", - )) - })?, - ValidatorId::Index(index) => Some(*index as usize), - }; - - Ok(( - index_opt - .and_then(|index| { - let validator = state.validators().get(index)?; - let balance = *state.balances().get(index)?; - let epoch = state.current_epoch(); - let far_future_epoch = chain.spec.far_future_epoch; - - Some(api_types::ValidatorData { - index: index as u64, - balance, - status: api_types::ValidatorStatus::from_validator( - validator, - epoch, - far_future_epoch, - ), - validator: validator.clone(), - }) - }) - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "unknown validator: {}", - validator_id - )) - })?, - execution_optimistic, - finalized, - )) - }, - )?; - - Ok(api_types::ExecutionOptimisticFinalizedResponse { - data, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) - }) - }, - ); + let get_beacon_state_validators_id = + states::get_beacon_state_validators_id(beacon_states_path.clone()); // GET beacon/states/{state_id}/committees?slot,index,epoch - let get_beacon_state_committees = beacon_states_path - .clone() - .and(warp::path("committees")) - .and(warp::query::()) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query: api_types::CommitteesQuery| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (data, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let current_epoch = state.current_epoch(); - let epoch = query.epoch.unwrap_or(current_epoch); - - // Attempt to obtain the committee_cache from the beacon chain - let decision_slot = (epoch.saturating_sub(2u64)) - .end_slot(T::EthSpec::slots_per_epoch()); - // Find the decision block and skip to another method on any kind - // of failure - let shuffling_id = if let Ok(Some(shuffling_decision_block)) = - chain.block_root_at_slot(decision_slot, WhenSlotSkipped::Prev) - { - Some(AttestationShufflingId { - shuffling_epoch: epoch, - shuffling_decision_block, - }) - } else { - None - }; - - // Attempt to read from the chain cache if there exists a - // shuffling_id - let maybe_cached_shuffling = if let Some(shuffling_id) = - shuffling_id.as_ref() - { - chain - .shuffling_cache - .try_write_for(std::time::Duration::from_secs(1)) - .and_then(|mut cache_write| cache_write.get(shuffling_id)) - .and_then(|cache_item| cache_item.wait().ok()) - } else { - None - }; - - let committee_cache = if let Some(shuffling) = - maybe_cached_shuffling - { - shuffling - } else { - let possibly_built_cache = - match RelativeEpoch::from_epoch(current_epoch, epoch) { - Ok(relative_epoch) - if state.committee_cache_is_initialized( - relative_epoch, - ) => - { - state.committee_cache(relative_epoch).cloned() - } - _ => CommitteeCache::initialized( - state, - epoch, - &chain.spec, - ), - } - .map_err(|e| { - match e { - BeaconStateError::EpochOutOfBounds => { - let max_sprp = - T::EthSpec::slots_per_historical_root() - as u64; - let first_subsequent_restore_point_slot = - ((epoch.start_slot( - T::EthSpec::slots_per_epoch(), - ) / max_sprp) - + 1) - * max_sprp; - if epoch < current_epoch { - warp_utils::reject::custom_bad_request( - format!( - "epoch out of bounds, \ - try state at slot {}", - first_subsequent_restore_point_slot, - ), - ) - } else { - warp_utils::reject::custom_bad_request( - "epoch out of bounds, \ - too far in future" - .into(), - ) - } - } - _ => warp_utils::reject::unhandled_error( - BeaconChainError::from(e), - ), - } - })?; - - // Attempt to write to the beacon cache (only if the cache - // size is not the default value). - if chain.config.shuffling_cache_size - != beacon_chain::shuffling_cache::DEFAULT_CACHE_SIZE - { - if let Some(shuffling_id) = shuffling_id { - if let Some(mut cache_write) = chain - .shuffling_cache - .try_write_for(std::time::Duration::from_secs(1)) - { - cache_write.insert_committee_cache( - shuffling_id, - &possibly_built_cache, - ); - } - } - } - possibly_built_cache - }; - - // Use either the supplied slot or all slots in the epoch. - let slots = - query.slot.map(|slot| vec![slot]).unwrap_or_else(|| { - epoch.slot_iter(T::EthSpec::slots_per_epoch()).collect() - }); - - // Use either the supplied committee index or all available indices. - let indices = - query.index.map(|index| vec![index]).unwrap_or_else(|| { - (0..committee_cache.committees_per_slot()).collect() - }); - - let mut response = Vec::with_capacity(slots.len() * indices.len()); - - for slot in slots { - // It is not acceptable to query with a slot that is not within the - // specified epoch. - if slot.epoch(T::EthSpec::slots_per_epoch()) != epoch { - return Err(warp_utils::reject::custom_bad_request( - format!("{} is not in epoch {}", slot, epoch), - )); - } - - for &index in &indices { - let committee = committee_cache - .get_beacon_committee(slot, index) - .ok_or_else(|| { - warp_utils::reject::custom_bad_request(format!( - "committee index {} does not exist in epoch {}", - index, epoch - )) - })?; - - response.push(api_types::CommitteeData { - index, - slot, - validators: committee - .committee - .iter() - .map(|i| *i as u64) - .collect(), - }); - } - } - - Ok((response, execution_optimistic, finalized)) - }, - )?; - Ok(api_types::ExecutionOptimisticFinalizedResponse { - data, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) - }) - }, - ); + let get_beacon_state_committees = + states::get_beacon_state_committees(beacon_states_path.clone()); // GET beacon/states/{state_id}/sync_committees?epoch - let get_beacon_state_sync_committees = beacon_states_path - .clone() - .and(warp::path("sync_committees")) - .and(warp::query::()) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query: api_types::SyncCommitteesQuery| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (sync_committee, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let current_epoch = state.current_epoch(); - let epoch = query.epoch.unwrap_or(current_epoch); - Ok(( - state - .get_built_sync_committee(epoch, &chain.spec) - .cloned() - .map_err(|e| match e { - BeaconStateError::SyncCommitteeNotKnown { .. } => { - warp_utils::reject::custom_bad_request(format!( - "state at epoch {} has no \ - sync committee for epoch {}", - current_epoch, epoch - )) - } - BeaconStateError::IncorrectStateVariant => { - warp_utils::reject::custom_bad_request(format!( - "state at epoch {} is not activated for Altair", - current_epoch, - )) - } - e => warp_utils::reject::beacon_state_error(e), - })?, - execution_optimistic, - finalized, - )) - }, - )?; - - let validators = chain - .validator_indices(sync_committee.pubkeys.iter()) - .map_err(warp_utils::reject::unhandled_error)?; - - let validator_aggregates = validators - .chunks_exact(T::EthSpec::sync_subcommittee_size()) - .map(|indices| api_types::SyncSubcommittee { - indices: indices.to_vec(), - }) - .collect(); - - let response = api_types::SyncCommitteeByValidatorIndices { - validators, - validator_aggregates, - }; - - Ok(api_types::GenericResponse::from(response) - .add_execution_optimistic_finalized(execution_optimistic, finalized)) - }) - }, - ); + let get_beacon_state_sync_committees = + states::get_beacon_state_sync_committees(beacon_states_path.clone()); // GET beacon/states/{state_id}/randao?epoch - let get_beacon_state_randao = beacon_states_path - .clone() - .and(warp::path("randao")) - .and(warp::query::()) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>, - query: api_types::RandaoQuery| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (randao, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let epoch = query.epoch.unwrap_or_else(|| state.current_epoch()); - let randao = *state.get_randao_mix(epoch).map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "epoch out of range: {e:?}" - )) - })?; - Ok((randao, execution_optimistic, finalized)) - }, - )?; - - Ok( - api_types::GenericResponse::from(api_types::RandaoMix { randao }) - .add_execution_optimistic_finalized(execution_optimistic, finalized), - ) - }) - }, - ); + let get_beacon_state_randao = states::get_beacon_state_randao(beacon_states_path.clone()); // GET beacon/states/{state_id}/pending_deposits - let get_beacon_state_pending_deposits = beacon_states_path - .clone() - .and(warp::path("pending_deposits")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_response_task(Priority::P1, move || { - let (data, execution_optimistic, finalized, fork_name) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let Ok(deposits) = state.pending_deposits() else { - return Err(warp_utils::reject::custom_bad_request( - "Pending deposits not found".to_string(), - )); - }; - - Ok(( - deposits.clone(), - execution_optimistic, - finalized, - state.fork_name_unchecked(), - )) - }, - )?; - - execution_optimistic_finalized_beacon_response( - ResponseIncludesVersion::Yes(fork_name), - execution_optimistic, - finalized, - data, - ) - .map(|res| warp::reply::json(&res).into_response()) - .map(|resp| add_consensus_version_header(resp, fork_name)) - }) - }, - ); + let get_beacon_state_pending_deposits = + states::get_beacon_state_pending_deposits(beacon_states_path.clone()); // GET beacon/states/{state_id}/pending_partial_withdrawals - let get_beacon_state_pending_partial_withdrawals = beacon_states_path - .clone() - .and(warp::path("pending_partial_withdrawals")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_response_task(Priority::P1, move || { - let (data, execution_optimistic, finalized, fork_name) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let Ok(withdrawals) = state.pending_partial_withdrawals() else { - return Err(warp_utils::reject::custom_bad_request( - "Pending withdrawals not found".to_string(), - )); - }; - - Ok(( - withdrawals.clone(), - execution_optimistic, - finalized, - state.fork_name_unchecked(), - )) - }, - )?; - - execution_optimistic_finalized_beacon_response( - ResponseIncludesVersion::Yes(fork_name), - execution_optimistic, - finalized, - data, - ) - .map(|res| warp::reply::json(&res).into_response()) - .map(|resp| add_consensus_version_header(resp, fork_name)) - }) - }, - ); + let get_beacon_state_pending_partial_withdrawals = + states::get_beacon_state_pending_partial_withdrawals(beacon_states_path.clone()); // GET beacon/states/{state_id}/pending_consolidations - let get_beacon_state_pending_consolidations = beacon_states_path - .clone() - .and(warp::path("pending_consolidations")) - .and(warp::path::end()) - .then( - |state_id: StateId, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let (data, execution_optimistic, finalized) = state_id - .map_state_and_execution_optimistic_and_finalized( - &chain, - |state, execution_optimistic, finalized| { - let Ok(consolidations) = state.pending_consolidations() else { - return Err(warp_utils::reject::custom_bad_request( - "Pending consolidations not found".to_string(), - )); - }; - - Ok((consolidations.clone(), execution_optimistic, finalized)) - }, - )?; - - Ok(api_types::ExecutionOptimisticFinalizedResponse { - data, - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - }) - }) - }, - ); + let get_beacon_state_pending_consolidations = + states::get_beacon_state_pending_consolidations(beacon_states_path.clone()); // GET beacon/headers // @@ -1270,6 +656,7 @@ pub fn serve( // mechanism for arbitrary forwards block iteration, we only support iterating forwards along // the canonical chain. let get_beacon_headers = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("headers")) .and(warp::query::()) @@ -1336,13 +723,13 @@ pub fn serve( // If the parent root was supplied, check that it matches the block // obtained via a slot lookup. - if let Some(parent_root) = parent_root_opt { - if block.parent_root() != parent_root { - return Err(warp_utils::reject::custom_not_found(format!( - "no canonical block at slot {} with parent root {}", - slot, parent_root - ))); - } + if let Some(parent_root) = parent_root_opt + && block.parent_root() != parent_root + { + return Err(warp_utils::reject::custom_not_found(format!( + "no canonical block at slot {} with parent root {}", + slot, parent_root + ))); } (root, block, execution_optimistic, finalized) @@ -1366,6 +753,7 @@ pub fn serve( // GET beacon/headers/{block_id} let get_beacon_headers_block_id = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("headers")) .and(warp::path::param::().or_else(|_| async { @@ -1414,29 +802,28 @@ pub fn serve( * beacon/blocks */ let consensus_version_header_filter = - warp::header::header::(CONSENSUS_VERSION_HEADER); + warp::header::header::(CONSENSUS_VERSION_HEADER).boxed(); let optional_consensus_version_header_filter = - warp::header::optional::(CONSENSUS_VERSION_HEADER); + warp::header::optional::(CONSENSUS_VERSION_HEADER).boxed(); // POST beacon/blocks let post_beacon_blocks = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(warp::path::end()) .and(warp::body::json()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |value: serde_json::Value, consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let request = PublishBlockRequest::::context_deserialize( &value, @@ -1452,7 +839,6 @@ pub fn serve( &network_tx, BroadcastValidation::default(), duplicate_block_status_code, - network_globals, ) .await }) @@ -1460,22 +846,21 @@ pub fn serve( ); let post_beacon_blocks_ssz = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(warp::path::end()) .and(warp::body::bytes()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |block_bytes: Bytes, consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( &block_bytes, @@ -1491,7 +876,6 @@ pub fn serve( &network_tx, BroadcastValidation::default(), duplicate_block_status_code, - network_globals, ) .await }) @@ -1499,24 +883,23 @@ pub fn serve( ); let post_beacon_blocks_v2 = eth_v2 + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(warp::query::()) .and(warp::path::end()) .and(warp::body::json()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, value: serde_json::Value, consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let request = PublishBlockRequest::::context_deserialize( &value, @@ -1533,7 +916,6 @@ pub fn serve( &network_tx, validation_level.broadcast_validation, duplicate_block_status_code, - network_globals, ) .await }) @@ -1541,24 +923,23 @@ pub fn serve( ); let post_beacon_blocks_v2_ssz = eth_v2 + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(warp::query::()) .and(warp::path::end()) .and(warp::body::bytes()) - .and(consensus_version_header_filter) + .and(consensus_version_header_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, block_bytes: Bytes, consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( &block_bytes, @@ -1574,7 +955,6 @@ pub fn serve( &network_tx, validation_level.broadcast_validation, duplicate_block_status_code, - network_globals, ) .await }) @@ -1587,6 +967,7 @@ pub fn serve( // POST beacon/blinded_blocks let post_beacon_blinded_blocks = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(warp::path::end()) @@ -1594,13 +975,11 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |block_contents: Arc>, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_blinded_block( block_contents, @@ -1608,7 +987,6 @@ pub fn serve( &network_tx, BroadcastValidation::default(), duplicate_block_status_code, - network_globals, ) .await }) @@ -1617,6 +995,7 @@ pub fn serve( // POST beacon/blocks let post_beacon_blinded_blocks_ssz = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(warp::path::end()) @@ -1624,13 +1003,11 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |block_bytes: Bytes, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( &block_bytes, @@ -1646,7 +1023,6 @@ pub fn serve( &network_tx, BroadcastValidation::default(), duplicate_block_status_code, - network_globals, ) .await }) @@ -1654,30 +1030,39 @@ pub fn serve( ); let post_beacon_blinded_blocks_v2 = eth_v2 + .clone() .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(warp::query::()) .and(warp::path::end()) .and(warp_utils::json::json()) + .and(consensus_version_header_filter) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, - blinded_block: Arc>, + blinded_block_json: serde_json::Value, + consensus_version: ForkName, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let blinded_block = + SignedBlindedBeaconBlock::::context_deserialize( + &blinded_block_json, + consensus_version, + ) + .map(Arc::new) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid JSON: {e:?}")) + })?; publish_blocks::publish_blinded_block( blinded_block, chain, &network_tx, validation_level.broadcast_validation, duplicate_block_status_code, - network_globals, ) .await }) @@ -1685,6 +1070,7 @@ pub fn serve( ); let post_beacon_blinded_blocks_v2_ssz = eth_v2 + .clone() .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(warp::query::()) @@ -1693,14 +1079,12 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) - .and(network_globals.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, block_bytes: Bytes, task_spawner: TaskSpawner, chain: Arc>, - network_tx: UnboundedSender>, - network_globals: Arc>| { + network_tx: UnboundedSender>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( &block_bytes, @@ -1716,7 +1100,6 @@ pub fn serve( &network_tx, validation_level.broadcast_validation, duplicate_block_status_code, - network_globals, ) .await }) @@ -1730,6 +1113,7 @@ pub fn serve( }); let beacon_blocks_path_v1 = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(block_id_or_err) @@ -1737,6 +1121,7 @@ pub fn serve( .and(chain_filter.clone()); let beacon_blocks_path_any = any_version + .clone() .and(warp::path("beacon")) .and(warp::path("blocks")) .and(block_id_or_err) @@ -1862,6 +1247,7 @@ pub fn serve( // GET beacon/blinded_blocks/{block_id} let get_beacon_blinded_block = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(block_id_or_err) @@ -1913,7 +1299,8 @@ pub fn serve( */ // GET beacon/blob_sidecars/{block_id} - let get_blobs = eth_v1 + let get_blob_sidecars = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("blob_sidecars")) .and(block_id_or_err) @@ -1963,499 +1350,123 @@ pub fn serve( }, ); + // GET beacon/blobs/{block_id} + let get_blobs = eth_v1 + .clone() + .and(warp::path("beacon")) + .and(warp::path("blobs")) + .and(block_id_or_err) + .and(warp::path::end()) + .and(multi_key_query::()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp::header::optional::("accept")) + .then( + |block_id: BlockId, + version_hashes_res: Result, + task_spawner: TaskSpawner, + chain: Arc>, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let versioned_hashes = version_hashes_res?; + let response = + block_id.get_blobs_by_versioned_hashes(versioned_hashes, &chain)?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(response.data.as_ssz_bytes().into()) + .map(|res: Response| add_ssz_content_type_header(res)) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => { + let res = execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::No, + response.metadata.execution_optimistic.unwrap_or(false), + response.metadata.finalized.unwrap_or(false), + response.data, + )?; + Ok(warp::reply::json(&res).into_response()) + } + } + }) + }, + ); + /* * beacon/pool */ let beacon_pool_path = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("pool")) .and(task_spawner_filter.clone()) - .and(chain_filter.clone()); + .and(chain_filter.clone()) + .boxed(); let beacon_pool_path_v2 = eth_v2 + .clone() .and(warp::path("beacon")) .and(warp::path("pool")) .and(task_spawner_filter.clone()) - .and(chain_filter.clone()); + .and(chain_filter.clone()) + .boxed(); let beacon_pool_path_any = any_version + .clone() .and(warp::path("beacon")) .and(warp::path("pool")) .and(task_spawner_filter.clone()) - .and(chain_filter.clone()); + .and(chain_filter.clone()) + .boxed(); - let post_beacon_pool_attestations_v1 = beacon_pool_path - .clone() - .and(warp::path("attestations")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .and(reprocess_send_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - attestations: Vec>, - network_tx: UnboundedSender>, - reprocess_tx: Option>| async move { - let attestations = attestations.into_iter().map(Either::Left).collect(); - let result = crate::publish_attestations::publish_attestations( - task_spawner, - chain, - attestations, - network_tx, - reprocess_tx, - ) - .await - .map(|()| warp::reply::json(&())); - convert_rejection(result).await - }, - ); - - let post_beacon_pool_attestations_v2 = beacon_pool_path_v2 - .clone() - .and(warp::path("attestations")) - .and(warp::path::end()) - .and(warp_utils::json::json::()) - .and(optional_consensus_version_header_filter) - .and(network_tx_filter.clone()) - .and(reprocess_send_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - payload: Value, - fork_name: Option, - network_tx: UnboundedSender>, - reprocess_tx: Option>| async move { - let attestations = - match crate::publish_attestations::deserialize_attestation_payload::( - payload, fork_name, - ) { - Ok(attestations) => attestations, - Err(err) => { - warn!( - error = ?err, - "Unable to deserialize attestation POST request" - ); - return warp::reply::with_status( - warp::reply::json( - &"Unable to deserialize request body".to_string(), - ), - eth2::StatusCode::BAD_REQUEST, - ) - .into_response(); - } - }; - - let result = crate::publish_attestations::publish_attestations( - task_spawner, - chain, - attestations, - network_tx, - reprocess_tx, - ) - .await - .map(|()| warp::reply::json(&())); - convert_rejection(result).await - }, - ); + let post_beacon_pool_attestations_v2 = post_beacon_pool_attestations_v2( + &network_tx_filter, + optional_consensus_version_header_filter, + &beacon_pool_path_v2, + ); // GET beacon/pool/attestations?committee_index,slot - let get_beacon_pool_attestations = beacon_pool_path_any - .clone() - .and(warp::path("attestations")) - .and(warp::path::end()) - .and(warp::query::()) - .then( - |endpoint_version: EndpointVersion, - task_spawner: TaskSpawner, - chain: Arc>, - query: api_types::AttestationPoolQuery| { - task_spawner.blocking_response_task(Priority::P1, move || { - let query_filter = |data: &AttestationData, committee_indices: HashSet| { - query.slot.is_none_or(|slot| slot == data.slot) - && query - .committee_index - .is_none_or(|index| committee_indices.contains(&index)) - }; - - let mut attestations = chain.op_pool.get_filtered_attestations(query_filter); - attestations.extend( - chain - .naive_aggregation_pool - .read() - .iter() - .filter(|&att| { - query_filter(att.data(), att.get_committee_indices_map()) - }) - .cloned(), - ); - // Use the current slot to find the fork version, and convert all messages to the - // current fork's format. This is to ensure consistent message types matching - // `Eth-Consensus-Version`. - let current_slot = - chain - .slot_clock - .now() - .ok_or(warp_utils::reject::custom_server_error( - "unable to read slot clock".to_string(), - ))?; - let fork_name = chain.spec.fork_name_at_slot::(current_slot); - let attestations = attestations - .into_iter() - .filter(|att| { - (fork_name.electra_enabled() && matches!(att, Attestation::Electra(_))) - || (!fork_name.electra_enabled() - && matches!(att, Attestation::Base(_))) - }) - .collect::>(); - - let require_version = match endpoint_version { - V1 => ResponseIncludesVersion::No, - V2 => ResponseIncludesVersion::Yes(fork_name), - _ => return Err(unsupported_version_rejection(endpoint_version)), - }; - - let res = beacon_response(require_version, &attestations); - Ok(add_consensus_version_header( - warp::reply::json(&res).into_response(), - fork_name, - )) - }) - }, - ); + let get_beacon_pool_attestations = get_beacon_pool_attestations(&beacon_pool_path_any); // POST beacon/pool/attester_slashings - let post_beacon_pool_attester_slashings = beacon_pool_path_any - .clone() - .and(warp::path("attester_slashings")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - // V1 and V2 are identical except V2 has a consensus version header in the request. - // We only require this header for SSZ deserialization, which isn't supported for - // this endpoint presently. - |_endpoint_version: EndpointVersion, - task_spawner: TaskSpawner, - chain: Arc>, - slashing: AttesterSlashing, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - let outcome = chain - .verify_attester_slashing_for_gossip(slashing.clone()) - .map_err(|e| { - warp_utils::reject::object_invalid(format!( - "gossip verification failed: {:?}", - e - )) - })?; - - // Notify the validator monitor. - chain - .validator_monitor - .read() - .register_api_attester_slashing(slashing.to_ref()); - - if let ObservationOutcome::New(slashing) = outcome { - publish_pubsub_message( - &network_tx, - PubsubMessage::AttesterSlashing(Box::new( - slashing.clone().into_inner(), - )), - )?; - - chain.import_attester_slashing(slashing); - } - - Ok(()) - }) - }, - ); + let post_beacon_pool_attester_slashings = + post_beacon_pool_attester_slashings(&network_tx_filter, &beacon_pool_path_any); // GET beacon/pool/attester_slashings let get_beacon_pool_attester_slashings = - beacon_pool_path_any - .clone() - .and(warp::path("attester_slashings")) - .and(warp::path::end()) - .then( - |endpoint_version: EndpointVersion, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_response_task(Priority::P1, move || { - let slashings = chain.op_pool.get_all_attester_slashings(); - - // Use the current slot to find the fork version, and convert all messages to the - // current fork's format. This is to ensure consistent message types matching - // `Eth-Consensus-Version`. - let current_slot = chain.slot_clock.now().ok_or( - warp_utils::reject::custom_server_error( - "unable to read slot clock".to_string(), - ), - )?; - let fork_name = chain.spec.fork_name_at_slot::(current_slot); - let slashings = slashings - .into_iter() - .filter(|slashing| { - (fork_name.electra_enabled() - && matches!(slashing, AttesterSlashing::Electra(_))) - || (!fork_name.electra_enabled() - && matches!(slashing, AttesterSlashing::Base(_))) - }) - .collect::>(); - - let require_version = match endpoint_version { - V1 => ResponseIncludesVersion::No, - V2 => ResponseIncludesVersion::Yes(fork_name), - _ => return Err(unsupported_version_rejection(endpoint_version)), - }; - - let res = beacon_response(require_version, &slashings); - Ok(add_consensus_version_header( - warp::reply::json(&res).into_response(), - fork_name, - )) - }) - }, - ); + get_beacon_pool_attester_slashings(&beacon_pool_path_any); // POST beacon/pool/proposer_slashings - let post_beacon_pool_proposer_slashings = beacon_pool_path - .clone() - .and(warp::path("proposer_slashings")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - slashing: ProposerSlashing, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - let outcome = chain - .verify_proposer_slashing_for_gossip(slashing.clone()) - .map_err(|e| { - warp_utils::reject::object_invalid(format!( - "gossip verification failed: {:?}", - e - )) - })?; - - // Notify the validator monitor. - chain - .validator_monitor - .read() - .register_api_proposer_slashing(&slashing); - - if let ObservationOutcome::New(slashing) = outcome { - publish_pubsub_message( - &network_tx, - PubsubMessage::ProposerSlashing(Box::new( - slashing.clone().into_inner(), - )), - )?; - - chain.import_proposer_slashing(slashing); - } - - Ok(()) - }) - }, - ); + let post_beacon_pool_proposer_slashings = + post_beacon_pool_proposer_slashings(&network_tx_filter, &beacon_pool_path); // GET beacon/pool/proposer_slashings - let get_beacon_pool_proposer_slashings = beacon_pool_path - .clone() - .and(warp::path("proposer_slashings")) - .and(warp::path::end()) - .then( - |task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let attestations = chain.op_pool.get_all_proposer_slashings(); - Ok(api_types::GenericResponse::from(attestations)) - }) - }, - ); + let get_beacon_pool_proposer_slashings = get_beacon_pool_proposer_slashings(&beacon_pool_path); // POST beacon/pool/voluntary_exits - let post_beacon_pool_voluntary_exits = beacon_pool_path - .clone() - .and(warp::path("voluntary_exits")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - exit: SignedVoluntaryExit, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - let outcome = chain - .verify_voluntary_exit_for_gossip(exit.clone()) - .map_err(|e| { - warp_utils::reject::object_invalid(format!( - "gossip verification failed: {:?}", - e - )) - })?; - - // Notify the validator monitor. - chain - .validator_monitor - .read() - .register_api_voluntary_exit(&exit.message); - - if let ObservationOutcome::New(exit) = outcome { - publish_pubsub_message( - &network_tx, - PubsubMessage::VoluntaryExit(Box::new(exit.clone().into_inner())), - )?; - - chain.import_voluntary_exit(exit); - } - - Ok(()) - }) - }, - ); + let post_beacon_pool_voluntary_exits = + post_beacon_pool_voluntary_exits(&network_tx_filter, &beacon_pool_path); // GET beacon/pool/voluntary_exits - let get_beacon_pool_voluntary_exits = beacon_pool_path - .clone() - .and(warp::path("voluntary_exits")) - .and(warp::path::end()) - .then( - |task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let attestations = chain.op_pool.get_all_voluntary_exits(); - Ok(api_types::GenericResponse::from(attestations)) - }) - }, - ); + let get_beacon_pool_voluntary_exits = get_beacon_pool_voluntary_exits(&beacon_pool_path); // POST beacon/pool/sync_committees - let post_beacon_pool_sync_committees = beacon_pool_path - .clone() - .and(warp::path("sync_committees")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - signatures: Vec, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - sync_committees::process_sync_committee_signatures( - signatures, network_tx, &chain, - )?; - Ok(api_types::GenericResponse::from(())) - }) - }, - ); + let post_beacon_pool_sync_committees = + post_beacon_pool_sync_committees(&network_tx_filter, &beacon_pool_path); // GET beacon/pool/bls_to_execution_changes - let get_beacon_pool_bls_to_execution_changes = beacon_pool_path - .clone() - .and(warp::path("bls_to_execution_changes")) - .and(warp::path::end()) - .then( - |task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let address_changes = chain.op_pool.get_all_bls_to_execution_changes(); - Ok(api_types::GenericResponse::from(address_changes)) - }) - }, - ); + let get_beacon_pool_bls_to_execution_changes = + get_beacon_pool_bls_to_execution_changes(&beacon_pool_path); // POST beacon/pool/bls_to_execution_changes - let post_beacon_pool_bls_to_execution_changes = beacon_pool_path - .clone() - .and(warp::path("bls_to_execution_changes")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - address_changes: Vec, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - let mut failures = vec![]; - - for (index, address_change) in address_changes.into_iter().enumerate() { - let validator_index = address_change.message.validator_index; - - match chain.verify_bls_to_execution_change_for_http_api(address_change) { - Ok(ObservationOutcome::New(verified_address_change)) => { - let validator_index = - verified_address_change.as_inner().message.validator_index; - let address = verified_address_change - .as_inner() - .message - .to_execution_address; - - // New to P2P *and* op pool, gossip immediately if post-Capella. - let received_pre_capella = - if chain.current_slot_is_post_capella().unwrap_or(false) { - ReceivedPreCapella::No - } else { - ReceivedPreCapella::Yes - }; - if matches!(received_pre_capella, ReceivedPreCapella::No) { - publish_pubsub_message( - &network_tx, - PubsubMessage::BlsToExecutionChange(Box::new( - verified_address_change.as_inner().clone(), - )), - )?; - } - - // Import to op pool (may return `false` if there's a race). - let imported = chain.import_bls_to_execution_change( - verified_address_change, - received_pre_capella, - ); - - info!( - %validator_index, - ?address, - published = - matches!(received_pre_capella, ReceivedPreCapella::No), - imported, - "Processed BLS to execution change" - ); - } - Ok(ObservationOutcome::AlreadyKnown) => { - debug!(%validator_index, "BLS to execution change already known"); - } - Err(e) => { - warn!( - validator_index, - reason = ?e, - source = "HTTP", - "Invalid BLS to execution change" - ); - failures.push(api_types::Failure::new( - index, - format!("invalid: {e:?}"), - )); - } - } - } - - if failures.is_empty() { - Ok(()) - } else { - Err(warp_utils::reject::indexed_bad_request( - "some BLS to execution changes failed to verify".into(), - failures, - )) - } - }) - }, - ); + let post_beacon_pool_bls_to_execution_changes = + post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); // POST beacon/pool/inclusion_lists let post_beacon_pool_inclusion_lists = beacon_pool_path @@ -2485,57 +1496,8 @@ pub fn serve( }, ); - // GET beacon/deposit_snapshot - let get_beacon_deposit_snapshot = eth_v1 - .and(warp::path("beacon")) - .and(warp::path("deposit_snapshot")) - .and(warp::path::end()) - .and(warp::header::optional::("accept")) - .and(task_spawner_filter.clone()) - .and(eth1_service_filter.clone()) - .then( - |accept_header: Option, - task_spawner: TaskSpawner, - eth1_service: eth1::Service| { - task_spawner.blocking_response_task(Priority::P1, move || match accept_header { - Some(api_types::Accept::Ssz) => eth1_service - .get_deposit_snapshot() - .map(|snapshot| { - Response::builder() - .status(200) - .body(snapshot.as_ssz_bytes().into()) - .map(|res: Response| add_ssz_content_type_header(res)) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }) - }) - .unwrap_or_else(|| { - Response::builder() - .status(503) - .body(Vec::new().into()) - .map(|res: Response| add_ssz_content_type_header(res)) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }) - }), - _ => { - let snapshot = eth1_service.get_deposit_snapshot(); - Ok( - warp::reply::json(&api_types::GenericResponse::from(snapshot)) - .into_response(), - ) - } - }) - }, - ); - let beacon_rewards_path = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("rewards")) .and(task_spawner_filter.clone()) @@ -2566,6 +1528,7 @@ pub fn serve( */ let builder_states_path = eth_v1 + .clone() .and(warp::path("builder")) .and(warp::path("states")) .and(chain_filter.clone()); @@ -2620,6 +1583,7 @@ pub fn serve( */ let beacon_light_client_path = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("light_client")) .and(light_client_server_filter) @@ -2723,7 +1687,7 @@ pub fn serve( let fork_name = chain .spec - .fork_name_at_slot::(*update.signature_slot()); + .fork_name_at_slot::(update.signature_slot()); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() .status(200) @@ -2772,6 +1736,7 @@ pub fn serve( */ let beacon_rewards_path = eth_v1 + .clone() .and(warp::path("beacon")) .and(warp::path("rewards")) .and(task_spawner_filter.clone()) @@ -2856,10 +1821,11 @@ pub fn serve( * config */ - let config_path = eth_v1.and(warp::path("config")); + let config_path = eth_v1.clone().and(warp::path("config")); // GET config/fork_schedule let get_config_fork_schedule = config_path + .clone() .and(warp::path("fork_schedule")) .and(warp::path::end()) .and(task_spawner_filter.clone()) @@ -2878,6 +1844,7 @@ pub fn serve( // GET config/spec let get_config_spec = config_path + .clone() .and(warp::path("spec")) .and(warp::path::end()) .and(task_spawner_filter.clone()) @@ -2886,7 +1853,7 @@ pub fn serve( move |task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { let config_and_preset = - ConfigAndPreset::from_chain_spec::(&chain.spec, None); + ConfigAndPreset::from_chain_spec::(&chain.spec); Ok(api_types::GenericResponse::from(config_and_preset)) }) }, @@ -2915,8 +1882,59 @@ pub fn serve( * debug */ + // GET debug/beacon/data_column_sidecars/{block_id} + let get_debug_data_column_sidecars = eth_v1 + .clone() + .and(warp::path("debug")) + .and(warp::path("beacon")) + .and(warp::path("data_column_sidecars")) + .and(block_id_or_err) + .and(warp::path::end()) + .and(multi_key_query::()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp::header::optional::("accept")) + .then( + |block_id: BlockId, + indices_res: Result, + task_spawner: TaskSpawner, + chain: Arc>, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let indices = indices_res?; + let (data_columns, fork_name, execution_optimistic, finalized) = + block_id.get_data_columns(indices, &chain)?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(data_columns.as_ssz_bytes().into()) + .map(|res: Response| add_ssz_content_type_header(res)) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => { + // Post as a V2 endpoint so we return the fork version. + let res = execution_optimistic_finalized_beacon_response( + ResponseIncludesVersion::Yes(fork_name), + execution_optimistic, + finalized, + &data_columns, + )?; + Ok(warp::reply::json(&res).into_response()) + } + } + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ); + // GET debug/beacon/states/{state_id} let get_debug_beacon_states = any_version + .clone() .and(warp::path("debug")) .and(warp::path("beacon")) .and(warp::path("states")) @@ -2992,6 +2010,7 @@ pub fn serve( // GET debug/beacon/heads let get_debug_beacon_heads = any_version + .clone() .and(warp::path("debug")) .and(warp::path("beacon")) .and(warp::path("heads")) @@ -3032,6 +2051,7 @@ pub fn serve( // GET debug/fork_choice let get_debug_fork_choice = eth_v1 + .clone() .and(warp::path("debug")) .and(warp::path("fork_choice")) .and(warp::path::end()) @@ -3069,12 +2089,38 @@ pub fn serve( .execution_status .block_hash() .map(|block_hash| block_hash.into_root()), + extra_data: ForkChoiceExtraData { + target_root: node.target_root, + justified_root: node.justified_checkpoint.root, + finalized_root: node.finalized_checkpoint.root, + unrealized_justified_root: node + .unrealized_justified_checkpoint + .map(|checkpoint| checkpoint.root), + unrealized_finalized_root: node + .unrealized_finalized_checkpoint + .map(|checkpoint| checkpoint.root), + unrealized_justified_epoch: node + .unrealized_justified_checkpoint + .map(|checkpoint| checkpoint.epoch), + unrealized_finalized_epoch: node + .unrealized_finalized_checkpoint + .map(|checkpoint| checkpoint.epoch), + execution_status: node.execution_status.to_string(), + best_child: node + .best_child + .and_then(|index| proto_array.nodes.get(index)) + .map(|child| child.root), + best_descendant: node + .best_descendant + .and_then(|index| proto_array.nodes.get(index)) + .map(|descendant| descendant.root), + }, } }) .collect::>(); Ok(ForkChoice { - justified_checkpoint: proto_array.justified_checkpoint, - finalized_checkpoint: proto_array.finalized_checkpoint, + justified_checkpoint: beacon_fork_choice.justified_checkpoint(), + finalized_checkpoint: beacon_fork_choice.finalized_checkpoint(), fork_choice_nodes, }) }) @@ -3087,6 +2133,7 @@ pub fn serve( // GET node/identity let get_node_identity = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("identity")) .and(warp::path::end()) @@ -3103,10 +2150,13 @@ pub fn serve( let discovery_addresses = enr.multiaddr_p2p_udp(); Ok(api_types::GenericResponse::from(api_types::IdentityData { peer_id: network_globals.local_peer_id().to_base58(), - enr, - p2p_addresses, - discovery_addresses, - metadata: from_meta_data::( + enr: enr.to_base64(), + p2p_addresses: p2p_addresses.iter().map(|a| a.to_string()).collect(), + discovery_addresses: discovery_addresses + .iter() + .map(|a| a.to_string()) + .collect(), + metadata: utils::from_meta_data::( &network_globals.local_metadata, &chain.spec, ), @@ -3117,6 +2167,7 @@ pub fn serve( // GET node/version let get_node_version = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("version")) .and(warp::path::end()) @@ -3130,6 +2181,7 @@ pub fn serve( // GET node/syncing let get_node_syncing = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("syncing")) .and(warp::path::end()) @@ -3191,6 +2243,7 @@ pub fn serve( // GET node/health let get_node_health = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("health")) .and(warp::path::end()) @@ -3239,6 +2292,7 @@ pub fn serve( // GET node/peers/{peer_id} let get_node_peers_by_id = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("peers")) .and(warp::path::param::()) @@ -3293,6 +2347,7 @@ pub fn serve( // GET node/peers let get_node_peers = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("peers")) .and(warp::path::end()) @@ -3357,6 +2412,7 @@ pub fn serve( // GET node/peer_count let get_node_peer_count = eth_v1 + .clone() .and(warp::path("node")) .and(warp::path("peer_count")) .and(warp::path::end()) @@ -3400,216 +2456,60 @@ pub fn serve( */ // GET validator/duties/proposer/{epoch} - let get_validator_duties_proposer = eth_v1 - .and(warp::path("validator")) - .and(warp::path("duties")) - .and(warp::path("proposer")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid epoch".to_string(), - )) - })) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |epoch: Epoch, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - proposer_duties::proposer_duties(epoch, &chain) - }) - }, - ); + let get_validator_duties_proposer = get_validator_duties_proposer( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // GET validator/blocks/{slot} - let get_validator_blocks = any_version - .and(warp::path("validator")) - .and(warp::path("blocks")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid slot".to_string(), - )) - })) - .and(warp::path::end()) - .and(warp::header::optional::("accept")) - .and(not_while_syncing_filter.clone()) - .and(warp::query::()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |endpoint_version: EndpointVersion, - slot: Slot, - accept_header: Option, - not_synced_filter: Result<(), Rejection>, - query: api_types::ValidatorBlocksQuery, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!(?slot, "Block production request from HTTP API"); - - not_synced_filter?; - - if endpoint_version == V3 { - produce_block_v3(accept_header, chain, slot, query).await - } else { - produce_block_v2(accept_header, chain, slot, query).await - } - }) - }, - ); + let get_validator_blocks = get_validator_blocks( + any_version.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // GET validator/blinded_blocks/{slot} - let get_validator_blinded_blocks = eth_v1 - .and(warp::path("validator")) - .and(warp::path("blinded_blocks")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid slot".to_string(), - )) - })) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(warp::query::()) - .and(warp::header::optional::("accept")) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |slot: Slot, - not_synced_filter: Result<(), Rejection>, - query: api_types::ValidatorBlocksQuery, - accept_header: Option, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - not_synced_filter?; - produce_blinded_block_v2(accept_header, chain, slot, query).await - }) - }, - ); + let get_validator_blinded_blocks = get_validator_blinded_blocks( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // GET validator/attestation_data?slot,committee_index - let get_validator_attestation_data = eth_v1 - .and(warp::path("validator")) - .and(warp::path("attestation_data")) - .and(warp::path::end()) - .and(warp::query::()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |query: api_types::ValidatorAttestationDataQuery, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - - let current_slot = chain.slot().map_err(warp_utils::reject::unhandled_error)?; - - // allow a tolerance of one slot to account for clock skew - if query.slot > current_slot + 1 { - return Err(warp_utils::reject::custom_bad_request(format!( - "request slot {} is more than one slot past the current slot {}", - query.slot, current_slot - ))); - } - - chain - .produce_unaggregated_attestation(query.slot, query.committee_index) - .map(|attestation| attestation.data().clone()) - .map(api_types::GenericResponse::from) - .map_err(warp_utils::reject::unhandled_error) - }) - }, - ); + let get_validator_attestation_data = get_validator_attestation_data( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // GET validator/aggregate_attestation?attestation_data_root,slot - let get_validator_aggregate_attestation = any_version - .and(warp::path("validator")) - .and(warp::path("aggregate_attestation")) - .and(warp::path::end()) - .and(warp::query::()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |endpoint_version: EndpointVersion, - query: api_types::ValidatorAggregateAttestationQuery, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_response_task(Priority::P0, move || { - not_synced_filter?; - crate::aggregate_attestation::get_aggregate_attestation( - query.slot, - &query.attestation_data_root, - query.committee_index, - endpoint_version, - chain, - ) - }) - }, - ); + let get_validator_aggregate_attestation = get_validator_aggregate_attestation( + any_version.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/duties/attester/{epoch} - let post_validator_duties_attester = eth_v1 - .and(warp::path("validator")) - .and(warp::path("duties")) - .and(warp::path("attester")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid epoch".to_string(), - )) - })) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(warp_utils::json::json()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |epoch: Epoch, - not_synced_filter: Result<(), Rejection>, - indices: api_types::ValidatorIndexData, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - attester_duties::attester_duties(epoch, &indices.0, &chain) - }) - }, - ); + let post_validator_duties_attester = post_validator_duties_attester( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/duties/sync/{epoch} - let post_validator_duties_sync = eth_v1 - .and(warp::path("validator")) - .and(warp::path("duties")) - .and(warp::path("sync")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid epoch".to_string(), - )) - })) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(warp_utils::json::json()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |epoch: Epoch, - not_synced_filter: Result<(), Rejection>, - indices: api_types::ValidatorIndexData, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - sync_committees::sync_committee_duties(epoch, &indices.0, &chain) - }) - }, - ); + let post_validator_duties_sync = post_validator_duties_sync( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/duties/inclusion_list/{epoch} let post_validator_duties_inclusion_list = eth_v1 @@ -3640,38 +2540,12 @@ pub fn serve( ); // GET validator/sync_committee_contribution - let get_validator_sync_committee_contribution = eth_v1 - .and(warp::path("validator")) - .and(warp::path("sync_committee_contribution")) - .and(warp::path::end()) - .and(warp::query::()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |sync_committee_data: SyncContributionData, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - chain - .get_aggregated_sync_committee_contribution(&sync_committee_data) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "unable to fetch sync contribution: {:?}", - e - )) - })? - .map(api_types::GenericResponse::from) - .ok_or_else(|| { - warp_utils::reject::custom_not_found( - "no matching sync contribution found".to_string(), - ) - }) - }) - }, - ); + let get_validator_sync_committee_contribution = get_validator_sync_committee_contribution( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // GET validator/inclusion_list?slot let get_validator_inclusion_list = eth_v1 @@ -3722,513 +2596,60 @@ pub fn serve( }, ); // POST validator/aggregate_and_proofs - let post_validator_aggregate_and_proofs = any_version - .and(warp::path("validator")) - .and(warp::path("aggregate_and_proofs")) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - // V1 and V2 are identical except V2 has a consensus version header in the request. - // We only require this header for SSZ deserialization, which isn't supported for - // this endpoint presently. - |_endpoint_version: EndpointVersion, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>, - aggregates: Vec>, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - let seen_timestamp = timestamp_now(); - let mut verified_aggregates = Vec::with_capacity(aggregates.len()); - let mut messages = Vec::with_capacity(aggregates.len()); - let mut failures = Vec::new(); + let post_validator_aggregate_and_proofs = post_validator_aggregate_and_proofs( + any_version.clone().clone(), + chain_filter.clone(), + network_tx_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); - // Verify that all messages in the post are valid before processing further - for (index, aggregate) in aggregates.iter().enumerate() { - match chain.verify_aggregated_attestation_for_gossip(aggregate) { - Ok(verified_aggregate) => { - messages.push(PubsubMessage::AggregateAndProofAttestation(Box::new( - verified_aggregate.aggregate().clone(), - ))); - - // Notify the validator monitor. - chain - .validator_monitor - .read() - .register_api_aggregated_attestation( - seen_timestamp, - verified_aggregate.aggregate(), - verified_aggregate.indexed_attestation(), - &chain.slot_clock, - ); - - verified_aggregates.push((index, verified_aggregate)); - } - // If we already know the attestation, don't broadcast it or attempt to - // further verify it. Return success. - // - // It's reasonably likely that two different validators produce - // identical aggregates, especially if they're using the same beacon - // node. - Err(AttnError::AttestationSupersetKnown(_)) => continue, - // If we've already seen this aggregator produce an aggregate, just - // skip this one. - // - // We're likely to see this with VCs that use fallback BNs. The first - // BN might time-out *after* publishing the aggregate and then the - // second BN will indicate it's already seen the aggregate. - // - // There's no actual error for the user or the network since the - // aggregate has been successfully published by some other node. - Err(AttnError::AggregatorAlreadyKnown(_)) => continue, - Err(e) => { - error!( - error = ?e, - request_index = index, - aggregator_index = aggregate.message().aggregator_index(), - attestation_index = aggregate.message().aggregate().committee_index(), - attestation_slot = %aggregate.message().aggregate().data().slot, - "Failure verifying aggregate and proofs" - ); - failures.push(api_types::Failure::new(index, format!("Verification: {:?}", e))); - } - } - } - - // Publish aggregate attestations to the libp2p network - if !messages.is_empty() { - publish_network_message(&network_tx, NetworkMessage::Publish { messages })?; - } - - // Import aggregate attestations - for (index, verified_aggregate) in verified_aggregates { - if let Err(e) = chain.apply_attestation_to_fork_choice(&verified_aggregate) { - error!( - error = ?e, - request_index = index, - aggregator_index = verified_aggregate.aggregate().message().aggregator_index(), - attestation_index = verified_aggregate.attestation().committee_index(), - attestation_slot = %verified_aggregate.attestation().data().slot, - "Failure applying verified aggregate attestation to fork choice" - ); - failures.push(api_types::Failure::new(index, format!("Fork choice: {:?}", e))); - } - if let Err(e) = chain.add_to_block_inclusion_pool(verified_aggregate) { - warn!( - error = ?e, - request_index = index, - "Could not add verified aggregate attestation to the inclusion pool" - ); - failures.push(api_types::Failure::new(index, format!("Op pool: {:?}", e))); - } - } - - if !failures.is_empty() { - Err(warp_utils::reject::indexed_bad_request("error processing aggregate and proofs".to_string(), - failures, - )) - } else { - Ok(()) - } - }) - }, - ); - - let post_validator_contribution_and_proofs = eth_v1 - .and(warp::path("validator")) - .and(warp::path("contribution_and_proofs")) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .and(warp_utils::json::json()) - .and(network_tx_filter.clone()) - .then( - |not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>, - contributions: Vec>, - network_tx: UnboundedSender>| { - task_spawner.blocking_json_task(Priority::P0, move || { - not_synced_filter?; - sync_committees::process_signed_contribution_and_proofs( - contributions, - network_tx, - &chain, - )?; - Ok(api_types::GenericResponse::from(())) - }) - }, - ); + let post_validator_contribution_and_proofs = post_validator_contribution_and_proofs( + eth_v1.clone().clone(), + chain_filter.clone(), + network_tx_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/beacon_committee_subscriptions - let post_validator_beacon_committee_subscriptions = eth_v1 - .and(warp::path("validator")) - .and(warp::path("beacon_committee_subscriptions")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(validator_subscription_tx_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |subscriptions: Vec, - validator_subscription_tx: Sender, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - let subscriptions: std::collections::BTreeSet<_> = subscriptions - .iter() - .map(|subscription| { - chain - .validator_monitor - .write() - .auto_register_local_validator(subscription.validator_index); - api_types::ValidatorSubscription { - attestation_committee_index: subscription.committee_index, - slot: subscription.slot, - committee_count_at_slot: subscription.committees_at_slot, - is_aggregator: subscription.is_aggregator, - } - }) - .collect(); - let message = - ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions }; - if let Err(e) = validator_subscription_tx.try_send(message) { - warn!( - info = "the host may be overloaded or resource-constrained", - error = ?e, - "Unable to process committee subscriptions" - ); - return Err(warp_utils::reject::custom_server_error( - "unable to queue subscription, host may be overloaded or shutting down" - .to_string(), - )); - } - - Ok(()) - }) - }, + let post_validator_beacon_committee_subscriptions = + post_validator_beacon_committee_subscriptions( + eth_v1.clone().clone(), + chain_filter.clone(), + validator_subscription_tx_filter.clone(), + task_spawner_filter.clone(), ); // POST validator/prepare_beacon_proposer - let post_validator_prepare_beacon_proposer = eth_v1 - .and(warp::path("validator")) - .and(warp::path("prepare_beacon_proposer")) - .and(warp::path::end()) - .and(not_while_syncing_filter.clone()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .and(warp_utils::json::json()) - .then( - |not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>, - preparation_data: Vec| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - not_synced_filter?; - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::unhandled_error)?; - - let current_slot = chain.slot().map_err(warp_utils::reject::unhandled_error)?; - let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - - debug!( - count = preparation_data.len(), - "Received proposer preparation data" - ); - - execution_layer - .update_proposer_preparation( - current_epoch, - preparation_data.iter().map(|data| (data, &None)), - ) - .await; - - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; - - Ok::<_, warp::reject::Rejection>(warp::reply::json(&()).into_response()) - }) - }, - ); + let post_validator_prepare_beacon_proposer = post_validator_prepare_beacon_proposer( + eth_v1.clone().clone(), + chain_filter.clone(), + network_tx_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/register_validator - let post_validator_register_validator = eth_v1 - .and(warp::path("validator")) - .and(warp::path("register_validator")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .and(warp_utils::json::json()) - .then( - |task_spawner: TaskSpawner, - chain: Arc>, - register_val_data: Vec| async { - let (tx, rx) = oneshot::channel(); - - let initial_result = task_spawner - .spawn_async_with_rejection_no_conversion(Priority::P0, async move { - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::unhandled_error)?; - let current_slot = chain - .slot_clock - .now_or_genesis() - .ok_or(BeaconChainError::UnableToReadSlot) - .map_err(warp_utils::reject::unhandled_error)?; - let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - - debug!( - count = register_val_data.len(), - "Received register validator request" - ); - - let head_snapshot = chain.head_snapshot(); - let spec = &chain.spec; - - let (preparation_data, filtered_registration_data): ( - Vec<(ProposerPreparationData, Option)>, - Vec, - ) = register_val_data - .into_iter() - .filter_map(|register_data| { - chain - .validator_index(®ister_data.message.pubkey) - .ok() - .flatten() - .and_then(|validator_index| { - let validator = head_snapshot - .beacon_state - .get_validator(validator_index) - .ok()?; - let validator_status = ValidatorStatus::from_validator( - validator, - current_epoch, - spec.far_future_epoch, - ) - .superstatus(); - let is_active_or_pending = - matches!(validator_status, ValidatorStatus::Pending) - || matches!( - validator_status, - ValidatorStatus::Active - ); - - // Filter out validators who are not 'active' or 'pending'. - is_active_or_pending.then_some({ - ( - ( - ProposerPreparationData { - validator_index: validator_index as u64, - fee_recipient: register_data - .message - .fee_recipient, - }, - Some(register_data.message.gas_limit), - ), - register_data, - ) - }) - }) - }) - .unzip(); - - // Update the prepare beacon proposer cache based on this request. - execution_layer - .update_proposer_preparation( - current_epoch, - preparation_data.iter().map(|(data, limit)| (data, limit)), - ) - .await; - - // Call prepare beacon proposer blocking with the latest update in order to make - // sure we have a local payload to fall back to in the event of the blinded block - // flow failing. - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; - - info!( - count = filtered_registration_data.len(), - "Forwarding register validator request to connected builder" - ); - - // It's a waste of a `BeaconProcessor` worker to just - // wait on a response from the builder (especially since - // they have frequent timeouts). Spawn a new task and - // send the response back to our original HTTP request - // task via a channel. - let builder_future = async move { - let arc_builder = chain - .execution_layer - .as_ref() - .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::unhandled_error)? - .builder(); - let builder = arc_builder - .as_ref() - .ok_or(BeaconChainError::BuilderMissing) - .map_err(warp_utils::reject::unhandled_error)?; - builder - .post_builder_validators(&filtered_registration_data) - .await - .map(|resp| warp::reply::json(&resp).into_response()) - .map_err(|e| { - warn!( - num_registrations = filtered_registration_data.len(), - error = ?e, - "Relay error when registering validator(s)" - ); - // Forward the HTTP status code if we are able to, otherwise fall back - // to a server error. - if let eth2::Error::ServerMessage(message) = e { - if message.code == StatusCode::BAD_REQUEST.as_u16() { - return warp_utils::reject::custom_bad_request( - message.message, - ); - } else { - // According to the spec this response should only be a 400 or 500, - // so we fall back to a 500 here. - return warp_utils::reject::custom_server_error( - message.message, - ); - } - } - warp_utils::reject::custom_server_error(format!("{e:?}")) - }) - }; - tokio::task::spawn(async move { tx.send(builder_future.await) }); - - // Just send a generic 200 OK from this closure. We'll - // ignore the `Ok` variant and form a proper response - // from what is sent back down the channel. - Ok(warp::reply::reply().into_response()) - }) - .await; - - if initial_result.is_err() { - return convert_rejection(initial_result).await; - } - - // Await a response from the builder without blocking a - // `BeaconProcessor` worker. - convert_rejection(rx.await.unwrap_or_else(|_| { - Ok(warp::reply::with_status( - warp::reply::json(&"No response from channel"), - eth2::StatusCode::INTERNAL_SERVER_ERROR, - ) - .into_response()) - })) - .await - }, - ); + let post_validator_register_validator = post_validator_register_validator( + eth_v1.clone().clone(), + chain_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/sync_committee_subscriptions - let post_validator_sync_committee_subscriptions = eth_v1 - .and(warp::path("validator")) - .and(warp::path("sync_committee_subscriptions")) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(validator_subscription_tx_filter) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |subscriptions: Vec, - validator_subscription_tx: Sender, - task_spawner: TaskSpawner, - chain: Arc>, - | { - task_spawner.blocking_json_task(Priority::P0, move || { - for subscription in subscriptions { - chain - .validator_monitor - .write() - .auto_register_local_validator(subscription.validator_index); - - let message = ValidatorSubscriptionMessage::SyncCommitteeSubscribe { - subscriptions: vec![subscription], - }; - if let Err(e) = validator_subscription_tx.try_send(message) { - warn!( - info = "the host may be overloaded or resource-constrained", - error = ?e, - "Unable to process sync subscriptions" - ); - return Err(warp_utils::reject::custom_server_error( - "unable to queue subscription, host may be overloaded or shutting down".to_string(), - )); - } - } - - Ok(()) - }) - }, - ); + let post_validator_sync_committee_subscriptions = post_validator_sync_committee_subscriptions( + eth_v1.clone().clone(), + chain_filter.clone(), + validator_subscription_tx_filter.clone(), + task_spawner_filter.clone(), + ); // POST validator/liveness/{epoch} - let post_validator_liveness_epoch = eth_v1 - .and(warp::path("validator")) - .and(warp::path("liveness")) - .and(warp::path::param::()) - .and(warp::path::end()) - .and(warp_utils::json::json()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |epoch: Epoch, - indices: api_types::ValidatorIndexData, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { - // Ensure the request is for either the current, previous or next epoch. - let current_epoch = - chain.epoch().map_err(warp_utils::reject::unhandled_error)?; - let prev_epoch = current_epoch.saturating_sub(Epoch::new(1)); - let next_epoch = current_epoch.saturating_add(Epoch::new(1)); - - if epoch < prev_epoch || epoch > next_epoch { - return Err(warp_utils::reject::custom_bad_request(format!( - "request epoch {} is more than one epoch from the current epoch {}", - epoch, current_epoch - ))); - } - - let liveness: Vec = indices - .0 - .iter() - .cloned() - .map(|index| { - let is_live = chain.validator_seen_at_epoch(index as usize, epoch); - api_types::StandardLivenessResponseData { index, is_live } - }) - .collect(); - - Ok(api_types::GenericResponse::from(liveness)) - }) - }, - ); + let post_validator_liveness_epoch = post_validator_liveness_epoch( + eth_v1.clone().clone(), + chain_filter.clone(), + task_spawner_filter.clone(), + ); // POST lighthouse/finalize let post_lighthouse_finalize = warp::path("lighthouse") @@ -4300,7 +2721,10 @@ pub fn serve( ); network_globals.add_trusted_peer(enr.clone()); - publish_network_message(&network_tx, NetworkMessage::ConnectTrustedPeer(enr))?; + utils::publish_network_message( + &network_tx, + NetworkMessage::ConnectTrustedPeer(enr), + )?; Ok(()) }) @@ -4331,7 +2755,7 @@ pub fn serve( ); network_globals.remove_trusted_peer(enr.clone()); - publish_network_message( + utils::publish_network_message( &network_tx, NetworkMessage::DisconnectTrustedPeer(enr), )?; @@ -4613,105 +3037,17 @@ pub fn serve( }, ); - // GET lighthouse/eth1/syncing - let get_lighthouse_eth1_syncing = warp::path("lighthouse") - .and(warp::path("eth1")) - .and(warp::path("syncing")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - let current_slot_opt = chain.slot().ok(); - - chain - .eth1_chain - .as_ref() - .ok_or_else(|| { - warp_utils::reject::custom_not_found( - "Eth1 sync is disabled. See the --eth1 CLI flag.".to_string(), - ) - }) - .and_then(|eth1| { - eth1.sync_status(chain.genesis_time, current_slot_opt, &chain.spec) - .ok_or_else(|| { - warp_utils::reject::custom_server_error( - "Unable to determine Eth1 sync status".to_string(), - ) - }) - }) - .map(api_types::GenericResponse::from) - }) - }, - ); - - // GET lighthouse/eth1/block_cache - let get_lighthouse_eth1_block_cache = warp::path("lighthouse") - .and(warp::path("eth1")) - .and(warp::path("block_cache")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(eth1_service_filter.clone()) - .then( - |task_spawner: TaskSpawner, eth1_service: eth1::Service| { - task_spawner.blocking_json_task(Priority::P1, move || { - Ok(api_types::GenericResponse::from( - eth1_service - .blocks() - .read() - .iter() - .cloned() - .collect::>(), - )) - }) - }, - ); - - // GET lighthouse/eth1/deposit_cache - let get_lighthouse_eth1_deposit_cache = warp::path("lighthouse") - .and(warp::path("eth1")) - .and(warp::path("deposit_cache")) - .and(warp::path::end()) - .and(task_spawner_filter.clone()) - .and(eth1_service_filter) - .then( - |task_spawner: TaskSpawner, eth1_service: eth1::Service| { - task_spawner.blocking_json_task(Priority::P1, move || { - Ok(api_types::GenericResponse::from( - eth1_service - .deposits() - .read() - .cache - .iter() - .cloned() - .collect::>(), - )) - }) - }, - ); - // GET lighthouse/staking let get_lighthouse_staking = warp::path("lighthouse") .and(warp::path("staking")) .and(warp::path::end()) .and(task_spawner_filter.clone()) - .and(chain_filter.clone()) - .then( - |task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P1, move || { - if chain.eth1_chain.is_some() { - Ok(()) - } else { - Err(warp_utils::reject::custom_not_found( - "staking is not enabled, \ - see the --staking CLI flag" - .to_string(), - )) - } - }) - }, - ); + .then(|task_spawner: TaskSpawner| { + // This API is fairly useless since we abolished the distinction between staking and + // non-staking nodes. We keep it for backwards-compatibility with LH v7.0.0, and in case + // we want to reintroduce the distinction in future. + task_spawner.blocking_json_task(Priority::P1, move || Ok(())) + }); let database_path = warp::path("lighthouse").and(warp::path("database")); @@ -4746,6 +3082,50 @@ pub fn serve( }, ); + // GET lighthouse/custody/info + let get_lighthouse_custody_info = warp::path("lighthouse") + .and(warp::path("custody")) + .and(warp::path("info")) + .and(warp::path::end()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || custody::info(chain)) + }, + ); + + // POST lighthouse/custody/backfill + let post_lighthouse_custody_backfill = warp::path("lighthouse") + .and(warp::path("custody")) + .and(warp::path("backfill")) + .and(warp::path::end()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + // Calling this endpoint will trigger custody backfill once `effective_epoch`` + // is finalized. + let effective_epoch = chain + .canonical_head + .cached_head() + .head_slot() + .epoch(T::EthSpec::slots_per_epoch()) + + 1; + let custody_context = chain.data_availability_checker.custody_context(); + // Reset validator custody requirements to `effective_epoch` with the latest + // cgc requiremnets. + custody_context.reset_validator_custody_requirements(effective_epoch); + // Update `DataColumnCustodyInfo` to reflect the custody change. + chain.update_data_column_custody_info(Some( + effective_epoch.start_slot(T::EthSpec::slots_per_epoch()), + )); + Ok(()) + }) + }, + ); + // GET lighthouse/analysis/block_rewards let get_lighthouse_block_rewards = warp::path("lighthouse") .and(warp::path("analysis")) @@ -4827,6 +3207,7 @@ pub fn serve( ); let get_events = eth_v1 + .clone() .and(warp::path("events")) .and(warp::path::end()) .and(multi_key_query::()) @@ -4849,6 +3230,9 @@ pub fn serve( api_types::EventTopic::BlobSidecar => { event_handler.subscribe_blob_sidecar() } + api_types::EventTopic::DataColumnSidecar => { + event_handler.subscribe_data_column_sidecar() + } api_types::EventTopic::Attestation => { event_handler.subscribe_attestation() } @@ -5007,18 +3391,19 @@ pub fn serve( .uor(get_beacon_block_attestations) .uor(get_beacon_blinded_block) .uor(get_beacon_block_root) + .uor(get_blob_sidecars) .uor(get_blobs) .uor(get_beacon_pool_attestations) .uor(get_beacon_pool_attester_slashings) .uor(get_beacon_pool_proposer_slashings) .uor(get_beacon_pool_voluntary_exits) .uor(get_beacon_pool_bls_to_execution_changes) - .uor(get_beacon_deposit_snapshot) .uor(get_beacon_rewards_blocks) .uor(get_config_fork_schedule) .uor(get_config_spec) .uor(get_config_deposit_contract) .uor(get_debug_beacon_states) + .uor(get_debug_data_column_sidecars) .uor(get_debug_beacon_heads) .uor(get_debug_fork_choice) .uor(get_node_identity) @@ -5045,11 +3430,9 @@ pub fn serve( .uor(get_lighthouse_proto_array) .uor(get_lighthouse_validator_inclusion_global) .uor(get_lighthouse_validator_inclusion) - .uor(get_lighthouse_eth1_syncing) - .uor(get_lighthouse_eth1_block_cache) - .uor(get_lighthouse_eth1_deposit_cache) .uor(get_lighthouse_staking) .uor(get_lighthouse_database_info) + .uor(get_lighthouse_custody_info) .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) .uor(get_beacon_light_client_optimistic_update) @@ -5078,7 +3461,6 @@ pub fn serve( .uor(post_beacon_blinded_blocks) .uor(post_beacon_blocks_v2) .uor(post_beacon_blinded_blocks_v2) - .uor(post_beacon_pool_attestations_v1) .uor(post_beacon_pool_attestations_v2) .uor(post_beacon_pool_attester_slashings) .uor(post_beacon_pool_proposer_slashings) @@ -5087,6 +3469,7 @@ pub fn serve( .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) + .uor(post_beacon_state_validator_identities) .uor(post_beacon_rewards_attestations) .uor(post_beacon_rewards_sync_committee) .uor(post_validator_duties_attester) @@ -5109,6 +3492,7 @@ pub fn serve( .uor(post_lighthouse_compaction) .uor(post_lighthouse_add_peer) .uor(post_lighthouse_remove_peer) + .uor(post_lighthouse_custody_backfill) .recover(warp_utils::reject::handle_rejection), ), ) @@ -5151,70 +3535,3 @@ pub fn serve( Ok(http_server) } - -fn from_meta_data( - meta_data: &RwLock>, - spec: &ChainSpec, -) -> api_types::MetaData { - let meta_data = meta_data.read(); - let format_hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); - - let seq_number = *meta_data.seq_number(); - let attnets = format_hex(&meta_data.attnets().clone().into_bytes()); - let syncnets = format_hex( - &meta_data - .syncnets() - .cloned() - .unwrap_or_default() - .into_bytes(), - ); - - if spec.is_peer_das_scheduled() { - api_types::MetaData::V3(api_types::MetaDataV3 { - seq_number, - attnets, - syncnets, - custody_group_count: meta_data.custody_group_count().cloned().unwrap_or_default(), - }) - } else { - api_types::MetaData::V2(api_types::MetaDataV2 { - seq_number, - attnets, - syncnets, - }) - } -} - -/// Publish a message to the libp2p pubsub network. -fn publish_pubsub_message( - network_tx: &UnboundedSender>, - message: PubsubMessage, -) -> Result<(), warp::Rejection> { - publish_network_message( - network_tx, - NetworkMessage::Publish { - messages: vec![message], - }, - ) -} - -/// Publish a message to the libp2p pubsub network. -fn publish_pubsub_messages( - network_tx: &UnboundedSender>, - messages: Vec>, -) -> Result<(), warp::Rejection> { - publish_network_message(network_tx, NetworkMessage::Publish { messages }) -} - -/// Publish a message to the libp2p network. -fn publish_network_message( - network_tx: &UnboundedSender>, - message: NetworkMessage, -) -> Result<(), warp::Rejection> { - network_tx.send(message).map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "unable to publish to network channel: {}", - e - )) - }) -} diff --git a/beacon_node/http_api/src/light_client.rs b/beacon_node/http_api/src/light_client.rs index 24b1338a72..86eef03218 100644 --- a/beacon_node/http_api/src/light_client.rs +++ b/beacon_node/http_api/src/light_client.rs @@ -1,19 +1,20 @@ use crate::version::{ - add_consensus_version_header, add_ssz_content_type_header, beacon_response, - ResponseIncludesVersion, + ResponseIncludesVersion, add_consensus_version_header, add_ssz_content_type_header, + beacon_response, }; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use eth2::beacon_response::BeaconResponse; use eth2::types::{ - self as api_types, ChainSpec, LightClientUpdate, LightClientUpdateResponseChunk, + self as api_types, LightClientUpdate, LightClientUpdateResponseChunk, LightClientUpdateResponseChunkInner, LightClientUpdatesQuery, }; use ssz::Encode; use std::sync::Arc; -use types::{BeaconResponse, ForkName, Hash256, LightClientBootstrap}; +use types::{EthSpec, ForkName, Hash256, LightClientBootstrap}; use warp::{ + Rejection, hyper::{Body, Response}, reply::Reply, - Rejection, }; const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u64 = 128; @@ -34,13 +35,15 @@ pub fn get_light_client_updates( match accept_header { Some(api_types::Accept::Ssz) => { let response_chunks = light_client_updates - .iter() - .map(|update| map_light_client_update_to_ssz_chunk::(&chain, update)) - .collect::>(); + .into_iter() + .flat_map(|update| { + map_light_client_update_to_response_chunk::(&chain, update).as_ssz_bytes() + }) + .collect(); Response::builder() .status(200) - .body(response_chunks.as_ssz_bytes()) + .body(response_chunks) .map(|res: Response>| add_ssz_content_type_header(res)) .map_err(|e| { warp_utils::reject::custom_server_error(format!( @@ -146,25 +149,20 @@ pub fn validate_light_client_updates_request( Ok(()) } -fn map_light_client_update_to_ssz_chunk( +fn map_light_client_update_to_response_chunk( chain: &BeaconChain, - light_client_update: &LightClientUpdate, -) -> LightClientUpdateResponseChunk { - let fork_name = chain - .spec - .fork_name_at_slot::(light_client_update.attested_header_slot()); + light_client_update: LightClientUpdate, +) -> LightClientUpdateResponseChunk { + let epoch = light_client_update + .attested_header_slot() + .epoch(T::EthSpec::slots_per_epoch()); + let fork_digest = chain.compute_fork_digest(epoch); - let fork_digest = ChainSpec::compute_fork_digest( - chain.spec.fork_version_for_name(fork_name), - chain.genesis_validators_root, - ); - - let payload = light_client_update.as_ssz_bytes(); - let response_chunk_len = fork_digest.len() + payload.len(); + let response_chunk_len = fork_digest.len() + light_client_update.ssz_bytes_len(); let response_chunk = LightClientUpdateResponseChunkInner { context: fork_digest, - payload, + payload: light_client_update, }; LightClientUpdateResponseChunk { diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index db82ff214c..3bd0cec7e3 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -1,22 +1,25 @@ use crate::{ build_block_contents, version::{ - add_consensus_block_value_header, add_consensus_version_header, + ResponseIncludesVersion, add_consensus_block_value_header, add_consensus_version_header, add_execution_payload_blinded_header, add_execution_payload_value_header, add_ssz_content_type_header, beacon_response, inconsistent_fork_rejection, - ResponseIncludesVersion, }, }; +use beacon_chain::graffiti_calculator::GraffitiSettings; use beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, ProduceBlockVerification, }; +use eth2::beacon_response::ForkVersionedResponse; use eth2::types::{self as api_types, ProduceBlockV3Metadata, SkipRandaoVerification}; +use lighthouse_tracing::{SPAN_PRODUCE_BLOCK_V2, SPAN_PRODUCE_BLOCK_V3}; use ssz::Encode; use std::sync::Arc; +use tracing::instrument; use types::{payload::BlockProductionVersion, *}; use warp::{ - hyper::{Body, Response}, Reply, + hyper::{Body, Response}, }; /// If default boost factor is provided in validator/blocks v3 request, we will skip the calculation @@ -41,6 +44,11 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = SPAN_PRODUCE_BLOCK_V3, + skip_all, + fields(%slot) +)] pub async fn produce_block_v3( accept_header: Option, chain: Arc>, @@ -61,11 +69,13 @@ pub async fn produce_block_v3( query.builder_boost_factor }; + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + let block_response_type = chain .produce_block_with_verification( randao_reveal, slot, - query.graffiti, + graffiti_settings, randao_verification, builder_boost_factor, BlockProductionVersion::V3, @@ -141,11 +151,13 @@ pub async fn produce_blinded_block_v2( })?; let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + let block_response_type = chain .produce_block_with_verification( randao_reveal, slot, - query.graffiti, + graffiti_settings, randao_verification, None, BlockProductionVersion::BlindedV2, @@ -156,6 +168,11 @@ pub async fn produce_blinded_block_v2( build_response_v2(chain, block_response_type, accept_header) } +#[instrument( + name = SPAN_PRODUCE_BLOCK_V2, + skip_all, + fields(%slot) +)] pub async fn produce_block_v2( accept_header: Option, chain: Arc>, @@ -170,12 +187,13 @@ pub async fn produce_block_v2( })?; let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); let block_response_type = chain .produce_block_with_verification( randao_reveal, slot, - query.graffiti, + graffiti_settings, randao_verification, None, BlockProductionVersion::FullV2, diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 971571f487..1ebb174785 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -2,13 +2,14 @@ use crate::state_id::StateId; use beacon_chain::{ - beacon_proposer_cache::{compute_proposer_duties_from_head, ensure_state_is_in_epoch}, BeaconChain, BeaconChainError, BeaconChainTypes, + beacon_proposer_cache::{ + compute_proposer_duties_from_head, ensure_state_can_determine_proposers_for_epoch, + }, }; use eth2::types::{self as api_types}; use safe_arith::SafeArith; use slot_clock::SlotClock; -use std::cmp::Ordering; use tracing::debug; use types::{Epoch, EthSpec, Hash256, Slot}; @@ -59,13 +60,13 @@ pub fn proposer_duties( .safe_add(1) .map_err(warp_utils::reject::arith_error)? { - let (proposers, dependent_root, execution_status, _fork) = + let (proposers, _dependent_root, legacy_dependent_root, execution_status, _fork) = compute_proposer_duties_from_head(request_epoch, chain) .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response( chain, request_epoch, - dependent_root, + legacy_dependent_root, execution_status.is_optimistic_or_invalid(), proposers, ) @@ -102,39 +103,38 @@ fn try_proposer_duties_from_cache( let head_block = &head.snapshot.beacon_block; let head_block_root = head.head_block_root(); let head_epoch = head_block.slot().epoch(T::EthSpec::slots_per_epoch()); + + // This code path can't handle requests for past epochs. + if head_epoch > request_epoch { + return Err(warp_utils::reject::custom_server_error(format!( + "head epoch {head_epoch} is later than request epoch {request_epoch}", + ))); + } + let head_decision_root = head .snapshot .beacon_state - .proposer_shuffling_decision_root(head_block_root) + .proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec) + .map_err(warp_utils::reject::beacon_state_error)?; + let legacy_dependent_root = head + .snapshot + .beacon_state + .legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root) .map_err(warp_utils::reject::beacon_state_error)?; let execution_optimistic = chain .is_optimistic_or_invalid_head_block(head_block) .map_err(warp_utils::reject::unhandled_error)?; - let dependent_root = match head_epoch.cmp(&request_epoch) { - // head_epoch == request_epoch - Ordering::Equal => head_decision_root, - // head_epoch < request_epoch - Ordering::Less => head_block_root, - // head_epoch > request_epoch - Ordering::Greater => { - return Err(warp_utils::reject::custom_server_error(format!( - "head epoch {} is later than request epoch {}", - head_epoch, request_epoch - ))) - } - }; - chain .beacon_proposer_cache .lock() - .get_epoch::(dependent_root, request_epoch) + .get_epoch::(head_decision_root, request_epoch) .cloned() .map(|indices| { convert_to_api_response( chain, request_epoch, - dependent_root, + legacy_dependent_root, execution_optimistic, indices.to_vec(), ) @@ -156,7 +156,7 @@ fn compute_and_cache_proposer_duties( current_epoch: Epoch, chain: &BeaconChain, ) -> Result { - let (indices, dependent_root, execution_status, fork) = + let (indices, dependent_root, legacy_dependent_root, execution_status, fork) = compute_proposer_duties_from_head(current_epoch, chain) .map_err(warp_utils::reject::unhandled_error)?; @@ -171,7 +171,7 @@ fn compute_and_cache_proposer_duties( convert_to_api_response( chain, current_epoch, - dependent_root, + legacy_dependent_root, execution_status.is_optimistic_or_invalid(), indices, ) @@ -204,18 +204,19 @@ fn compute_historic_proposer_duties( } }; - let (state, execution_optimistic) = - if let Some((state_root, mut state, execution_optimistic)) = state_opt { - // If we've loaded the head state it might be from a previous epoch, ensure it's in a - // suitable epoch. - ensure_state_is_in_epoch(&mut state, state_root, epoch, &chain.spec) - .map_err(warp_utils::reject::unhandled_error)?; - (state, execution_optimistic) - } else { - let (state, execution_optimistic, _finalized) = - StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?; - (state, execution_optimistic) - }; + let (state, execution_optimistic) = if let Some((state_root, mut state, execution_optimistic)) = + state_opt + { + // If we've loaded the head state it might be from a previous epoch, ensure it's in a + // suitable epoch. + ensure_state_can_determine_proposers_for_epoch(&mut state, state_root, epoch, &chain.spec) + .map_err(warp_utils::reject::unhandled_error)?; + (state, execution_optimistic) + } else { + let (state, execution_optimistic, _finalized) = + StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?; + (state, execution_optimistic) + }; // Ensure the state lookup was correct. if state.current_epoch() != epoch { @@ -227,18 +228,24 @@ fn compute_historic_proposer_duties( } let indices = state - .get_beacon_proposer_indices(&chain.spec) + .get_beacon_proposer_indices(epoch, &chain.spec) .map_err(BeaconChainError::from) .map_err(warp_utils::reject::unhandled_error)?; // We can supply the genesis block root as the block root since we know that the only block that // decides its own root is the genesis block. - let dependent_root = state - .proposer_shuffling_decision_root(chain.genesis_block_root) + let legacy_dependent_root = state + .legacy_proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root) .map_err(BeaconChainError::from) .map_err(warp_utils::reject::unhandled_error)?; - convert_to_api_response(chain, epoch, dependent_root, execution_optimistic, indices) + convert_to_api_response( + chain, + epoch, + legacy_dependent_root, + execution_optimistic, + indices, + ) } /// Converts the internal representation of proposer duties into one that is compatible with the diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index db85b8f205..947edf56d9 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -36,24 +36,19 @@ //! attestations and there's no immediate cause for concern. use crate::task_spawner::{Priority, TaskSpawner}; use beacon_chain::{ - single_attestation::single_attestation_to_attestation, validator_monitor::timestamp_now, AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes, + validator_monitor::timestamp_now, }; use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage}; -use either::Either; +use beacon_processor::{Work, WorkEvent}; use eth2::types::Failure; use lighthouse_network::PubsubMessage; use network::NetworkMessage; -use serde_json::Value; -use std::borrow::Cow; use std::sync::Arc; use std::time::Duration; -use tokio::sync::{ - mpsc::{Sender, UnboundedSender}, - oneshot, -}; +use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tracing::{debug, error, warn}; -use types::{Attestation, EthSpec, ForkName, SingleAttestation}; +use types::SingleAttestation; // Error variants are only used in `Debug` and considered `dead_code` by the compiler. #[derive(Debug)] @@ -65,8 +60,6 @@ pub enum Error { ReprocessDisabled, ReprocessFull, ReprocessTimeout, - InvalidJson(#[allow(dead_code)] serde_json::Error), - FailedConversion(#[allow(dead_code)] Box), } enum PublishAttestationResult { @@ -76,66 +69,24 @@ enum PublishAttestationResult { Failure(Error), } -#[allow(clippy::type_complexity)] -pub fn deserialize_attestation_payload( - payload: Value, - fork_name: Option, -) -> Result, SingleAttestation>>, Error> { - if fork_name.is_some_and(|fork_name| fork_name.electra_enabled()) || fork_name.is_none() { - if fork_name.is_none() { - warn!("No Consensus Version header specified."); - } - - Ok(serde_json::from_value::>(payload) - .map_err(Error::InvalidJson)? - .into_iter() - .map(Either::Right) - .collect()) - } else { - Ok( - serde_json::from_value::>>(payload) - .map_err(Error::InvalidJson)? - .into_iter() - .map(Either::Left) - .collect(), - ) - } -} - fn verify_and_publish_attestation( chain: &Arc>, - either_attestation: &Either, SingleAttestation>, + attestation: &SingleAttestation, seen_timestamp: Duration, network_tx: &UnboundedSender>, ) -> Result<(), Error> { - let attestation = convert_to_attestation(chain, either_attestation)?; let verified_attestation = chain - .verify_unaggregated_attestation_for_gossip(&attestation, None) + .verify_unaggregated_attestation_for_gossip(attestation, None) .map_err(Error::Validation)?; - match either_attestation { - Either::Left(attestation) => { - // Publish. - network_tx - .send(NetworkMessage::Publish { - messages: vec![PubsubMessage::Attestation(Box::new(( - verified_attestation.subnet_id(), - attestation.clone(), - )))], - }) - .map_err(|_| Error::Publication)?; - } - Either::Right(single_attestation) => { - network_tx - .send(NetworkMessage::Publish { - messages: vec![PubsubMessage::SingleAttestation(Box::new(( - verified_attestation.subnet_id(), - single_attestation.clone(), - )))], - }) - .map_err(|_| Error::Publication)?; - } - } + network_tx + .send(NetworkMessage::Publish { + messages: vec![PubsubMessage::Attestation(Box::new(( + verified_attestation.subnet_id(), + attestation.clone(), + )))], + }) + .map_err(|_| Error::Publication)?; // Notify the validator monitor. chain @@ -172,73 +123,24 @@ fn verify_and_publish_attestation( } } -fn convert_to_attestation<'a, T: BeaconChainTypes>( - chain: &Arc>, - attestation: &'a Either, SingleAttestation>, -) -> Result>, Error> { - match attestation { - Either::Left(a) => Ok(Cow::Borrowed(a)), - Either::Right(single_attestation) => { - let conversion_result = chain.with_committee_cache( - single_attestation.data.target.root, - single_attestation - .data - .slot - .epoch(T::EthSpec::slots_per_epoch()), - |committee_cache, _| { - let Some(committee) = committee_cache.get_beacon_committee( - single_attestation.data.slot, - single_attestation.committee_index, - ) else { - return Ok(Err(AttestationError::NoCommitteeForSlotAndIndex { - slot: single_attestation.data.slot, - index: single_attestation.committee_index, - })); - }; - - Ok(single_attestation_to_attestation::( - single_attestation, - committee.committee, - ) - .map(Cow::Owned)) - }, - ); - match conversion_result { - Ok(Ok(attestation)) => Ok(attestation), - Ok(Err(e)) => Err(Error::Validation(e)), - // Map the error returned by `with_committee_cache` for unknown blocks into the - // `UnknownHeadBlock` error that is gracefully handled. - Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => { - Err(Error::Validation(AttestationError::UnknownHeadBlock { - beacon_block_root, - })) - } - Err(e) => Err(Error::FailedConversion(Box::new(e))), - } - } - } -} - pub async fn publish_attestations( task_spawner: TaskSpawner, chain: Arc>, - attestations: Vec, SingleAttestation>>, + attestations: Vec, network_tx: UnboundedSender>, - reprocess_send: Option>, + allow_reprocess: bool, ) -> Result<(), warp::Rejection> { // Collect metadata about attestations which we'll use to report failures. We need to // move the `attestations` vec into the blocking task, so this small overhead is unavoidable. let attestation_metadata = attestations .iter() - .map(|att| match att { - Either::Left(att) => (att.data().slot, att.committee_index()), - Either::Right(att) => (att.data.slot, Some(att.committee_index)), - }) + .map(|att| (att.data.slot, Some(att.committee_index))) .collect::>(); // Gossip validate and publish attestations that can be immediately processed. let seen_timestamp = timestamp_now(); let mut prelim_results = task_spawner + .clone() .blocking_task(Priority::P0, move || { Ok(attestations .into_iter() @@ -253,7 +155,7 @@ pub async fn publish_attestations( Err(Error::Validation(AttestationError::UnknownHeadBlock { beacon_block_root, })) => { - let Some(reprocess_tx) = &reprocess_send else { + if !allow_reprocess { return PublishAttestationResult::Failure(Error::ReprocessDisabled); }; // Re-process. @@ -277,7 +179,13 @@ pub async fn publish_attestations( beacon_block_root, process_fn: Box::new(reprocess_fn), }); - if reprocess_tx.try_send(reprocess_msg).is_err() { + if task_spawner + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { PublishAttestationResult::Failure(Error::ReprocessFull) } else { PublishAttestationResult::Reprocessing(rx) diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 9b1a3f8677..b54c071eb8 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -3,36 +3,39 @@ use std::future::Future; use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; -use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; use beacon_chain::{ - build_blob_data_column_sidecars, AvailabilityProcessingStatus, BeaconChain, BeaconChainError, - BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, NotifyExecutionLayer, + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, + IntoGossipVerifiedBlock, NotifyExecutionLayer, build_blob_data_column_sidecars, }; -use eth2::types::{ - BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, - PublishBlockRequest, SignedBlockContents, +use eth2::{ + StatusCode, + types::{ + BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, + FullPayloadContents, PublishBlockRequest, SignedBlockContents, + }, }; -use execution_layer::ProvenancedPayload; +use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; -use lighthouse_network::{NetworkGlobals, PubsubMessage}; +use lighthouse_network::PubsubMessage; +use lighthouse_tracing::SPAN_PUBLISH_BLOCK; use network::NetworkMessage; use rand::prelude::SliceRandom; use slot_clock::SlotClock; use std::marker::PhantomData; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; -use tracing::{debug, error, info, warn}; +use tracing::{Span, debug, debug_span, error, field, info, instrument, warn}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, SignedBlindedBeaconBlock, }; -use warp::http::StatusCode; -use warp::{reply::Response, Rejection, Reply}; +use warp::{Rejection, Reply, reply::Response}; pub type UnverifiedBlobs = Option<( KzgProofs<::EthSpec>, @@ -75,6 +78,12 @@ impl ProvenancedBlock> /// Handles a request from the HTTP API for full blocks. #[allow(clippy::too_many_arguments)] +#[instrument( + name = SPAN_PUBLISH_BLOCK, + level = "info", + skip_all, + fields(block_root = field::Empty, ?validation_level, block_slot = field::Empty, provenance = field::Empty) +)] pub async fn publish_block>( block_root: Option, provenanced_block: ProvenancedBlock, @@ -82,7 +91,6 @@ pub async fn publish_block>( network_tx: &UnboundedSender>, validation_level: BroadcastValidation, duplicate_status_code: StatusCode, - network_globals: Arc>, ) -> Result { let seen_timestamp = timestamp_now(); let block_publishing_delay_for_testing = chain.config.block_publishing_delay; @@ -97,9 +105,16 @@ pub async fn publish_block>( } else { "builder" }; - let block = unverified_block.inner_block(); - debug!(slot = %block.slot(), "Signed block received in HTTP API"); + let block = unverified_block.inner_block(); + let block_root = block_root.unwrap_or_else(|| block.canonical_root()); + + let current_span = Span::current(); + current_span.record("provenance", provenance); + current_span.record("block_root", field::display(block_root)); + current_span.record("block_slot", field::display(block.slot())); + + debug!("Signed block received in HTTP API"); /* actually publish a block */ let publish_block_p2p = move |block: Arc>, @@ -123,9 +138,10 @@ pub async fn publish_block>( "Signed block published to network via HTTP API" ); - crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())).map_err( - |_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish)), - )?; + crate::utils::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())) + .map_err(|_| { + BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish)) + })?; Ok(()) }; @@ -134,18 +150,15 @@ pub async fn publish_block>( let slot = block.message().slot(); let sender_clone = network_tx.clone(); - let build_sidecar_task_handle = - spawn_build_data_sidecar_task(chain.clone(), block.clone(), unverified_blobs)?; + let build_sidecar_task_handle = spawn_build_data_sidecar_task( + chain.clone(), + block.clone(), + unverified_blobs, + current_span.clone(), + )?; // Gossip verify the block and blobs/data columns separately. - let gossip_verified_block_result = unverified_block - .into_gossip_verified_block(&chain, network_globals.custody_columns_count() as usize); - let block_root = block_root.unwrap_or_else(|| { - gossip_verified_block_result.as_ref().map_or_else( - |_| block.canonical_root(), - |verified_block| verified_block.block_root, - ) - }); + let gossip_verified_block_result = unverified_block.into_gossip_verified_block(&chain); let should_publish_block = gossip_verified_block_result.is_ok(); if BroadcastValidation::Gossip == validation_level && should_publish_block { @@ -204,7 +217,7 @@ pub async fn publish_block>( } } - if gossip_verified_columns.iter().map(Option::is_some).count() > 0 { + if !gossip_verified_columns.is_empty() { if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { // Subtract block publishing delay if it is also used. // Note: if `data_column_publishing_delay` is less than `block_publishing_delay`, it @@ -224,28 +237,30 @@ pub async fn publish_block>( publish_column_sidecars(network_tx, &gossip_verified_columns, &chain).map_err(|_| { warp_utils::reject::custom_server_error("unable to publish data column sidecars".into()) })?; - let sampling_columns_indices = &network_globals.sampling_columns; + let epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns_indices = chain.sampling_columns_for_epoch(epoch); let sampling_columns = gossip_verified_columns .into_iter() - .flatten() .filter(|data_column| sampling_columns_indices.contains(&data_column.index())) - .collect(); + .collect::>(); - // Importing the columns could trigger block import and network publication in the case - // where the block was already seen on gossip. - if let Err(e) = - Box::pin(chain.process_gossip_data_columns(sampling_columns, publish_fn)).await - { - let msg = format!("Invalid data column: {e}"); - return if let BroadcastValidation::Gossip = validation_level { - Err(warp_utils::reject::broadcast_without_import(msg)) - } else { - error!( - reason = &msg, - "Invalid data column during block publication" - ); - Err(warp_utils::reject::custom_bad_request(msg)) - }; + if !sampling_columns.is_empty() { + // Importing the columns could trigger block import and network publication in the case + // where the block was already seen on gossip. + if let Err(e) = + Box::pin(chain.process_gossip_data_columns(sampling_columns, publish_fn)).await + { + let msg = format!("Invalid data column: {e}"); + return if let BroadcastValidation::Gossip = validation_level { + Err(warp_utils::reject::broadcast_without_import(msg)) + } else { + error!( + reason = &msg, + "Invalid data column during block publication" + ); + Err(warp_utils::reject::custom_bad_request(msg)) + }; + } } } @@ -290,24 +305,19 @@ pub async fn publish_block>( message: "duplicate block".to_string(), stacktraces: vec![], }), - duplicate_status_code, + warp_utils::status_code::convert(duplicate_status_code)?, ) .into_response()) } } - Err(BlockError::DuplicateImportStatusUnknown(root)) => { + Err(BlockError::DuplicateImportStatusUnknown(_)) => { debug!( - block_root = ?root, slot = %block.slot(), "Block previously seen" ); let import_result = Box::pin(chain.process_block( block_root, - RpcBlock::new_without_blobs( - Some(block_root), - block.clone(), - network_globals.custody_columns_count() as usize, - ), + RpcBlock::new_without_blobs(Some(block_root), block.clone()), NotifyExecutionLayer::Yes, BlockImportSource::HttpApi, publish_fn, @@ -337,7 +347,7 @@ pub async fn publish_block>( type BuildDataSidecarTaskResult = Result< ( Vec>>, - Vec>>, + Vec>, ), Rejection, >; @@ -350,6 +360,7 @@ fn spawn_build_data_sidecar_task( chain: Arc>, block: Arc>>, proofs_and_blobs: UnverifiedBlobs, + current_span: Span, ) -> Result>, Rejection> { chain .clone() @@ -359,6 +370,7 @@ fn spawn_build_data_sidecar_task( let Some((kzg_proofs, blobs)) = proofs_and_blobs else { return Ok((vec![], vec![])); }; + let _guard = debug_span!(parent: current_span, "build_data_sidecars").entered(); let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); if !peer_das_enabled { @@ -369,7 +381,7 @@ fn spawn_build_data_sidecar_task( } else { // Post PeerDAS: construct data columns. let gossip_verified_data_columns = - build_gossip_verified_data_columns(&chain, &block, blobs, kzg_proofs)?; + build_data_columns(&chain, &block, blobs, kzg_proofs)?; Ok((vec![], gossip_verified_data_columns)) } }, @@ -384,58 +396,33 @@ fn spawn_build_data_sidecar_task( }) } -fn build_gossip_verified_data_columns( +/// Build data columns as wrapped `GossipVerifiedDataColumn`s. +/// There is no need to actually perform gossip verification on columns that a block producer +/// is publishing. In the locally constructed case, cell proof verification happens in the EL. +/// In the externally constructed case, there wont be any columns here. +fn build_data_columns( chain: &BeaconChain, block: &SignedBeaconBlock>, blobs: BlobsList, kzg_cell_proofs: KzgProofs, -) -> Result>>, Rejection> { +) -> Result>, Rejection> { let slot = block.slot(); let data_column_sidecars = build_blob_data_column_sidecars(chain, block, blobs, kzg_cell_proofs).map_err(|e| { error!( error = ?e, %slot, - "Invalid data column - not publishing block" + "Invalid data column - not publishing data columns" ); warp_utils::reject::custom_bad_request(format!("{e:?}")) })?; - let slot = block.slot(); let gossip_verified_data_columns = data_column_sidecars .into_iter() - .map(|data_column_sidecar| { - let column_index = data_column_sidecar.index; - let subnet = DataColumnSubnetId::from_column_index(column_index, &chain.spec); - let gossip_verified_column = - GossipVerifiedDataColumn::new(data_column_sidecar, subnet.into(), chain); - - match gossip_verified_column { - Ok(blob) => Ok(Some(blob)), - Err(GossipDataColumnError::PriorKnown { proposer, .. }) => { - // Log the error but do not abort publication, we may need to publish the block - // or some of the other data columns if the block & data columns are only - // partially published by the other publisher. - debug!( - column_index, - %slot, - proposer, - "Data column for publication already known" - ); - Ok(None) - } - Err(e) => { - error!( - column_index, - %slot, - error = ?e, - "Data column for publication is gossip-invalid" - ); - Err(warp_utils::reject::custom_bad_request(format!("{e:?}"))) - } - } + .filter_map(|data_column_sidecar| { + GossipVerifiedDataColumn::new_for_block_publishing(data_column_sidecar, chain).ok() }) - .collect::, Rejection>>()?; + .collect::>(); Ok(gossip_verified_data_columns) } @@ -506,19 +493,18 @@ fn publish_blob_sidecars( blob: &GossipVerifiedBlob, ) -> Result<(), BlockError> { let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); - crate::publish_pubsub_message(sender_clone, pubsub_message) + crate::utils::publish_pubsub_message(sender_clone, pubsub_message) .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) } fn publish_column_sidecars( sender_clone: &UnboundedSender>, - data_column_sidecars: &[Option>], + data_column_sidecars: &[GossipVerifiedDataColumn], chain: &BeaconChain, ) -> Result<(), BlockError> { let malicious_withhold_count = chain.config.malicious_withhold_count; let mut data_column_sidecars = data_column_sidecars .iter() - .flatten() .map(|d| d.clone_data_column()) .collect::>(); if malicious_withhold_count > 0 { @@ -527,7 +513,11 @@ fn publish_column_sidecars( .saturating_sub(malicious_withhold_count); // Randomize columns before dropping the last malicious_withhold_count items data_column_sidecars.shuffle(&mut **chain.rng.lock()); - data_column_sidecars.truncate(columns_to_keep); + let dropped_indices = data_column_sidecars + .drain(columns_to_keep..) + .map(|d| d.index) + .collect::>(); + debug!(indices = ?dropped_indices, "Dropping data columns from publishing"); } let pubsub_messages = data_column_sidecars .into_iter() @@ -536,7 +526,7 @@ fn publish_column_sidecars( PubsubMessage::DataColumnSidecar(Box::new((subnet, data_col))) }) .collect::>(); - crate::publish_pubsub_messages(sender_clone, pubsub_messages) + crate::utils::publish_pubsub_messages(sender_clone, pubsub_messages) .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) } @@ -628,30 +618,38 @@ pub async fn publish_blinded_block( network_tx: &UnboundedSender>, validation_level: BroadcastValidation, duplicate_status_code: StatusCode, - network_globals: Arc>, ) -> Result { let block_root = blinded_block.canonical_root(); - let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?; - publish_block::( - Some(block_root), - full_block, - chain, - network_tx, - validation_level, - duplicate_status_code, - network_globals, - ) - .await + let full_block_opt = reconstruct_block(chain.clone(), block_root, blinded_block).await?; + + if let Some(full_block) = full_block_opt { + publish_block::( + Some(block_root), + full_block, + chain, + network_tx, + validation_level, + duplicate_status_code, + ) + .await + } else { + // From the fulu fork, builders are responsible for publishing and + // will no longer return the full payload and blobs. + Ok(warp::reply().into_response()) + } } /// Deconstruct the given blinded block, and construct a full block. This attempts to use the /// execution layer's payload cache, and if that misses, attempts a blind block proposal to retrieve /// the full payload. +/// +/// From the Fulu fork, external builders no longer return the full payload and blobs, and this +/// function will always return `Ok(None)` on successful submission of blinded block. pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: Arc>, -) -> Result>>, Rejection> { +) -> Result>>>, Rejection> { let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() { let el = chain.execution_layer.as_ref().ok_or_else(|| { warp_utils::reject::custom_server_error("Missing execution layer".to_string()) @@ -691,17 +689,24 @@ pub async fn reconstruct_block( "builder", ); - let full_payload = el - .propose_blinded_beacon_block(block_root, &block) + match el + .propose_blinded_beacon_block(block_root, &block, &chain.spec) .await .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Blind block proposal failed: {:?}", e )) - })?; - info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network"); - ProvenancedPayload::Builder(full_payload) + })? { + SubmitBlindedBlockResponse::V1(full_payload) => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + ProvenancedPayload::Builder(*full_payload) + } + SubmitBlindedBlockResponse::V2 => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + return Ok(None); + } + } }; Some(full_payload_contents) @@ -729,6 +734,7 @@ pub async fn reconstruct_block( .map(|(block, blobs)| ProvenancedBlock::builder(block, blobs)) } } + .map(Some) .map_err(|e| { warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}")) }) diff --git a/beacon_node/http_api/src/publish_inclusion_lists.rs b/beacon_node/http_api/src/publish_inclusion_lists.rs index d0330ff164..b484ebfb3f 100644 --- a/beacon_node/http_api/src/publish_inclusion_lists.rs +++ b/beacon_node/http_api/src/publish_inclusion_lists.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use beacon_chain::inclusion_list_verification::GossipInclusionListError; -use beacon_chain::{validator_monitor::timestamp_now, BeaconChain, BeaconChainTypes}; +use beacon_chain::{BeaconChain, BeaconChainTypes, validator_monitor::timestamp_now}; use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; use eth2::types::Failure; use lighthouse_network::PubsubMessage; diff --git a/beacon_node/http_api/src/standard_block_rewards.rs b/beacon_node/http_api/src/standard_block_rewards.rs index 2f78649d78..fda8f0ad1d 100644 --- a/beacon_node/http_api/src/standard_block_rewards.rs +++ b/beacon_node/http_api/src/standard_block_rewards.rs @@ -1,6 +1,6 @@ -use crate::sync_committee_rewards::get_state_before_applying_block; use crate::BlockId; use crate::ExecutionOptimistic; +use crate::sync_committee_rewards::get_state_before_applying_block; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::StandardBlockReward; use std::sync::Arc; diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index a9f66de467..13fb9b2c58 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -1,5 +1,5 @@ -use crate::metrics; use crate::ExecutionOptimistic; +use crate::metrics; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::StateId as CoreStateId; use std::fmt; diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index aa126bbc82..b9fa24ad6a 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -1,12 +1,12 @@ //! Handlers for sync committee endpoints. -use crate::publish_pubsub_message; +use crate::utils::publish_pubsub_message; use beacon_chain::sync_committee_verification::{ Error as SyncVerificationError, VerifiedSyncCommitteeMessage, }; use beacon_chain::{ - validator_monitor::timestamp_now, BeaconChain, BeaconChainError, BeaconChainTypes, - StateSkipConfig, + BeaconChain, BeaconChainError, BeaconChainTypes, StateSkipConfig, + validator_monitor::timestamp_now, }; use eth2::types::{self as api_types}; use lighthouse_network::PubsubMessage; @@ -17,8 +17,8 @@ use std::collections::HashMap; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, error, warn}; use types::{ - slot_data::SlotData, BeaconStateError, Epoch, EthSpec, SignedContributionAndProof, - SyncCommitteeMessage, SyncDuty, SyncSubnetId, + BeaconStateError, Epoch, EthSpec, SignedContributionAndProof, SyncCommitteeMessage, SyncDuty, + SyncSubnetId, slot_data::SlotData, }; /// The struct that is returned to the requesting HTTP client. @@ -49,7 +49,7 @@ pub fn sync_committee_duties( return Ok(convert_to_response( verify_unknown_validators(duties, request_epoch, chain)?, execution_optimistic, - )) + )); } Err(BeaconChainError::SyncDutiesError(BeaconStateError::SyncCommitteeNotKnown { .. @@ -273,15 +273,15 @@ pub fn process_sync_committee_signatures( } } - if let Some(verified) = verified_for_pool { - if let Err(e) = chain.add_to_naive_sync_aggregation_pool(verified) { - error!( - error = ?e, - slot = %sync_committee_signature.slot, - validator_index = sync_committee_signature.validator_index, - "Unable to add sync committee signature to pool" - ); - } + if let Some(verified) = verified_for_pool + && let Err(e) = chain.add_to_naive_sync_aggregation_pool(verified) + { + error!( + error = ?e, + slot = %sync_committee_signature.slot, + validator_index = sync_committee_signature.validator_index, + "Unable to add sync committee signature to pool" + ); } } @@ -320,6 +320,38 @@ pub fn process_signed_contribution_and_proofs( let seen_timestamp = timestamp_now(); + if let Some(latest_optimistic_update) = chain + .light_client_server_cache + .should_broadcast_latest_optimistic_update() + { + let _ = publish_pubsub_message( + &network_tx, + PubsubMessage::LightClientOptimisticUpdate(Box::new(latest_optimistic_update)), + ) + .inspect_err(|e| { + error!( + error = ?e, + "Unable to broadcast latest light client optimistic update" + ); + }); + }; + + if let Some(latest_finality_update) = chain + .light_client_server_cache + .should_broadcast_latest_finality_update() + { + let _ = publish_pubsub_message( + &network_tx, + PubsubMessage::LightClientFinalityUpdate(Box::new(latest_finality_update)), + ) + .inspect_err(|e| { + error!( + error = ?e, + "Unable to broadcast latest light client finality update" + ); + }); + }; + // Verify contributions & broadcast to the network. for (index, contribution) in signed_contribution_and_proofs.into_iter().enumerate() { let aggregator_index = contribution.message.aggregator_index; diff --git a/beacon_node/http_api/src/task_spawner.rs b/beacon_node/http_api/src/task_spawner.rs index a679b294f6..834cd29971 100644 --- a/beacon_node/http_api/src/task_spawner.rs +++ b/beacon_node/http_api/src/task_spawner.rs @@ -30,6 +30,7 @@ impl Priority { } /// Spawns tasks on the `BeaconProcessor` or directly on the tokio executor. +#[derive(Clone)] pub struct TaskSpawner { /// Used to send tasks to the `BeaconProcessor`. The tokio executor will be /// used if this is `None`. @@ -155,6 +156,32 @@ impl TaskSpawner { .and_then(|x| x) } } + + pub fn try_send(&self, work_event: WorkEvent) -> Result<(), warp::Rejection> { + if let Some(beacon_processor_send) = &self.beacon_processor_send { + let error_message = match beacon_processor_send.try_send(work_event) { + Ok(()) => None, + Err(TrySendError::Full(_)) => { + Some("The task was dropped. The server is overloaded.") + } + Err(TrySendError::Closed(_)) => { + Some("The task was dropped. The server is shutting down.") + } + }; + + if let Some(error_message) = error_message { + return Err(warp_utils::reject::custom_server_error( + error_message.to_string(), + )); + }; + + Ok(()) + } else { + Err(warp_utils::reject::custom_server_error( + "The beacon processor is unavailable".to_string(), + )) + } + } } /// Send a task to the beacon processor and await execution. diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index f78a361dad..27e2a27d35 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -1,7 +1,8 @@ use crate::{Config, Context}; use beacon_chain::{ - test_utils::{BeaconChainHarness, BoxedMutator, Builder, EphemeralHarnessType}, BeaconChain, BeaconChainTypes, + custody_context::NodeCustodyType, + test_utils::{BeaconChainHarness, BoxedMutator, Builder, EphemeralHarnessType}, }; use beacon_processor::{ BeaconProcessor, BeaconProcessorChannels, BeaconProcessorConfig, BeaconProcessorQueueLengths, @@ -10,14 +11,14 @@ use directory::DEFAULT_ROOT_DIR; use eth2::{BeaconNodeHttpClient, Timeouts}; use lighthouse_network::rpc::methods::MetaDataV3; use lighthouse_network::{ + ConnectedPoint, Enr, NetworkConfig, NetworkGlobals, PeerId, PeerManager, discv5::enr::CombinedKey, libp2p::swarm::{ - behaviour::{ConnectionEstablished, FromSwarm}, ConnectionId, NetworkBehaviour, + behaviour::{ConnectionEstablished, FromSwarm}, }, rpc::methods::{MetaData, MetaDataV2}, types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState}, - ConnectedPoint, Enr, NetworkConfig, NetworkGlobals, PeerId, PeerManager, }; use network::{NetworkReceivers, NetworkSenders}; use sensitive_url::SensitiveUrl; @@ -60,8 +61,29 @@ type Mutator = BoxedMutator, MemoryStore>; impl InteractiveTester { pub async fn new(spec: Option, validator_count: usize) -> Self { - Self::new_with_initializer_and_mutator(spec, validator_count, None, None, Config::default()) - .await + Self::new_with_initializer_and_mutator( + spec, + validator_count, + None, + None, + Config::default(), + true, + NodeCustodyType::Fullnode, + ) + .await + } + + pub async fn new_supernode(spec: Option, validator_count: usize) -> Self { + Self::new_with_initializer_and_mutator( + spec, + validator_count, + None, + None, + Config::default(), + true, + NodeCustodyType::Supernode, + ) + .await } pub async fn new_with_initializer_and_mutator( @@ -70,6 +92,8 @@ impl InteractiveTester { initializer: Option>, mutator: Option>, config: Config, + use_mock_builder: bool, + node_custody_type: NodeCustodyType, ) -> Self { let mut harness_builder = BeaconChainHarness::builder(E::default()) .spec_or_default(spec.map(Arc::new)) @@ -85,13 +109,15 @@ impl InteractiveTester { .fresh_ephemeral_store() }; + harness_builder = harness_builder.node_custody_type(node_custody_type); + // Add a mutator for the beacon chain builder which will be called in // `HarnessBuilder::build`. if let Some(mutator) = mutator { harness_builder = harness_builder.initial_mutator(mutator); } - let harness = harness_builder.build(); + let mut harness = harness_builder.build(); let ApiServer { ctx, @@ -103,15 +129,47 @@ impl InteractiveTester { tokio::spawn(server); - let client = BeaconNodeHttpClient::new( - SensitiveUrl::parse(&format!( - "http://{}:{}", - listening_socket.ip(), - listening_socket.port() - )) - .unwrap(), - Timeouts::set_all(Duration::from_secs(1)), - ); + // Late-initalize the mock builder now that the mock execution node and beacon API ports + // have been allocated. + let beacon_api_ip = listening_socket.ip(); + let beacon_api_port = listening_socket.port(); + let beacon_url = + SensitiveUrl::parse(format!("http://{beacon_api_ip}:{beacon_api_port}").as_str()) + .unwrap(); + + // We disable apply_operations because it breaks the mock builder's ability to return + // payloads. + let apply_operations = false; + + // We disable strict registration checks too, because it makes HTTP tests less fiddly to + // write. + let strict_registrations = false; + + // Broadcast to the BN only if Fulu is scheduled. In the broadcast validation tests we want + // to infer things from the builder return code, and pre-Fulu it's simpler to let the BN + // handle broadcast and return detailed codes. Post-Fulu the builder doesn't return the + // block at all, so we *need* the builder to do the broadcast and return a 400 if the block + // is invalid. + let broadcast_to_bn = ctx.chain.as_ref().unwrap().spec.is_fulu_scheduled(); + + if use_mock_builder { + let mock_builder_server = harness.set_mock_builder( + beacon_url.clone(), + strict_registrations, + apply_operations, + broadcast_to_bn, + ); + + tokio::spawn(mock_builder_server); + } + + // Use 5s timeouts on CI, as there are several sources of artifical slowness, including + // mock-builder. + let timeouts = Timeouts { + default: Duration::from_secs(5), + ..Timeouts::set_all(Duration::from_secs(5)) + }; + let client = BeaconNodeHttpClient::new(beacon_url.clone(), timeouts); Self { ctx, @@ -125,7 +183,7 @@ impl InteractiveTester { pub async fn create_api_server( chain: Arc>, test_runtime: &TestRuntime, -) -> ApiServer> { +) -> ApiServer + use> { create_api_server_with_config(chain, Config::default(), test_runtime).await } @@ -133,7 +191,7 @@ pub async fn create_api_server_with_config( chain: Arc>, http_config: Config, test_runtime: &TestRuntime, -) -> ApiServer> { +) -> ApiServer + use> { // Use port 0 to allocate a new unused port. let port = 0; @@ -188,8 +246,6 @@ pub async fn create_api_server_with_config( })); *network_globals.sync_state.write() = SyncState::Synced; - let eth1_service = eth1::Service::new(eth1::Config::default(), chain.spec.clone()).unwrap(); - let beacon_processor_config = BeaconProcessorConfig { // The number of workers must be greater than one. Tests which use the // builder workflow sometimes require an internal HTTP request in order @@ -201,12 +257,9 @@ pub async fn create_api_server_with_config( let BeaconProcessorChannels { beacon_processor_tx, beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx, } = BeaconProcessorChannels::new(&beacon_processor_config); let beacon_processor_send = beacon_processor_tx; - let reprocess_send = work_reprocessing_tx.clone(); BeaconProcessor { network_globals: network_globals.clone(), executor: test_runtime.task_executor.clone(), @@ -215,8 +268,6 @@ pub async fn create_api_server_with_config( } .spawn_manager( beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx, None, chain.slot_clock.clone(), chain.spec.maximum_gossip_clock_disparity(), @@ -241,8 +292,6 @@ pub async fn create_api_server_with_config( network_senders: Some(network_senders), network_globals: Some(network_globals), beacon_processor_send: Some(beacon_processor_send), - beacon_processor_reprocess_send: Some(reprocess_send), - eth1_service: Some(eth1_service), sse_logging_components: None, }); diff --git a/beacon_node/http_api/src/ui.rs b/beacon_node/http_api/src/ui.rs index 80a9ed896d..1538215a0b 100644 --- a/beacon_node/http_api/src/ui.rs +++ b/beacon_node/http_api/src/ui.rs @@ -1,5 +1,5 @@ use beacon_chain::{ - validator_monitor::HISTORIC_EPOCHS, BeaconChain, BeaconChainError, BeaconChainTypes, + BeaconChain, BeaconChainError, BeaconChainTypes, validator_monitor::HISTORIC_EPOCHS, }; use eth2::types::{Epoch, ValidatorStatus}; use serde::{Deserialize, Serialize}; @@ -126,23 +126,22 @@ pub fn get_validator_info( let mut validators = HashMap::new(); for id in ids { - if let Ok(index) = id.parse::() { - if let Some(validator) = chain + if let Ok(index) = id.parse::() + && let Some(validator) = chain .validator_monitor .read() .get_monitored_validator(index) - { - let mut info = vec![]; - for epoch in epochs.clone() { - if let Some(total_balance) = validator.get_total_balance(Epoch::new(epoch)) { - info.push(ValidatorInfoValues { - epoch, - total_balance, - }); - } + { + let mut info = vec![]; + for epoch in epochs.clone() { + if let Some(total_balance) = validator.get_total_balance(Epoch::new(epoch)) { + info.push(ValidatorInfoValues { + epoch, + total_balance, + }); } - validators.insert(id.clone(), ValidatorInfo { info }); } + validators.insert(id.clone(), ValidatorInfo { info }); } } @@ -198,58 +197,57 @@ pub fn post_validator_monitor_metrics( let mut validators = HashMap::new(); for id in ids { - if let Ok(index) = id.parse::() { - if let Some(validator) = chain + if let Ok(index) = id.parse::() + && let Some(validator) = chain .validator_monitor .read() .get_monitored_validator(index) - { - let val_metrics = validator.metrics.read(); - let attestation_hits = val_metrics.attestation_hits; - let attestation_misses = val_metrics.attestation_misses; - let attestation_head_hits = val_metrics.attestation_head_hits; - let attestation_head_misses = val_metrics.attestation_head_misses; - let attestation_target_hits = val_metrics.attestation_target_hits; - let attestation_target_misses = val_metrics.attestation_target_misses; - let latest_attestation_inclusion_distance = - val_metrics.latest_attestation_inclusion_distance; - drop(val_metrics); + { + let val_metrics = validator.metrics.read(); + let attestation_hits = val_metrics.attestation_hits; + let attestation_misses = val_metrics.attestation_misses; + let attestation_head_hits = val_metrics.attestation_head_hits; + let attestation_head_misses = val_metrics.attestation_head_misses; + let attestation_target_hits = val_metrics.attestation_target_hits; + let attestation_target_misses = val_metrics.attestation_target_misses; + let latest_attestation_inclusion_distance = + val_metrics.latest_attestation_inclusion_distance; + drop(val_metrics); - let attestations = attestation_hits + attestation_misses; - let attestation_hit_percentage: f64 = if attestations == 0 { - 0.0 - } else { - (100 * attestation_hits / attestations) as f64 - }; - let head_attestations = attestation_head_hits + attestation_head_misses; - let attestation_head_hit_percentage: f64 = if head_attestations == 0 { - 0.0 - } else { - (100 * attestation_head_hits / head_attestations) as f64 - }; + let attestations = attestation_hits + attestation_misses; + let attestation_hit_percentage: f64 = if attestations == 0 { + 0.0 + } else { + (100 * attestation_hits / attestations) as f64 + }; + let head_attestations = attestation_head_hits + attestation_head_misses; + let attestation_head_hit_percentage: f64 = if head_attestations == 0 { + 0.0 + } else { + (100 * attestation_head_hits / head_attestations) as f64 + }; - let target_attestations = attestation_target_hits + attestation_target_misses; - let attestation_target_hit_percentage: f64 = if target_attestations == 0 { - 0.0 - } else { - (100 * attestation_target_hits / target_attestations) as f64 - }; + let target_attestations = attestation_target_hits + attestation_target_misses; + let attestation_target_hit_percentage: f64 = if target_attestations == 0 { + 0.0 + } else { + (100 * attestation_target_hits / target_attestations) as f64 + }; - let metrics = ValidatorMetrics { - attestation_hits, - attestation_misses, - attestation_hit_percentage, - attestation_head_hits, - attestation_head_misses, - attestation_head_hit_percentage, - attestation_target_hits, - attestation_target_misses, - attestation_target_hit_percentage, - latest_attestation_inclusion_distance, - }; + let metrics = ValidatorMetrics { + attestation_hits, + attestation_misses, + attestation_hit_percentage, + attestation_head_hits, + attestation_head_misses, + attestation_head_hit_percentage, + attestation_target_hits, + attestation_target_misses, + attestation_target_hit_percentage, + latest_attestation_inclusion_distance, + }; - validators.insert(id.clone(), metrics); - } + validators.insert(id.clone(), metrics); } } diff --git a/beacon_node/http_api/src/utils.rs b/beacon_node/http_api/src/utils.rs new file mode 100644 index 0000000000..f2b859ebe5 --- /dev/null +++ b/beacon_node/http_api/src/utils.rs @@ -0,0 +1,90 @@ +use crate::task_spawner::TaskSpawner; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::EndpointVersion; +use lighthouse_network::PubsubMessage; +use lighthouse_network::rpc::methods::MetaData; +use network::{NetworkMessage, ValidatorSubscriptionMessage}; +use parking_lot::RwLock; +use std::sync::Arc; +use tokio::sync::mpsc::{Sender, UnboundedSender}; +use types::{ChainSpec, EthSpec, ForkName}; +use warp::Rejection; +use warp::filters::BoxedFilter; + +pub type ResponseFilter = BoxedFilter<(warp::reply::Response,)>; +pub type AnyVersionFilter = BoxedFilter<(EndpointVersion,)>; +pub type EthV1Filter = BoxedFilter<()>; +pub type ChainFilter = BoxedFilter<(Arc>,)>; +pub type NotWhileSyncingFilter = BoxedFilter<(Result<(), Rejection>,)>; +pub type TaskSpawnerFilter = BoxedFilter<(TaskSpawner<::EthSpec>,)>; +pub type ValidatorSubscriptionTxFilter = BoxedFilter<(Sender,)>; +pub type NetworkTxFilter = + BoxedFilter<(UnboundedSender::EthSpec>>,)>; +pub type OptionalConsensusVersionHeaderFilter = BoxedFilter<(Option,)>; + +pub fn from_meta_data( + meta_data: &RwLock>, + spec: &ChainSpec, +) -> eth2::types::MetaData { + let meta_data = meta_data.read(); + let format_hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); + + let seq_number = *meta_data.seq_number(); + let attnets = format_hex(&meta_data.attnets().clone().into_bytes()); + let syncnets = format_hex( + &meta_data + .syncnets() + .cloned() + .unwrap_or_default() + .into_bytes(), + ); + + if spec.is_peer_das_scheduled() { + eth2::types::MetaData::V3(eth2::types::MetaDataV3 { + seq_number, + attnets, + syncnets, + custody_group_count: meta_data.custody_group_count().cloned().unwrap_or_default(), + }) + } else { + eth2::types::MetaData::V2(eth2::types::MetaDataV2 { + seq_number, + attnets, + syncnets, + }) + } +} + +/// Publish a message to the libp2p pubsub network. +pub fn publish_pubsub_message( + network_tx: &UnboundedSender>, + message: PubsubMessage, +) -> Result<(), warp::Rejection> { + publish_network_message( + network_tx, + NetworkMessage::Publish { + messages: vec![message], + }, + ) +} + +/// Publish a message to the libp2p pubsub network. +pub fn publish_pubsub_messages( + network_tx: &UnboundedSender>, + messages: Vec>, +) -> Result<(), warp::Rejection> { + publish_network_message(network_tx, NetworkMessage::Publish { messages }) +} + +/// Publish a message to the libp2p network. +pub fn publish_network_message( + network_tx: &UnboundedSender>, + message: NetworkMessage, +) -> Result<(), warp::Rejection> { + network_tx.send(message).map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "unable to publish to network channel: {}", + e + )) + }) +} diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs deleted file mode 100644 index 25b0feb99e..0000000000 --- a/beacon_node/http_api/src/validator.rs +++ /dev/null @@ -1,22 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use types::{BeaconState, PublicKeyBytes}; - -/// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator -/// index and then ensures that the validator exists in the given `state`. -pub fn pubkey_to_validator_index( - chain: &BeaconChain, - state: &BeaconState, - pubkey: &PublicKeyBytes, -) -> Result, Box> { - chain - .validator_index(pubkey) - .map_err(Box::new)? - .filter(|&index| { - state - .validators() - .get(index) - .is_some_and(|v| v.pubkey == *pubkey) - }) - .map(Result::Ok) - .transpose() -} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs new file mode 100644 index 0000000000..8baf7c5245 --- /dev/null +++ b/beacon_node/http_api/src/validator/mod.rs @@ -0,0 +1,972 @@ +use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, + ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message, +}; +use crate::version::V3; +use crate::{StateId, attester_duties, proposer_duties, sync_committees}; +use beacon_chain::attestation_verification::VerifiedAttestation; +use beacon_chain::validator_monitor::timestamp_now; +use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes}; +use bls::PublicKeyBytes; +use eth2::StatusCode; +use eth2::types::{ + Accept, BeaconCommitteeSubscription, EndpointVersion, Failure, GenericResponse, + StandardLivenessResponseData, StateId as CoreStateId, ValidatorAggregateAttestationQuery, + ValidatorAttestationDataQuery, ValidatorBlocksQuery, ValidatorIndexData, ValidatorStatus, +}; +use lighthouse_network::PubsubMessage; +use network::{NetworkMessage, ValidatorSubscriptionMessage}; +use slot_clock::SlotClock; +use std::sync::Arc; +use tokio::sync::mpsc::{Sender, UnboundedSender}; +use tokio::sync::oneshot; +use tracing::{debug, error, info, warn}; +use types::{ + BeaconState, Epoch, EthSpec, ProposerPreparationData, SignedAggregateAndProof, + SignedContributionAndProof, SignedValidatorRegistrationData, Slot, SyncContributionData, + ValidatorSubscription, +}; +use warp::{Filter, Rejection, Reply}; +use warp_utils::reject::convert_rejection; + +/// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator +/// index and then ensures that the validator exists in the given `state`. +pub fn pubkey_to_validator_index( + chain: &BeaconChain, + state: &BeaconState, + pubkey: &PublicKeyBytes, +) -> Result, Box> { + chain + .validator_index(pubkey) + .map_err(Box::new)? + .filter(|&index| { + state + .validators() + .get(index) + .is_some_and(|v| v.pubkey == *pubkey) + }) + .map(Result::Ok) + .transpose() +} + +// GET validator/sync_committee_contribution +pub fn get_validator_sync_committee_contribution( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("sync_committee_contribution")) + .and(warp::path::end()) + .and(warp::query::()) + .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |sync_committee_data: SyncContributionData, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + chain + .get_aggregated_sync_committee_contribution(&sync_committee_data) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "unable to fetch sync contribution: {:?}", + e + )) + })? + .map(GenericResponse::from) + .ok_or_else(|| { + warp_utils::reject::custom_not_found( + "no matching sync contribution found".to_string(), + ) + }) + }) + }, + ) + .boxed() +} + +// POST validator/duties/sync/{epoch} +pub fn post_validator_duties_sync( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("sync")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid epoch".to_string(), + )) + })) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp_utils::json::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, + indices: ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + sync_committees::sync_committee_duties(epoch, &indices.0, &chain) + }) + }, + ) + .boxed() +} + +// POST validator/duties/attester/{epoch} +pub fn post_validator_duties_attester( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("attester")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid epoch".to_string(), + )) + })) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp_utils::json::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, + indices: ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + attester_duties::attester_duties(epoch, &indices.0, &chain) + }) + }, + ) + .boxed() +} + +// GET validator/aggregate_attestation?attestation_data_root,slot +pub fn get_validator_aggregate_attestation( + any_version: AnyVersionFilter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + any_version + .and(warp::path("validator")) + .and(warp::path("aggregate_attestation")) + .and(warp::path::end()) + .and(warp::query::()) + .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |endpoint_version: EndpointVersion, + query: ValidatorAggregateAttestationQuery, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P0, move || { + not_synced_filter?; + crate::aggregate_attestation::get_aggregate_attestation( + query.slot, + &query.attestation_data_root, + query.committee_index, + endpoint_version, + chain, + ) + }) + }, + ) + .boxed() +} + +// GET validator/attestation_data?slot,committee_index +pub fn get_validator_attestation_data( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("attestation_data")) + .and(warp::path::end()) + .and(warp::query::()) + .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |query: ValidatorAttestationDataQuery, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + + let current_slot = chain.slot().map_err(warp_utils::reject::unhandled_error)?; + + // allow a tolerance of one slot to account for clock skew + if query.slot > current_slot + 1 { + return Err(warp_utils::reject::custom_bad_request(format!( + "request slot {} is more than one slot past the current slot {}", + query.slot, current_slot + ))); + } + + chain + .produce_unaggregated_attestation(query.slot, query.committee_index) + .map(|attestation| attestation.data().clone()) + .map(GenericResponse::from) + .map_err(warp_utils::reject::unhandled_error) + }) + }, + ) + .boxed() +} + +// GET validator/blinded_blocks/{slot} +pub fn get_validator_blinded_blocks( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("blinded_blocks")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp::query::()) + .and(warp::header::optional::("accept")) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |slot: Slot, + not_synced_filter: Result<(), Rejection>, + query: ValidatorBlocksQuery, + accept_header: Option, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + not_synced_filter?; + produce_blinded_block_v2(accept_header, chain, slot, query).await + }) + }, + ) + .boxed() +} + +// GET validator/blocks/{slot} +pub fn get_validator_blocks( + any_version: AnyVersionFilter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + any_version + .and(warp::path("validator")) + .and(warp::path("blocks")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(warp::query::()) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |endpoint_version: EndpointVersion, + slot: Slot, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + query: ValidatorBlocksQuery, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Block production request from HTTP API"); + + not_synced_filter?; + + if endpoint_version == V3 { + produce_block_v3(accept_header, chain, slot, query).await + } else { + produce_block_v2(accept_header, chain, slot, query).await + } + }) + }, + ) + .boxed() +} + +// POST validator/liveness/{epoch} +pub fn post_validator_liveness_epoch( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("liveness")) + .and(warp::path::param::()) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |epoch: Epoch, + indices: ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + // Ensure the request is for either the current, previous or next epoch. + let current_epoch = + chain.epoch().map_err(warp_utils::reject::unhandled_error)?; + let prev_epoch = current_epoch.saturating_sub(Epoch::new(1)); + let next_epoch = current_epoch.saturating_add(Epoch::new(1)); + + if epoch < prev_epoch || epoch > next_epoch { + return Err(warp_utils::reject::custom_bad_request(format!( + "request epoch {} is more than one epoch from the current epoch {}", + epoch, current_epoch + ))); + } + + let liveness: Vec = indices + .0 + .iter() + .cloned() + .map(|index| { + let is_live = chain.validator_seen_at_epoch(index as usize, epoch); + StandardLivenessResponseData { index, is_live } + }) + .collect(); + + Ok(GenericResponse::from(liveness)) + }) + }, + ) + .boxed() +} + +// POST validator/sync_committee_subscriptions +pub fn post_validator_sync_committee_subscriptions( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + validator_subscription_tx_filter: ValidatorSubscriptionTxFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("sync_committee_subscriptions")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(validator_subscription_tx_filter) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |subscriptions: Vec, + validator_subscription_tx: Sender, + task_spawner: TaskSpawner, + chain: Arc>, + | { + task_spawner.blocking_json_task(Priority::P0, move || { + for subscription in subscriptions { + chain + .validator_monitor + .write() + .auto_register_local_validator(subscription.validator_index); + + let message = ValidatorSubscriptionMessage::SyncCommitteeSubscribe { + subscriptions: vec![subscription], + }; + if let Err(e) = validator_subscription_tx.try_send(message) { + warn!( + info = "the host may be overloaded or resource-constrained", + error = ?e, + "Unable to process sync subscriptions" + ); + return Err(warp_utils::reject::custom_server_error( + "unable to queue subscription, host may be overloaded or shutting down".to_string(), + )); + } + } + + Ok(()) + }) + }, + ).boxed() +} + +// POST validator/register_validator +pub fn post_validator_register_validator( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("register_validator")) + .and(warp::path::end()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp_utils::json::json()) + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + register_val_data: Vec| async { + let (tx, rx) = oneshot::channel(); + + let initial_result = task_spawner + .spawn_async_with_rejection_no_conversion(Priority::P0, async move { + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BeaconChainError::ExecutionLayerMissing) + .map_err(warp_utils::reject::unhandled_error)?; + let current_slot = chain + .slot_clock + .now_or_genesis() + .ok_or(BeaconChainError::UnableToReadSlot) + .map_err(warp_utils::reject::unhandled_error)?; + let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); + + debug!( + count = register_val_data.len(), + "Received register validator request" + ); + + let head_snapshot = chain.head_snapshot(); + let spec = &chain.spec; + + let (preparation_data, filtered_registration_data): ( + Vec<(ProposerPreparationData, Option)>, + Vec, + ) = register_val_data + .into_iter() + .filter_map(|register_data| { + chain + .validator_index(®ister_data.message.pubkey) + .ok() + .flatten() + .and_then(|validator_index| { + let validator = head_snapshot + .beacon_state + .get_validator(validator_index) + .ok()?; + let validator_status = ValidatorStatus::from_validator( + validator, + current_epoch, + spec.far_future_epoch, + ) + .superstatus(); + let is_active_or_pending = + matches!(validator_status, ValidatorStatus::Pending) + || matches!( + validator_status, + ValidatorStatus::Active + ); + + // Filter out validators who are not 'active' or 'pending'. + is_active_or_pending.then_some({ + ( + ( + ProposerPreparationData { + validator_index: validator_index as u64, + fee_recipient: register_data + .message + .fee_recipient, + }, + Some(register_data.message.gas_limit), + ), + register_data, + ) + }) + }) + }) + .unzip(); + + // Update the prepare beacon proposer cache based on this request. + execution_layer + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|(data, limit)| (data, limit)), + ) + .await; + + // Call prepare beacon proposer blocking with the latest update in order to make + // sure we have a local payload to fall back to in the event of the blinded block + // flow failing. + chain + .prepare_beacon_proposer(current_slot) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + + info!( + count = filtered_registration_data.len(), + "Forwarding register validator request to connected builder" + ); + + // It's a waste of a `BeaconProcessor` worker to just + // wait on a response from the builder (especially since + // they have frequent timeouts). Spawn a new task and + // send the response back to our original HTTP request + // task via a channel. + let builder_future = async move { + let arc_builder = chain + .execution_layer + .as_ref() + .ok_or(BeaconChainError::ExecutionLayerMissing) + .map_err(warp_utils::reject::unhandled_error)? + .builder(); + let builder = arc_builder + .as_ref() + .ok_or(BeaconChainError::BuilderMissing) + .map_err(warp_utils::reject::unhandled_error)?; + builder + .post_builder_validators(&filtered_registration_data) + .await + .map(|resp| warp::reply::json(&resp).into_response()) + .map_err(|e| { + warn!( + num_registrations = filtered_registration_data.len(), + error = ?e, + "Relay error when registering validator(s)" + ); + // Forward the HTTP status code if we are able to, otherwise fall back + // to a server error. + if let eth2::Error::ServerMessage(message) = e { + if message.code == StatusCode::BAD_REQUEST.as_u16() { + return warp_utils::reject::custom_bad_request( + message.message, + ); + } else { + // According to the spec this response should only be a 400 or 500, + // so we fall back to a 500 here. + return warp_utils::reject::custom_server_error( + message.message, + ); + } + } + warp_utils::reject::custom_server_error(format!("{e:?}")) + }) + }; + tokio::task::spawn(async move { tx.send(builder_future.await) }); + + // Just send a generic 200 OK from this closure. We'll + // ignore the `Ok` variant and form a proper response + // from what is sent back down the channel. + Ok(warp::reply::reply().into_response()) + }) + .await; + + if initial_result.is_err() { + return convert_rejection(initial_result).await; + } + + // Await a response from the builder without blocking a + // `BeaconProcessor` worker. + convert_rejection(rx.await.unwrap_or_else(|_| { + Ok(warp::reply::with_status( + warp::reply::json(&"No response from channel"), + warp::http::StatusCode::INTERNAL_SERVER_ERROR, + ) + .into_response()) + })) + .await + }, + ) + .boxed() +} + +// POST validator/prepare_beacon_proposer +pub fn post_validator_prepare_beacon_proposer( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("prepare_beacon_proposer")) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(network_tx_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp_utils::json::json()) + .then( + |not_synced_filter: Result<(), Rejection>, + network_tx: UnboundedSender>, + task_spawner: TaskSpawner, + chain: Arc>, + preparation_data: Vec| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + not_synced_filter?; + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BeaconChainError::ExecutionLayerMissing) + .map_err(warp_utils::reject::unhandled_error)?; + + let current_slot = chain + .slot_clock + .now_or_genesis() + .ok_or(BeaconChainError::UnableToReadSlot) + .map_err(warp_utils::reject::unhandled_error)?; + let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); + + debug!( + count = preparation_data.len(), + "Received proposer preparation data" + ); + + execution_layer + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|data| (data, &None)), + ) + .await; + + chain + .prepare_beacon_proposer(current_slot) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + + if chain.spec.is_peer_das_scheduled() { + let (finalized_beacon_state, _, _) = + StateId(CoreStateId::Finalized).state(&chain)?; + let validators_and_balances = preparation_data + .iter() + .filter_map(|preparation| { + if let Ok(effective_balance) = finalized_beacon_state + .get_effective_balance(preparation.validator_index as usize) + { + Some((preparation.validator_index as usize, effective_balance)) + } else { + None + } + }) + .collect::>(); + + let current_slot = + chain.slot().map_err(warp_utils::reject::unhandled_error)?; + if let Some(cgc_change) = chain + .data_availability_checker + .custody_context() + .register_validators(validators_and_balances, current_slot, &chain.spec) + { + chain.update_data_column_custody_info(Some( + cgc_change + .effective_epoch + .start_slot(T::EthSpec::slots_per_epoch()), + )); + + network_tx.send(NetworkMessage::CustodyCountChanged { + new_custody_group_count: cgc_change.new_custody_group_count, + sampling_count: cgc_change.sampling_count, + }).unwrap_or_else(|e| { + debug!(error = %e, "Could not send message to the network service. \ + Likely shutdown") + }); + } + } + + Ok::<_, warp::reject::Rejection>(warp::reply::json(&()).into_response()) + }) + }, + ) + .boxed() +} + +// POST validator/beacon_committee_subscriptions +pub fn post_validator_beacon_committee_subscriptions( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + validator_subscription_tx_filter: ValidatorSubscriptionTxFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("beacon_committee_subscriptions")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(validator_subscription_tx_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |committee_subscriptions: Vec, + validator_subscription_tx: Sender, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let subscriptions: std::collections::BTreeSet<_> = committee_subscriptions + .iter() + .map(|subscription| { + chain + .validator_monitor + .write() + .auto_register_local_validator(subscription.validator_index); + ValidatorSubscription { + attestation_committee_index: subscription.committee_index, + slot: subscription.slot, + committee_count_at_slot: subscription.committees_at_slot, + is_aggregator: subscription.is_aggregator, + } + }) + .collect(); + + let message = + ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions }; + if let Err(e) = validator_subscription_tx.try_send(message) { + warn!( + info = "the host may be overloaded or resource-constrained", + error = ?e, + "Unable to process committee subscriptions" + ); + return Err(warp_utils::reject::custom_server_error( + "unable to queue subscription, host may be overloaded or shutting down" + .to_string(), + )); + } + Ok(()) + }) + }, + ) + .boxed() +} + +pub fn post_validator_contribution_and_proofs( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("contribution_and_proofs")) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + |not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>, + contributions: Vec>, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + sync_committees::process_signed_contribution_and_proofs( + contributions, + network_tx, + &chain, + )?; + Ok(GenericResponse::from(())) + }) + }, + ) + .boxed() +} + +// POST validator/aggregate_and_proofs +pub fn post_validator_aggregate_and_proofs( + any_version: AnyVersionFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + any_version + .and(warp::path("validator")) + .and(warp::path("aggregate_and_proofs")) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .then( + // V1 and V2 are identical except V2 has a consensus version header in the request. + // We only require this header for SSZ deserialization, which isn't supported for + // this endpoint presently. + |_endpoint_version: EndpointVersion, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>, + aggregates: Vec>, + network_tx: UnboundedSender>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + let seen_timestamp = timestamp_now(); + let mut verified_aggregates = Vec::with_capacity(aggregates.len()); + let mut messages = Vec::with_capacity(aggregates.len()); + let mut failures = Vec::new(); + + // Verify that all messages in the post are valid before processing further + for (index, aggregate) in aggregates.iter().enumerate() { + match chain.verify_aggregated_attestation_for_gossip(aggregate) { + Ok(verified_aggregate) => { + messages.push(PubsubMessage::AggregateAndProofAttestation(Box::new( + verified_aggregate.aggregate().clone(), + ))); + + // Notify the validator monitor. + chain + .validator_monitor + .read() + .register_api_aggregated_attestation( + seen_timestamp, + verified_aggregate.aggregate(), + verified_aggregate.indexed_attestation(), + &chain.slot_clock, + ); + + verified_aggregates.push((index, verified_aggregate)); + } + // If we already know the attestation, don't broadcast it or attempt to + // further verify it. Return success. + // + // It's reasonably likely that two different validators produce + // identical aggregates, especially if they're using the same beacon + // node. + Err(AttestationError::AttestationSupersetKnown(_)) => continue, + // If we've already seen this aggregator produce an aggregate, just + // skip this one. + // + // We're likely to see this with VCs that use fallback BNs. The first + // BN might time-out *after* publishing the aggregate and then the + // second BN will indicate it's already seen the aggregate. + // + // There's no actual error for the user or the network since the + // aggregate has been successfully published by some other node. + Err(AttestationError::AggregatorAlreadyKnown(_)) => continue, + Err(e) => { + error!( + error = ?e, + request_index = index, + aggregator_index = aggregate.message().aggregator_index(), + attestation_index = aggregate.message().aggregate().committee_index(), + attestation_slot = %aggregate.message().aggregate().data().slot, + "Failure verifying aggregate and proofs" + ); + failures.push(Failure::new(index, format!("Verification: {:?}", e))); + } + } + } + + // Publish aggregate attestations to the libp2p network + if !messages.is_empty() { + publish_network_message(&network_tx, NetworkMessage::Publish { messages })?; + } + + // Import aggregate attestations + for (index, verified_aggregate) in verified_aggregates { + if let Err(e) = chain.apply_attestation_to_fork_choice(&verified_aggregate) { + error!( + error = ?e, + request_index = index, + aggregator_index = verified_aggregate.aggregate().message().aggregator_index(), + attestation_index = verified_aggregate.attestation().committee_index(), + attestation_slot = %verified_aggregate.attestation().data().slot, + "Failure applying verified aggregate attestation to fork choice" + ); + failures.push(Failure::new(index, format!("Fork choice: {:?}", e))); + } + if let Err(e) = chain.add_to_block_inclusion_pool(verified_aggregate) { + warn!( + error = ?e, + request_index = index, + "Could not add verified aggregate attestation to the inclusion pool" + ); + failures.push(Failure::new(index, format!("Op pool: {:?}", e))); + } + } + + if !failures.is_empty() { + Err(warp_utils::reject::indexed_bad_request("error processing aggregate and proofs".to_string(), + failures, + )) + } else { + Ok(()) + } + }) + }, + ).boxed() +} + +// GET validator/duties/proposer/{epoch} +pub fn get_validator_duties_proposer( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("proposer")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid epoch".to_string(), + )) + })) + .and(warp::path::end()) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + proposer_duties::proposer_duties(epoch, &chain) + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator_inclusion.rs b/beacon_node/http_api/src/validator_inclusion.rs index dd4e137ce6..16010b63f3 100644 --- a/beacon_node/http_api/src/validator_inclusion.rs +++ b/beacon_node/http_api/src/validator_inclusion.rs @@ -4,7 +4,7 @@ use eth2::{ lighthouse::{GlobalValidatorInclusionData, ValidatorInclusionData}, types::ValidatorId, }; -use state_processing::per_epoch_processing::{process_epoch, EpochProcessingSummary}; +use state_processing::per_epoch_processing::{EpochProcessingSummary, process_epoch}; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; /// Returns the state in the last slot of `epoch`. diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 90ddd1ee8f..6856318265 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -2,7 +2,7 @@ use crate::state_id::StateId; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::{ self as api_types, ExecutionOptimisticFinalizedResponse, ValidatorBalanceData, ValidatorData, - ValidatorId, ValidatorStatus, + ValidatorId, ValidatorIdentityData, ValidatorStatus, }; use std::{collections::HashSet, sync::Arc}; @@ -18,8 +18,18 @@ pub fn get_beacon_state_validators( |state, execution_optimistic, finalized| { let epoch = state.current_epoch(); let far_future_epoch = chain.spec.far_future_epoch; - let ids_filter_set: Option> = - query_ids.as_ref().map(HashSet::from_iter); + + // Map [] to None, indicating that no filtering should be applied (return all + // validators). + let ids_filter_set: Option> = query_ids + .as_ref() + .filter(|list| !list.is_empty()) + .map(HashSet::from_iter); + + let statuses_filter_set: Option> = query_statuses + .as_ref() + .filter(|list| !list.is_empty()) + .map(HashSet::from_iter); Ok(( state @@ -42,10 +52,11 @@ pub fn get_beacon_state_validators( far_future_epoch, ); - let status_matches = query_statuses.as_ref().is_none_or(|statuses| { - statuses.contains(&status) - || statuses.contains(&status.superstatus()) - }); + let status_matches = + statuses_filter_set.as_ref().is_none_or(|statuses| { + statuses.contains(&status) + || statuses.contains(&status.superstatus()) + }); if status_matches { Some(ValidatorData { @@ -119,3 +130,51 @@ pub fn get_beacon_state_validator_balances( finalized: Some(finalized), }) } + +pub fn get_beacon_state_validator_identities( + state_id: StateId, + chain: Arc>, + optional_ids: Option<&[ValidatorId]>, +) -> Result>, warp::Rejection> { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let ids_filter_set: Option> = match optional_ids { + // Same logic as validator_balances endpoint above + Some([]) => None, + Some(ids) => Some(HashSet::from_iter(ids.iter())), + None => None, + }; + + Ok(( + // From the BeaconState, extract the Validator data and convert it into ValidatorIdentityData type + state + .validators() + .iter() + .enumerate() + // filter by validator id(s) if provided + .filter(|(index, validator)| { + ids_filter_set.as_ref().is_none_or(|ids_set| { + ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) + || ids_set.contains(&ValidatorId::Index(*index as u64)) + }) + }) + .map(|(index, validator)| ValidatorIdentityData { + index: index as u64, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }) + .collect::>(), + execution_optimistic, + finalized, + )) + }, + )?; + + Ok(api_types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) +} diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index 361e8e78ea..371064c886 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -1,16 +1,14 @@ use crate::api_types::EndpointVersion; +use eth2::beacon_response::{ + BeaconResponse, ExecutionOptimisticFinalizedBeaconResponse, + ExecutionOptimisticFinalizedMetadata, ForkVersionedResponse, UnversionedResponse, +}; use eth2::{ CONSENSUS_BLOCK_VALUE_HEADER, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, EXECUTION_PAYLOAD_BLINDED_HEADER, EXECUTION_PAYLOAD_VALUE_HEADER, SSZ_CONTENT_TYPE_HEADER, }; use serde::Serialize; -use types::{ - beacon_response::{ - ExecutionOptimisticFinalizedBeaconResponse, ExecutionOptimisticFinalizedMetadata, - }, - BeaconResponse, ForkName, ForkVersionedResponse, InconsistentFork, Uint256, - UnversionedResponse, -}; +use types::{ForkName, InconsistentFork, Uint256}; use warp::reply::{self, Reply, Response}; pub const V1: EndpointVersion = EndpointVersion(1); diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index cd590580be..357b78cf41 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1,24 +1,23 @@ +use beacon_chain::custody_context::NodeCustodyType; +use beacon_chain::test_utils::test_spec; use beacon_chain::{ + GossipVerifiedBlock, IntoGossipVerifiedBlock, WhenSlotSkipped, test_utils::{AttestationStrategy, BlockStrategy}, - GossipVerifiedBlock, IntoGossipVerifiedBlock, }; -use eth2::reqwest::StatusCode; +use eth2::reqwest::{Response, StatusCode}; use eth2::types::{BroadcastValidation, PublishBlockRequest}; +use fixed_bytes::FixedBytesExtended; use http_api::test_utils::InteractiveTester; -use http_api::{publish_blinded_block, publish_block, reconstruct_block, Config, ProvenancedBlock}; +use http_api::{Config, ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block}; use std::collections::HashSet; use std::sync::Arc; -use types::{ - ColumnIndex, Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, MainnetEthSpec, Slot, -}; +use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, MainnetEthSpec, Slot}; use warp::Rejection; use warp_utils::reject::CustomBadRequest; type E = MainnetEthSpec; /* - * TODO(fulu): write PeerDAS equivalent tests for these. - * * We have the following test cases, which are duplicated for the blinded variant of the route: * * - `broadcast_validation=gossip` @@ -39,9 +38,6 @@ type E = MainnetEthSpec; * */ -// Default custody group count for tests -const CGC: usize = 8; - /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=gossip`. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn gossip_invalid() { @@ -78,7 +74,7 @@ pub async fn gossip_invalid() { }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; @@ -89,7 +85,18 @@ pub async fn gossip_invalid() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + let pre_finalized_block_root = Hash256::zero(); + let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the + // block. + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + + assert_server_message_error(error_response, expected_error_msg); } /// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. @@ -127,15 +134,11 @@ pub async fn gossip_partial_pass() { }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; - assert!(response.is_err()); - - let error_response = response.unwrap_err(); - - assert_eq!(error_response.status(), Some(StatusCode::ACCEPTED)); + assert_eq!(response.unwrap().status(), StatusCode::ACCEPTED); } // This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. @@ -168,7 +171,7 @@ pub async fn gossip_full_pass() { let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), @@ -177,10 +180,12 @@ pub async fn gossip_full_pass() { .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } // This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. @@ -217,16 +222,18 @@ pub async fn gossip_full_pass_ssz() { let (block_contents_tuple, _) = tester.harness.make_block(state_a, slot_b).await; let block_contents = block_contents_tuple.into(); - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&block_contents, validation_level) .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block_contents.signed_block().canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block_contents.signed_block().canonical_root()) + ); } /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`. @@ -264,7 +271,7 @@ pub async fn consensus_invalid() { }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; @@ -274,7 +281,19 @@ pub async fn consensus_invalid() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + + let pre_finalized_block_root = Hash256::zero(); + let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the + // block. + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + + assert_server_message_error(error_response, expected_error_msg); } /// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. @@ -304,13 +323,17 @@ pub async fn consensus_gossip() { let slot_a = Slot::new(num_initial); let slot_b = slot_a + 1; + let mut correct_state_root = Hash256::ZERO; let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester .harness - .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .make_block_with_modifier(state_a, slot_b, |b| { + *correct_state_root = *b.state_root(); + *b.state_root_mut() = Hash256::zero() + }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; @@ -320,7 +343,14 @@ pub async fn consensus_gossip() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()); + assert_server_message_error( + error_response, + format!( + "BAD_REQUEST: Invalid block: StateRootMismatch {{ block: {}, \ + local: {correct_state_root:?} }}", + Hash256::ZERO + ), + ); } /// This test checks that a block that is valid from both a gossip and consensus perspective, but nonetheless equivocates, is accepted when using `broadcast_validation=consensus`. @@ -367,14 +397,13 @@ pub async fn consensus_partial_pass_only_consensus() { ); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain, CGC); + let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain); assert!(gossip_block_b.is_ok()); - let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain, CGC); + let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain); assert!(gossip_block_a.is_err()); /* submit `block_b` which should induce equivocation */ let channel = tokio::sync::mpsc::unbounded_channel(); - let network_globals = tester.ctx.network_globals.clone().unwrap(); let publication_result = publish_block( None, @@ -383,15 +412,16 @@ pub async fn consensus_partial_pass_only_consensus() { &channel.0, validation_level, StatusCode::ACCEPTED, - network_globals, ) .await; assert!(publication_result.is_ok(), "{publication_result:?}"); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block_b_root)); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block_b_root) + ); } /// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. @@ -424,7 +454,7 @@ pub async fn consensus_full_pass() { let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), @@ -433,10 +463,12 @@ pub async fn consensus_full_pass() { .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus_and_equivocation`. @@ -476,7 +508,7 @@ pub async fn equivocation_invalid() { }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; @@ -486,7 +518,19 @@ pub async fn equivocation_invalid() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + + let pre_finalized_block_root = Hash256::zero(); + let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the + // block. + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + + assert_server_message_error(error_response, expected_error_msg); } /// This test checks that a block that is valid from both a gossip and consensus perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. @@ -534,21 +578,25 @@ pub async fn equivocation_consensus_early_equivocation() { assert_ne!(block_a.state_root(), block_b.state_root()); /* submit `block_a` as valid */ - assert!(tester - .client - .post_beacon_blocks_v2_ssz( - &PublishBlockRequest::new(block_a.clone(), blobs_a), - validation_level - ) - .await - .is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block_a.canonical_root())); + assert!( + tester + .client + .post_beacon_blocks_v2_ssz( + &PublishBlockRequest::new(block_a.clone(), blobs_a), + validation_level + ) + .await + .is_ok() + ); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block_a.canonical_root()) + ); /* submit `block_b` which should induce equivocation */ - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block_b.clone(), blobs_b), @@ -574,7 +622,8 @@ pub async fn equivocation_gossip() { // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let tester = InteractiveTester::::new(None, validator_count).await; + let spec = test_spec::(); + let tester = InteractiveTester::::new(Some(spec), validator_count).await; // Create some chain depth. tester.harness.advance_slot(); @@ -590,14 +639,18 @@ pub async fn equivocation_gossip() { let slot_a = Slot::new(num_initial); let slot_b = slot_a + 1; + let mut correct_state_root = Hash256::zero(); let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester .harness - .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .make_block_with_modifier(state_a, slot_b, |b| { + *correct_state_root = *b.state_root(); + *b.state_root_mut() = Hash256::zero() + }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; @@ -607,7 +660,13 @@ pub async fn equivocation_gossip() { /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()); + assert_server_message_error( + error_response, + format!( + "BAD_REQUEST: Invalid block: StateRootMismatch {{ block: {}, local: {correct_state_root} }}", + Hash256::zero() + ), + ); } /// This test checks that a block that is valid from both a gossip and consensus perspective but @@ -657,14 +716,13 @@ pub async fn equivocation_consensus_late_equivocation() { ); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain, CGC); + let gossip_block_b = block_b.into_gossip_verified_block(&tester.harness.chain); assert!(gossip_block_b.is_ok()); - let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain, CGC); + let gossip_block_a = block_a.into_gossip_verified_block(&tester.harness.chain); assert!(gossip_block_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); - let network_globals = tester.ctx.network_globals.clone().unwrap(); let publication_result = publish_block( None, @@ -673,7 +731,6 @@ pub async fn equivocation_consensus_late_equivocation() { &channel.0, validation_level, StatusCode::ACCEPTED, - network_globals, ) .await; @@ -720,7 +777,7 @@ pub async fn equivocation_full_pass() { let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), @@ -729,10 +786,12 @@ pub async fn equivocation_full_pass() { .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=gossip`. @@ -763,28 +822,50 @@ pub async fn blinded_gossip_invalid() { tester.harness.advance_slot(); - let (block_contents_tuple, _) = tester + // Ensure there's at least one blob in the block, so we don't run into failures when the + // block generator logic changes, as different errors could be returned: + // * Invalidity of blocks: `NotFinalizedDescendant` + // * Invalidity of blobs: `ParentUnknown` + tester .harness - .make_block_with_modifier(chain_state_before, slot, |b| { + .execution_block_generator() + .set_min_blob_count(1); + let (blinded_block, _) = tester + .harness + .make_blinded_block_with_modifier(chain_state_before, slot, |b| { *b.state_root_mut() = Hash256::zero(); *b.parent_root_mut() = Hash256::zero(); }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); - - /* mandated by Beacon API spec */ assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + + let pre_finalized_block_root = Hash256::zero(); + let expected_error_msg = if tester.harness.spec.is_fulu_scheduled() { + format!( + "BAD_REQUEST: NotFinalizedDescendant {{ block_parent_root: {pre_finalized_block_root:?} }}" + ) + } else { + // Since Deneb, the invalidity of the blobs will be detected prior to the invalidity of the + // block. + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}") + }; + + assert_server_message_error(error_response, expected_error_msg); } -/// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. +/// Process a blinded block that is invalid, but valid on gossip. +/// +/// Due to the checks conducted by the "relay" (mock-builder) when `broadcast_to_bn` is set (post +/// Fulu), we can't always assert that we get a 202 status for this block -- post Fulu the relay +/// detects it as invalid and the BN returns an error. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn blinded_gossip_partial_pass() { /* this test targets gossip-level validation */ @@ -812,22 +893,27 @@ pub async fn blinded_gossip_partial_pass() { tester.harness.advance_slot(); - let (block_contents_tuple, _) = tester + let (blinded_block, _) = tester .harness - .make_block_with_modifier(chain_state_before, slot, |b| { + .make_blinded_block_with_modifier(chain_state_before, slot, |b| { *b.state_root_mut() = Hash256::zero() }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; - assert!(response.is_err()); - - let error_response = response.unwrap_err(); - - assert_eq!(error_response.status(), Some(StatusCode::ACCEPTED)); + if tester.harness.spec.is_fulu_scheduled() { + let error_response = response.unwrap_err(); + // XXX: this should be a 400 but is a 500 due to the mock-builder being janky + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + } else { + assert_eq!(response.unwrap().status(), StatusCode::ACCEPTED); + } } // This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. @@ -859,16 +945,19 @@ pub async fn blinded_gossip_full_pass() { let state_a = tester.harness.get_current_state(); let (blinded_block, _) = tester.harness.make_blinded_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&blinded_block.canonical_root())); + assert_eq!(response.unwrap().status(), StatusCode::OK); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&blinded_block.canonical_root()) + ); } // This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. @@ -901,16 +990,19 @@ pub async fn blinded_gossip_full_pass_ssz() { let state_a = tester.harness.get_current_state(); let (blinded_block, _) = tester.harness.make_blinded_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blinded_blocks_v2_ssz(&blinded_block, validation_level) .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&blinded_block.canonical_root())); + assert_eq!(response.unwrap().status(), StatusCode::OK); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&blinded_block.canonical_root()) + ); } /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus`. @@ -922,7 +1014,7 @@ pub async fn blinded_consensus_invalid() { // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; - let num_initial: u64 = 31; + let num_initial: u64 = 256; let tester = InteractiveTester::::new(None, validator_count).await; // Create some chain depth. @@ -941,25 +1033,48 @@ pub async fn blinded_consensus_invalid() { tester.harness.advance_slot(); - let (block_contents_tuple, _) = tester + let finalized_slot = chain_state_before + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + assert_ne!(finalized_slot, 0); + let pre_finalized_block_root = tester .harness - .make_block_with_modifier(chain_state_before, slot, |b| { + .chain + .block_root_at_slot(finalized_slot - 1, WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + + let (blinded_block, _) = tester + .harness + .make_blinded_block_with_modifier(chain_state_before, slot, |b| { *b.state_root_mut() = Hash256::zero(); - *b.parent_root_mut() = Hash256::zero(); + *b.parent_root_mut() = pre_finalized_block_root; }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); /* mandated by Beacon API spec */ - assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + if tester.harness.spec.is_fulu_scheduled() { + // XXX: this should be a 400 but is a 500 due to the mock-builder being janky + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + } else { + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + assert_server_message_error( + error_response, + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), + ); + } } /// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. @@ -989,23 +1104,44 @@ pub async fn blinded_consensus_gossip() { let slot_a = Slot::new(num_initial); let slot_b = slot_a + 1; + let mut correct_state_root = Hash256::zero(); + let state_a = tester.harness.get_current_state(); - let (block_contents_tuple, _) = tester + let (blinded_block, _) = tester .harness - .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .make_blinded_block_with_modifier(state_a, slot_b, |b| { + *correct_state_root = *b.state_root(); + *b.state_root_mut() = Hash256::zero() + }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; + assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); /* mandated by Beacon API spec */ - assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()); + if tester.harness.spec.is_fulu_scheduled() { + // XXX: this should be a 400 but is a 500 due to the mock-builder being janky + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + } else { + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + assert_server_message_error( + error_response, + format!( + "BAD_REQUEST: Invalid block: StateRootMismatch {{ block: {}, \ + local: {correct_state_root} }}", + Hash256::ZERO + ), + ); + } } /// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. @@ -1038,16 +1174,18 @@ pub async fn blinded_consensus_full_pass() { let state_a = tester.harness.get_current_state(); let (blinded_block, _) = tester.harness.make_blinded_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&blinded_block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&blinded_block.canonical_root()) + ); } /// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus_and_equivocation`. @@ -1060,7 +1198,7 @@ pub async fn blinded_equivocation_invalid() { // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; - let num_initial: u64 = 31; + let num_initial: u64 = 256; let tester = InteractiveTester::::new(None, validator_count).await; // Create some chain depth. @@ -1079,25 +1217,47 @@ pub async fn blinded_equivocation_invalid() { tester.harness.advance_slot(); - let (block_contents_tuple, _) = tester + let finalized_slot = chain_state_before + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); + assert_ne!(finalized_slot, 0); + let pre_finalized_block_root = tester .harness - .make_block_with_modifier(chain_state_before, slot, |b| { + .chain + .block_root_at_slot(finalized_slot - 1, WhenSlotSkipped::Prev) + .unwrap() + .unwrap(); + + let (blinded_block, _) = tester + .harness + .make_blinded_block_with_modifier(chain_state_before, slot, |b| { *b.state_root_mut() = Hash256::zero(); - *b.parent_root_mut() = Hash256::zero(); + *b.parent_root_mut() = pre_finalized_block_root; }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); /* mandated by Beacon API spec */ - assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()); + if tester.harness.spec.is_fulu_scheduled() { + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + } else { + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + assert_server_message_error( + error_response, + format!("BAD_REQUEST: ParentUnknown {{ parent_root: {pre_finalized_block_root:?} }}"), + ); + } } /// This test checks that a block that is valid from both a gossip and consensus perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. @@ -1147,18 +1307,20 @@ pub async fn blinded_equivocation_consensus_early_equivocation() { assert_ne!(block_a.state_root(), block_b.state_root()); /* submit `block_a` as valid */ - assert!(tester + tester .client .post_beacon_blinded_blocks_v2(&block_a, validation_level) .await - .is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block_a.canonical_root())); + .unwrap(); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block_a.canonical_root()) + ); /* submit `block_b` which should induce equivocation */ - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blinded_blocks_v2(&block_b, validation_level) .await; @@ -1166,8 +1328,15 @@ pub async fn blinded_equivocation_consensus_early_equivocation() { let error_response: eth2::Error = response.err().unwrap(); - assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - assert_server_message_error(error_response, "BAD_REQUEST: Slashable".to_string()); + if tester.harness.spec.is_fulu_scheduled() { + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR) + ); + } else { + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + assert_server_message_error(error_response, "BAD_REQUEST: Slashable".to_string()); + } } /// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus_and_equivocation`. @@ -1198,24 +1367,42 @@ pub async fn blinded_equivocation_gossip() { let slot_a = Slot::new(num_initial); let slot_b = slot_a + 1; + let mut correct_state_root = Hash256::zero(); let state_a = tester.harness.get_current_state(); - let (block_contents_tuple, _) = tester + let (blinded_block, _) = tester .harness - .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .make_blinded_block_with_modifier(state_a, slot_b, |b| { + *correct_state_root = *b.state_root(); + *b.state_root_mut() = Hash256::zero() + }) .await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client - .post_beacon_blinded_blocks_v2(&block_contents_tuple.0.clone_as_blinded(), validation_level) + .post_beacon_blinded_blocks_v2(&blinded_block, validation_level) .await; - assert!(response.is_err()); + assert!(response.is_err()); let error_response: eth2::Error = response.err().unwrap(); /* mandated by Beacon API spec */ - assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); - - assert_server_message_error(error_response, "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()); + if tester.harness.spec.is_fulu_scheduled() { + // XXX: this should be a 400 but is a 500 due to the mock-builder being janky + assert_eq!( + error_response.status(), + Some(StatusCode::INTERNAL_SERVER_ERROR), + "{error_response:?}" + ); + } else { + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + assert_server_message_error( + error_response, + format!( + "BAD_REQUEST: Invalid block: StateRootMismatch {{ block: {}, local: {correct_state_root} }}", + Hash256::zero() + ), + ); + } } /// This test checks that a block that is valid from both a gossip and @@ -1270,53 +1457,58 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { ); assert_ne!(block_a.state_root(), block_b.state_root()); - let unblinded_block_a = reconstruct_block( - tester.harness.chain.clone(), - block_a.canonical_root(), - Arc::new(block_a), - ) - .await - .unwrap(); - let unblinded_block_b = reconstruct_block( - tester.harness.chain.clone(), - block_b.canonical_root(), - block_b.clone(), - ) - .await - .unwrap(); + // From fulu builders never send back a full payload, hence further checks in this test + // are not possible + if !tester.harness.spec.is_fulu_scheduled() { + let unblinded_block_a = reconstruct_block( + tester.harness.chain.clone(), + block_a.canonical_root(), + Arc::new(block_a), + ) + .await + .expect("failed to reconstruct block") + .expect("block expected"); - let inner_block_a = match unblinded_block_a { - ProvenancedBlock::Local(a, _, _) => a, - ProvenancedBlock::Builder(a, _, _) => a, - }; - let inner_block_b = match unblinded_block_b { - ProvenancedBlock::Local(b, _, _) => b, - ProvenancedBlock::Builder(b, _, _) => b, - }; + let unblinded_block_b = reconstruct_block( + tester.harness.chain.clone(), + block_b.canonical_root(), + block_b.clone(), + ) + .await + .expect("failed to reconstruct block") + .expect("block expected"); - let gossip_block_b = GossipVerifiedBlock::new(inner_block_b, &tester.harness.chain, CGC); - assert!(gossip_block_b.is_ok()); - let gossip_block_a = GossipVerifiedBlock::new(inner_block_a, &tester.harness.chain, CGC); - assert!(gossip_block_a.is_err()); + let inner_block_a = match unblinded_block_a { + ProvenancedBlock::Local(a, _, _) => a, + ProvenancedBlock::Builder(a, _, _) => a, + }; + let inner_block_b = match unblinded_block_b { + ProvenancedBlock::Local(b, _, _) => b, + ProvenancedBlock::Builder(b, _, _) => b, + }; - let channel = tokio::sync::mpsc::unbounded_channel(); - let network_globals = tester.ctx.network_globals.clone().unwrap(); + let gossip_block_b = GossipVerifiedBlock::new(inner_block_b, &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(inner_block_a, &tester.harness.chain); + assert!(gossip_block_a.is_err()); - let publication_result = publish_blinded_block( - block_b, - tester.harness.chain, - &channel.0, - validation_level, - StatusCode::ACCEPTED, - network_globals, - ) - .await; + let channel = tokio::sync::mpsc::unbounded_channel(); - assert!(publication_result.is_err()); + let publication_result = publish_blinded_block( + block_b, + tester.harness.chain, + &channel.0, + validation_level, + StatusCode::ACCEPTED, + ) + .await; - let publication_error: Rejection = publication_result.unwrap_err(); + assert!(publication_result.is_err()); - assert!(publication_error.find::().is_some()); + let publication_error: Rejection = publication_result.unwrap_err(); + + assert!(publication_error.find::().is_some()); + } } /// This test checks that a block that is valid from both a gossip and consensus perspective (and does not equivocate) is accepted when using `broadcast_validation=consensus_and_equivocation`. @@ -1350,30 +1542,36 @@ pub async fn blinded_equivocation_full_pass() { let state_a = tester.harness.get_current_state(); let (block, _) = tester.harness.make_blinded_block(state_a, slot_b).await; - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blinded_blocks_v2(&block, validation_level) .await; assert!(response.is_ok()); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } -/// This test checks that an HTTP POST request with the block & blobs succeeds with a 200 response -/// even if the block has already been seen on gossip without any blobs. +/// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response +/// even if the block has already been seen on gossip without any blobs/columns. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn block_seen_on_gossip_without_blobs() { +pub async fn block_seen_on_gossip_without_blobs_or_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); - let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let tester = InteractiveTester::::new(None, validator_count).await; + let state = tester.harness.get_current_state(); + let fork_name = state.fork_name(&tester.harness.spec).unwrap(); + if !fork_name.deneb_enabled() { + return; + } // Create some chain depth. tester.harness.advance_slot(); @@ -1398,17 +1596,19 @@ pub async fn block_seen_on_gossip_without_blobs() { // Simulate the block being seen on gossip. block .clone() - .into_gossip_verified_block(&tester.harness.chain, CGC) + .into_gossip_verified_block(&tester.harness.chain) .unwrap(); // It should not yet be added to fork choice because blobs have not been seen. - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); // Post the block *and* blobs to the HTTP API. - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some(blobs)), @@ -1418,24 +1618,30 @@ pub async fn block_seen_on_gossip_without_blobs() { // This should result in the block being fully imported. response.unwrap(); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } -/// This test checks that an HTTP POST request with the block & blobs succeeds with a 200 response -/// even if the block has already been seen on gossip without all blobs. +/// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response +/// even if the block has already been seen on gossip without all blobs/columns. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn block_seen_on_gossip_with_some_blobs() { +pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); - let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let tester = InteractiveTester::::new(None, validator_count).await; + let state = tester.harness.get_current_state(); + let fork_name = state.fork_name(&tester.harness.spec).unwrap(); + if !fork_name.deneb_enabled() { + return; + } // Create some chain depth. tester.harness.advance_slot(); @@ -1448,6 +1654,10 @@ pub async fn block_seen_on_gossip_with_some_blobs() { ) .await; tester.harness.advance_slot(); + tester + .harness + .execution_block_generator() + .set_min_blob_count(2); let slot_a = Slot::new(num_initial); let slot_b = slot_a + 1; @@ -1467,7 +1677,7 @@ pub async fn block_seen_on_gossip_with_some_blobs() { // Simulate the block being seen on gossip. block .clone() - .into_gossip_verified_block(&tester.harness.chain, CGC) + .into_gossip_verified_block(&tester.harness.chain) .unwrap(); // Simulate some of the blobs being seen on gossip. @@ -1477,18 +1687,20 @@ pub async fn block_seen_on_gossip_with_some_blobs() { &block, partial_blobs.iter(), partial_kzg_proofs.iter(), - Some(get_custody_columns(&tester)), + Some(get_custody_columns(&tester, block.slot())), ) .await; // It should not yet be added to fork choice because all blobs have not been seen. - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); // Post the block *and* all blobs to the HTTP API. - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some(blobs)), @@ -1498,24 +1710,31 @@ pub async fn block_seen_on_gossip_with_some_blobs() { // This should result in the block being fully imported. response.unwrap(); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } -/// This test checks that an HTTP POST request with the block & blobs succeeds with a 200 response -/// even if the blobs have already been seen on gossip. +/// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response +/// even if the blobs/columns have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn blobs_seen_on_gossip_without_block() { +pub async fn blobs_or_columns_seen_on_gossip_without_block() { + let spec = test_spec::(); let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); - let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count).await; + let state = tester.harness.get_current_state(); + let fork_name = state.fork_name(&tester.harness.spec).unwrap(); + if !fork_name.deneb_enabled() { + return; + } // Create some chain depth. tester.harness.advance_slot(); @@ -1543,18 +1762,20 @@ pub async fn blobs_seen_on_gossip_without_block() { &block, blobs.iter(), kzg_proofs.iter(), - Some(get_custody_columns(&tester)), + Some(get_custody_columns(&tester, block.slot())), ) .await; // It should not yet be added to fork choice because the block has not been seen. - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); // Post the block *and* all blobs to the HTTP API. - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some((kzg_proofs, blobs))), @@ -1564,24 +1785,30 @@ pub async fn blobs_seen_on_gossip_without_block() { // This should result in the block being fully imported. response.unwrap(); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } /// This test checks that an HTTP POST request with the block succeeds with a 200 response /// if just the blobs have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn blobs_seen_on_gossip_without_block_and_no_http_blobs() { +async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); - let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let tester = InteractiveTester::::new(None, validator_count).await; + let state = tester.harness.get_current_state(); + let fork_name = state.fork_name(&tester.harness.spec).unwrap(); + if !fork_name.deneb_enabled() { + return; + } // Create some chain depth. tester.harness.advance_slot(); @@ -1610,18 +1837,20 @@ pub async fn blobs_seen_on_gossip_without_block_and_no_http_blobs() { &block, blobs.iter(), kzg_proofs.iter(), - Some(get_custody_columns(&tester)), + Some(get_custody_columns(&tester, block.slot())), ) .await; // It should not yet be added to fork choice because the block has not been seen. - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); // Post just the block to the HTTP API (blob lists are empty). - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new( @@ -1634,14 +1863,16 @@ pub async fn blobs_seen_on_gossip_without_block_and_no_http_blobs() { // This should result in the block being fully imported. response.unwrap(); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn slashable_blobs_seen_on_gossip_cause_failure() { +async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let validation_level: Option = Some(BroadcastValidation::ConsensusAndEquivocation); @@ -1649,8 +1880,12 @@ pub async fn slashable_blobs_seen_on_gossip_cause_failure() { // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); - let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let tester = InteractiveTester::::new(None, validator_count).await; + let state = tester.harness.get_current_state(); + let fork_name = state.fork_name(&tester.harness.spec).unwrap(); + if !fork_name.deneb_enabled() { + return; + } // Create some chain depth. tester.harness.advance_slot(); @@ -1680,18 +1915,20 @@ pub async fn slashable_blobs_seen_on_gossip_cause_failure() { &block_b, blobs_b.iter(), kzg_proofs_b.iter(), - Some(get_custody_columns(&tester)), + Some(get_custody_columns(&tester, block_b.slot())), ) .await; // It should not yet be added to fork choice because block B has not been seen. - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block_b.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block_b.canonical_root()) + ); // Post block A *and* all its blobs to the HTTP API. - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block_a.clone(), Some((kzg_proofs_a, blobs_a))), @@ -1701,10 +1938,12 @@ pub async fn slashable_blobs_seen_on_gossip_cause_failure() { // This should not result in block A being fully imported. response.unwrap_err(); - assert!(!tester - .harness - .chain - .block_is_known_to_fork_choice(&block_a.canonical_root())); + assert!( + !tester + .harness + .chain + .block_is_known_to_fork_choice(&block_a.canonical_root()) + ); } /// This test checks that an HTTP POST request with a duplicate block & blobs results in the @@ -1717,10 +1956,9 @@ pub async fn duplicate_block_status_code() { // `validator_count // 32`. let validator_count = 64; let num_initial: u64 = 31; - let spec = ForkName::latest_stable().make_genesis_spec(E::default_spec()); let duplicate_block_status_code = StatusCode::IM_A_TEAPOT; let tester = InteractiveTester::::new_with_initializer_and_mutator( - Some(spec), + None, validator_count, None, None, @@ -1728,6 +1966,8 @@ pub async fn duplicate_block_status_code() { duplicate_block_status_code, ..Config::default() }, + true, + NodeCustodyType::Fullnode, ) .await; @@ -1752,20 +1992,22 @@ pub async fn duplicate_block_status_code() { // Post the block blobs to the HTTP API once. let block_request = PublishBlockRequest::new(block.clone(), Some((kzg_proofs, blobs))); - let response: Result<(), eth2::Error> = tester + let response: Result = tester .client .post_beacon_blocks_v2_ssz(&block_request, validation_level) .await; // This should result in the block being fully imported. response.unwrap(); - assert!(tester - .harness - .chain - .block_is_known_to_fork_choice(&block.canonical_root())); + assert!( + tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root()) + ); // Post again. - let duplicate_response: Result<(), eth2::Error> = tester + let duplicate_response: Result = tester .client .post_beacon_blocks_v2_ssz(&block_request, validation_level) .await; @@ -1780,12 +2022,15 @@ fn assert_server_message_error(error_response: eth2::Error, expected_message: St assert_eq!(err.message, expected_message); } -fn get_custody_columns(tester: &InteractiveTester) -> HashSet { +fn get_custody_columns(tester: &InteractiveTester, slot: Slot) -> HashSet { + let epoch = slot.epoch(E::slots_per_epoch()); tester .ctx - .network_globals + .chain .as_ref() .unwrap() - .sampling_columns - .clone() + .sampling_columns_for_epoch(epoch) + .iter() + .copied() + .collect() } diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 10e1d01536..b96c8bd112 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -1,16 +1,19 @@ //! Tests for API behaviour across fork boundaries. +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - test_utils::{RelativeSyncCommittee, DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME}, StateSkipConfig, + test_utils::{DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME, RelativeSyncCommittee}, }; +use bls::PublicKey; use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee}; use execution_layer::test_utils::generate_genesis_header; -use genesis::{bls_withdrawal_credentials, InteropGenesisBuilder}; +use fixed_bytes::FixedBytesExtended; +use genesis::{InteropGenesisBuilder, bls_withdrawal_credentials}; use http_api::test_utils::*; use std::collections::HashSet; use types::{ + Address, ChainSpec, Epoch, EthSpec, Hash256, MinimalEthSpec, Slot, test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs}, - Address, ChainSpec, Epoch, EthSpec, FixedBytesExtended, Hash256, MinimalEthSpec, Slot, }; type E = MinimalEthSpec; @@ -149,10 +152,41 @@ async fn attestations_across_fork_with_skip_slots() { .flat_map(|(atts, _)| atts.iter().map(|(att, _)| att.clone())) .collect::>(); + let unaggregated_attestations = unaggregated_attestations + .into_iter() + .map(|attn| { + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = fork_state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + attn.to_single_attestation_with_attester_index(attester_index as u64) + .unwrap() + }) + .collect::>(); + assert!(!unaggregated_attestations.is_empty()); let fork_name = harness.spec.fork_name_at_slot::(fork_slot); client - .post_beacon_pool_attestations_v1(&unaggregated_attestations) + .post_beacon_pool_attestations_v2::(unaggregated_attestations, fork_name) .await .unwrap(); @@ -360,7 +394,7 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { fn withdrawal_credentials_fn<'a>( index: usize, - _: &'a types::PublicKey, + _: &'a PublicKey, spec: &'a ChainSpec, ) -> Hash256 { // It is a bit inefficient to regenerate the whole keypair here, but this is a workaround. @@ -394,6 +428,8 @@ async fn bls_to_execution_changes_update_all_around_capella_fork() { })), None, Default::default(), + true, + NodeCustodyType::Fullnode, ) .await; let harness = &tester.harness; diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 9f6935461b..b04c812773 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -1,14 +1,17 @@ //! Generic tests that make use of the (newer) `InteractiveApiTester` +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, - test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, ChainConfig, + chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, + test_utils::{ + AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy, test_spec, + }, }; -use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage; -use either::Either; +use beacon_processor::{Work, WorkEvent, work_reprocessing_queue::ReprocessQueueMessage}; use eth2::types::ProduceBlockV3Response; use eth2::types::{DepositContractData, StateId}; use execution_layer::{ForkchoiceState, PayloadAttributes}; +use fixed_bytes::FixedBytesExtended; use http_api::test_utils::InteractiveTester; use parking_lot::Mutex; use slot_clock::SlotClock; @@ -19,8 +22,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use types::{ - Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, FixedBytesExtended, ForkName, - Hash256, MainnetEthSpec, MinimalEthSpec, ProposerPreparationData, Slot, Uint256, + Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, + MinimalEthSpec, ProposerPreparationData, Slot, Uint256, }; type E = MainnetEthSpec; @@ -58,7 +61,10 @@ async fn state_by_root_pruned_from_fork_choice() { type E = MinimalEthSpec; let validator_count = 24; - let spec = ForkName::Eip7805.make_genesis_spec(E::default_spec()); + // TODO(EIP-7732): extend test for Gloas by reverting back to using `ForkName::latest()` + // Issue is that this test does block production via `extend_chain_with_sync` which expects to be able to use `state.latest_execution_payload_header` during block production, but Gloas uses `latest_execution_bid` instead + // This will be resolved in a subsequent block processing PR + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); let tester = InteractiveTester::::new_with_initializer_and_mutator( Some(spec.clone()), @@ -74,6 +80,8 @@ async fn state_by_root_pruned_from_fork_choice() { })), None, Default::default(), + false, + NodeCustodyType::Fullnode, ) .await; @@ -396,7 +404,10 @@ pub async fn proposer_boost_re_org_test( assert!(head_slot > 0); // Test using the latest fork so that we simulate conditions as similar to mainnet as possible. - let mut spec = ForkName::Eip7805.make_genesis_spec(E::default_spec()); + // TODO(EIP-7732): extend test for Gloas by reverting back to using `ForkName::latest()` + // Issue is that `get_validator_blocks_v3` below expects to be able to use `state.latest_execution_payload_header` during `produce_block_on_state` -> `produce_partial_beacon_block` -> `get_execution_payload`, but gloas will no longer support this state field + // This will be resolved in a subsequent block processing PR + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); spec.terminal_total_difficulty = Uint256::from(1); // Ensure there are enough validators to have `attesters_per_slot`. @@ -430,6 +441,8 @@ pub async fn proposer_boost_re_org_test( ) })), Default::default(), + false, + NodeCustodyType::Fullnode, ) .await; let harness = &tester.harness; @@ -539,7 +552,7 @@ pub async fn proposer_boost_re_org_test( slot_a, num_parent_votes, ); - harness.process_attestations(block_a_parent_votes); + harness.process_attestations(block_a_parent_votes, &state_a); // Attest to block A during slot B. for _ in 0..parent_distance { @@ -553,7 +566,7 @@ pub async fn proposer_boost_re_org_test( slot_b, num_empty_votes, ); - harness.process_attestations(block_a_empty_votes); + harness.process_attestations(block_a_empty_votes, &state_a); let remaining_attesters = all_validators .iter() @@ -586,7 +599,7 @@ pub async fn proposer_boost_re_org_test( slot_b, num_head_votes, ); - harness.process_attestations(block_b_head_votes); + harness.process_attestations(block_b_head_votes, &state_b); let payload_lookahead = harness.chain.config.prepare_payload_lookahead; let fork_choice_lookahead = Duration::from_millis(500); @@ -633,7 +646,7 @@ pub async fn proposer_boost_re_org_test( .into(); let (unsigned_block_type, _) = tester .client - .get_validator_blocks_v3::(slot_c, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot_c, &randao_reveal, None, None, None) .await .unwrap(); @@ -667,6 +680,7 @@ pub async fn proposer_boost_re_org_test( // Check the fork choice updates that were sent. let forkchoice_updates = forkchoice_updates.lock(); + let block_a_exec_hash = block_a .0 .message() @@ -818,10 +832,10 @@ pub async fn fork_choice_before_proposal() { block_root_c, slot_c, ); - harness.process_attestations(attestations_c); + harness.process_attestations(attestations_c, &state_c); // Apply the attestations to B, but don't re-run fork choice. - harness.process_attestations(attestations_b); + harness.process_attestations(attestations_b, &state_b); // Due to proposer boost, the head should be C during slot C. assert_eq!( @@ -894,7 +908,7 @@ async fn queue_attestations_from_http() { let fork_name = tester.harness.spec.fork_name_at_slot::(attestation_slot); // Make attestations to the block and POST them to the beacon node on a background thread. - let attestation_future = if fork_name.electra_enabled() { + let attestation_future = { let single_attestations = harness .make_single_attestations( &all_validators, @@ -907,30 +921,9 @@ async fn queue_attestations_from_http() { .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) .collect::>(); - let attestations = Either::Right(single_attestations); - tokio::spawn(async move { client - .post_beacon_pool_attestations_v2::(attestations, fork_name) - .await - .expect("attestations should be processed successfully") - }) - } else { - let attestations = harness - .make_unaggregated_attestations( - &all_validators, - &post_state, - block.0.state_root(), - block_root.into(), - attestation_slot, - ) - .into_iter() - .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) - .collect::>(); - - tokio::spawn(async move { - client - .post_beacon_pool_attestations_v1(&attestations) + .post_beacon_pool_attestations_v2::(single_attestations, fork_name) .await .expect("attestations should be processed successfully") }) @@ -945,15 +938,259 @@ async fn queue_attestations_from_http() { .unwrap(); tester .ctx - .beacon_processor_reprocess_send + .beacon_processor_send .as_ref() .unwrap() - .send(ReprocessQueueMessage::BlockImported { - block_root, - parent_root, + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(ReprocessQueueMessage::BlockImported { + block_root, + parent_root, + }), }) - .await .unwrap(); attestation_future.await.unwrap(); } + +// Test that a request for next epoch proposer duties suceeds when the current slot clock is within +// gossip clock disparity (500ms) of the new epoch. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn proposer_duties_with_gossip_tolerance() { + let validator_count = 24; + + let tester = InteractiveTester::::new(None, validator_count).await; + let harness = &tester.harness; + let spec = &harness.spec; + let client = &tester.client; + + let num_initial = 4 * E::slots_per_epoch() - 1; + let next_epoch_start_slot = Slot::new(num_initial + 1); + + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await; + + assert_eq!(harness.chain.slot().unwrap(), num_initial); + + // Set the clock to just before the next epoch. + harness.chain.slot_clock.advance_time( + Duration::from_secs(spec.seconds_per_slot) - spec.maximum_gossip_clock_disparity(), + ); + assert_eq!( + harness + .chain + .slot_clock + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) + .unwrap(), + next_epoch_start_slot + ); + + let head_state = harness.get_current_state(); + let head_block_root = harness.head_block_root(); + let tolerant_current_epoch = next_epoch_start_slot.epoch(E::slots_per_epoch()); + + // This is a regression test for the bug described here: + // https://github.com/sigp/lighthouse/pull/8130/files#r2386594566 + // + // To trigger it, we need to prime the proposer shuffling cache with an incorrect entry which + // the previous code would be liable to lookup due to the bugs in its decision root calculation. + let wrong_decision_root = head_state + .proposer_shuffling_decision_root(head_block_root, spec) + .unwrap(); + let wrong_proposer_indices = vec![0; E::slots_per_epoch() as usize]; + harness + .chain + .beacon_proposer_cache + .lock() + .insert( + tolerant_current_epoch, + wrong_decision_root, + wrong_proposer_indices.clone(), + head_state.fork(), + ) + .unwrap(); + + // Request the proposer duties. + let proposer_duties_tolerant_current_epoch = client + .get_validator_duties_proposer(tolerant_current_epoch) + .await + .unwrap(); + + assert_eq!( + proposer_duties_tolerant_current_epoch.dependent_root, + head_state + .legacy_proposer_shuffling_decision_root_at_epoch( + tolerant_current_epoch, + head_block_root, + ) + .unwrap() + ); + assert_ne!( + proposer_duties_tolerant_current_epoch + .data + .iter() + .map(|data| data.validator_index as usize) + .collect::>(), + wrong_proposer_indices, + ); + + // We should get the exact same result after properly advancing into the epoch. + harness + .chain + .slot_clock + .advance_time(spec.maximum_gossip_clock_disparity()); + assert_eq!(harness.chain.slot().unwrap(), next_epoch_start_slot); + let proposer_duties_current_epoch = client + .get_validator_duties_proposer(tolerant_current_epoch) + .await + .unwrap(); + + assert_eq!( + proposer_duties_tolerant_current_epoch, + proposer_duties_current_epoch + ); +} + +// Test that a request to `lighthouse/custody/backfill` succeeds by verifying that `CustodyContext` and `DataColumnCustodyInfo` +// have been updated with the correct values. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn lighthouse_restart_custody_backfill() { + let spec = test_spec::(); + + // Skip pre-Fulu. + if !spec.is_fulu_scheduled() { + return; + } + + let validator_count = 24; + + let tester = InteractiveTester::::new_supernode(Some(spec), validator_count).await; + let harness = &tester.harness; + let spec = &harness.spec; + let client = &tester.client; + let min_cgc = spec.custody_requirement; + let max_cgc = spec.number_of_custody_groups; + + let num_blocks = 2 * E::slots_per_epoch(); + + let custody_context = harness.chain.data_availability_checker.custody_context(); + + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await; + + let cgc_at_head = custody_context.custody_group_count_at_head(spec); + let earliest_data_column_epoch = harness.chain.earliest_custodied_data_column_epoch(); + + assert_eq!(cgc_at_head, max_cgc); + assert_eq!(earliest_data_column_epoch, None); + + custody_context + .update_and_backfill_custody_count_at_epoch(harness.chain.epoch().unwrap(), cgc_at_head); + client.post_lighthouse_custody_backfill().await.unwrap(); + + let cgc_at_head = custody_context.custody_group_count_at_head(spec); + let cgc_at_previous_epoch = + custody_context.custody_group_count_at_epoch(harness.chain.epoch().unwrap() - 1, spec); + let earliest_data_column_epoch = harness.chain.earliest_custodied_data_column_epoch(); + + // `DataColumnCustodyInfo` should have been updated to the head epoch + assert_eq!( + earliest_data_column_epoch, + Some(harness.chain.epoch().unwrap() + 1) + ); + // Cgc requirements should have stayed the same at head + assert_eq!(cgc_at_head, max_cgc); + // Cgc requirements at the previous epoch should be `min_cgc` + // This allows for custody backfill to re-fetch columns for this epoch. + assert_eq!(cgc_at_previous_epoch, min_cgc); +} + +// Test that a request for next epoch proposer duties suceeds when the current slot clock is within +// gossip clock disparity (500ms) of the new epoch. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn lighthouse_custody_info() { + let mut spec = test_spec::(); + + // Skip pre-Fulu. + if !spec.is_fulu_scheduled() { + return; + } + + // Use a short DA expiry period so we can observe non-zero values for the oldest data column + // slot. + spec.min_epochs_for_blob_sidecars_requests = 2; + spec.min_epochs_for_data_column_sidecars_requests = 2; + + let validator_count = 24; + + let tester = InteractiveTester::::new(Some(spec), validator_count).await; + let harness = &tester.harness; + let spec = &harness.spec; + let client = &tester.client; + + let num_initial = 2 * E::slots_per_epoch(); + let num_secondary = 2 * E::slots_per_epoch(); + + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await; + + assert_eq!(harness.chain.slot().unwrap(), num_initial); + + let info = client.get_lighthouse_custody_info().await.unwrap(); + assert_eq!(info.earliest_custodied_data_column_slot, 0); + assert_eq!(info.custody_group_count, spec.custody_requirement); + assert_eq!( + info.custody_columns.len(), + info.custody_group_count as usize + ); + + // Advance the chain some more to expire some blobs. + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_secondary as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + LightClientStrategy::Disabled, + ) + .await; + + assert_eq!(harness.chain.slot().unwrap(), num_initial + num_secondary); + + let info = client.get_lighthouse_custody_info().await.unwrap(); + assert_eq!( + info.earliest_custodied_data_column_slot, + num_initial + num_secondary + - spec.min_epochs_for_data_column_sidecars_requests * E::slots_per_epoch() + ); + assert_eq!(info.custody_group_count, spec.custody_requirement); + assert_eq!( + info.custody_columns.len(), + info.custody_group_count as usize + ); +} diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index b8a42ce2cc..556b75cb85 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -1,7 +1,7 @@ //! Tests related to the beacon node's sync status use beacon_chain::{ - test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, BlockError, + test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, }; use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; @@ -12,8 +12,10 @@ type E = MinimalEthSpec; /// Create a new test environment that is post-merge with `chain_depth` blocks. async fn post_merge_tester(chain_depth: u64, validator_count: u64) -> InteractiveTester { - // Test using latest fork so that we simulate conditions as similar to mainnet as possible. - let mut spec = ForkName::Eip7805.make_genesis_spec(E::default_spec()); + // TODO(EIP-7732): extend tests for Gloas by reverting back to using `ForkName::latest()` + // Issue is that these tests do block production via `extend_chain_with_sync` which expects to be able to use `state.latest_execution_payload_header` during block production, but Gloas uses `latest_execution_bid` instead + // This will be resolved in a subsequent block processing PR + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); spec.terminal_total_difficulty = Uint256::from(1); let tester = InteractiveTester::::new(Some(spec), validator_count as usize).await; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 89dfff0b57..c4d4e588ec 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1,36 +1,42 @@ +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::RelativeSyncCommittee; use beacon_chain::{ - test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, BeaconChain, ChainConfig, StateSkipConfig, WhenSlotSkipped, -}; -use either::Either; -use eth2::{ - mixin::{RequestAccept, ResponseForkName, ResponseOptional}, - reqwest::RequestBuilder, - types::{ - BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, + test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, }, +}; +use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; +use eth2::{ BeaconNodeHttpClient, Error, Error::ServerMessage, StatusCode, Timeouts, + mixin::{RequestAccept, ResponseForkName, ResponseOptional}, + reqwest::{RequestBuilder, Response}, + types::{ + BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, + }, }; use execution_layer::expected_gas_limit; use execution_layer::test_utils::{ - mock_builder_extra_data, mock_el_extra_data, MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_GAS_LIMIT, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, + MockBuilder, Operation, mock_builder_extra_data, mock_el_extra_data, }; -use futures::stream::{Stream, StreamExt}; +use fixed_bytes::FixedBytesExtended; use futures::FutureExt; +use futures::stream::{Stream, StreamExt}; use http_api::{ - test_utils::{create_api_server, ApiServer}, BlockId, StateId, + test_utils::{ApiServer, create_api_server}, }; -use lighthouse_network::{types::SyncState, Enr, EnrExt, PeerId}; +use lighthouse_network::{Enr, PeerId, types::SyncState}; use network::NetworkReceivers; +use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; +use ssz::BitList; use state_processing::per_block_processing::get_expected_withdrawals; use state_processing::per_slot_processing; use state_processing::state_advance::partial_state_advance; @@ -40,9 +46,8 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::application_domain::ApplicationDomain; use types::{ - attestation::AttestationBase, AggregateSignature, BitList, Domain, EthSpec, ExecutionBlockHash, - Hash256, Keypair, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, SingleAttestation, - Slot, + Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, + SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, }; type E = MainnetEthSpec; @@ -88,6 +93,7 @@ struct ApiTester { struct ApiTesterConfig { spec: ChainSpec, retain_historic_states: bool, + node_custody_type: NodeCustodyType, } impl Default for ApiTesterConfig { @@ -97,6 +103,7 @@ impl Default for ApiTesterConfig { Self { spec, retain_historic_states: false, + node_custody_type: NodeCustodyType::Fullnode, } } } @@ -114,15 +121,11 @@ impl ApiTester { Self::new_from_config(ApiTesterConfig::default()).await } - pub async fn new_with_hard_forks(altair: bool, bellatrix: bool) -> Self { - let mut config = ApiTesterConfig::default(); - // Set whether the chain has undergone each hard fork. - if altair { - config.spec.altair_fork_epoch = Some(Epoch::new(0)); - } - if bellatrix { - config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - } + pub async fn new_with_hard_forks() -> Self { + let config = ApiTesterConfig { + spec: test_spec::(), + ..Default::default() + }; Self::new_from_config(config).await } @@ -138,7 +141,8 @@ impl ApiTester { .deterministic_keypairs(VALIDATOR_COUNT) .deterministic_withdrawal_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() - .mock_execution_layer_with_config() + .mock_execution_layer() + .node_custody_type(config.node_custody_type) .build(); harness @@ -176,6 +180,9 @@ impl ApiTester { "precondition: current slot is one after head" ); + // Set a min blob count for the next block for get_blobs testing + harness.execution_block_generator().set_min_blob_count(2); + let (next_block, _next_state) = harness .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap()) .await; @@ -292,7 +299,19 @@ impl ApiTester { let beacon_api_port = listening_socket.port(); let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{beacon_api_port}").as_str()).unwrap(); - let mock_builder_server = harness.set_mock_builder(beacon_url.clone()); + + // Be strict with validator registrations, but don't bother applying operations, that flag + // is only used by mock-builder tests. + let strict_registrations = true; + let apply_operations = true; + let broadcast_to_bn = true; + + let mock_builder_server = harness.set_mock_builder( + beacon_url.clone(), + strict_registrations, + apply_operations, + broadcast_to_bn, + ); // Start the mock builder service prior to building the chain out. harness @@ -335,6 +354,7 @@ impl ApiTester { .deterministic_keypairs(VALIDATOR_COUNT) .deterministic_withdrawal_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() + .mock_execution_layer() .build(), ); @@ -420,7 +440,7 @@ impl ApiTester { } pub async fn new_mev_tester() -> Self { - let tester = Self::new_with_hard_forks(true, true) + let tester = Self::new_with_hard_forks() .await .test_post_validator_register_validator() .await; @@ -430,10 +450,7 @@ impl ApiTester { } pub async fn new_mev_tester_default_payload_value() -> Self { - let mut config = ApiTesterConfig { - retain_historic_states: false, - spec: E::default_spec(), - }; + let mut config = ApiTesterConfig::default(); config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); let tester = Self::new_from_config(config) @@ -965,6 +982,87 @@ impl ApiTester { self } + pub async fn test_beacon_states_validator_identities(self) -> Self { + for state_id in self.interesting_state_ids() { + for validator_indices in self.interesting_validator_indices() { + let state_opt = state_id.state(&self.chain).ok(); + let validators: Vec = match state_opt.as_ref() { + Some((state, _execution_optimistic, _finalized)) => { + state.validators().clone().to_vec() + } + None => vec![], + }; + + let validator_index_ids = validator_indices + .iter() + .cloned() + .map(ValidatorId::Index) + .collect::>(); + + let validator_pubkey_ids = validator_indices + .iter() + .cloned() + .map(|i| { + ValidatorId::PublicKey( + validators + .get(i as usize) + .map_or(PublicKeyBytes::empty(), |val| val.pubkey), + ) + }) + .collect::>(); + + let result_index_ids = self + .client + .post_beacon_states_validator_identities(state_id.0, validator_index_ids) + .await + .unwrap() + .map(|res| res.data); + let result_pubkey_ids = self + .client + .post_beacon_states_validator_identities(state_id.0, validator_pubkey_ids) + .await + .unwrap() + .map(|res| res.data); + + let expected = state_opt.map(|(state, _execution_optimistic, _finalized)| { + // If validator_indices is empty, return identities for all validators + if validator_indices.is_empty() { + state + .validators() + .iter() + .enumerate() + .map(|(index, validator)| ValidatorIdentityData { + index: index as u64, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }) + .collect() + } else { + let mut validators = Vec::with_capacity(validator_indices.len()); + + for i in validator_indices { + if i < state.validators().len() as u64 { + // access each validator, and then transform the data into ValidatorIdentityData + let validator = state.validators().get(i as usize).unwrap(); + validators.push(ValidatorIdentityData { + index: i, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }); + } + } + + validators + } + }); + + assert_eq!(result_index_ids, expected, "{:?}", state_id); + assert_eq!(result_pubkey_ids, expected, "{:?}", state_id); + } + } + self + } + pub async fn test_beacon_states_validators(self) -> Self { for state_id in self.interesting_state_ids() { for statuses in self.interesting_validator_statuses() { @@ -999,7 +1097,7 @@ impl ApiTester { .get_beacon_states_validators( state_id.0, Some(validator_index_ids.as_slice()), - None, + Some(statuses.as_slice()), ) .await .unwrap() @@ -1009,20 +1107,28 @@ impl ApiTester { .get_beacon_states_validators( state_id.0, Some(validator_pubkey_ids.as_slice()), - None, + Some(statuses.as_slice()), ) .await .unwrap() .map(|res| res.data); let post_result_index_ids = self .client - .post_beacon_states_validators(state_id.0, Some(validator_index_ids), None) + .post_beacon_states_validators( + state_id.0, + Some(validator_index_ids), + Some(statuses.clone()), + ) .await .unwrap() .map(|res| res.data); let post_result_pubkey_ids = self .client - .post_beacon_states_validators(state_id.0, Some(validator_pubkey_ids), None) + .post_beacon_states_validators( + state_id.0, + Some(validator_pubkey_ids), + Some(statuses.clone()), + ) .await .unwrap() .map(|res| res.data); @@ -1033,7 +1139,13 @@ impl ApiTester { let mut validators = Vec::with_capacity(validator_indices.len()); - for i in validator_indices { + let expected_indices = if validator_indices.is_empty() { + (0..state.validators().len() as u64).collect() + } else { + validator_indices.clone() + }; + + for i in expected_indices { if i >= state.validators().len() as u64 { continue; } @@ -1043,8 +1155,8 @@ impl ApiTester { epoch, far_future_epoch, ); - if statuses.contains(&status) - || statuses.is_empty() + if statuses.is_empty() + || statuses.contains(&status) || statuses.contains(&status.superstatus()) { validators.push(ValidatorData { @@ -1209,12 +1321,14 @@ impl ApiTester { .ok() .map(|(state, _execution_optimistic, _finalized)| state); - let result = self + let result = match self .client .get_beacon_states_pending_deposits(state_id.0) .await - .unwrap() - .map(|res| res.data); + { + Ok(response) => response, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; if result.is_none() && state_opt.is_none() { continue; @@ -1223,7 +1337,12 @@ impl ApiTester { let state = state_opt.as_mut().expect("result should be none"); let expected = state.pending_deposits().unwrap(); - assert_eq!(result.unwrap(), expected.to_vec()); + let response = result.unwrap(); + assert_eq!(response.data(), &expected.to_vec()); + + // Check that the version header is returned in the response + let fork_name = state.fork_name(&self.chain.spec).unwrap(); + assert_eq!(response.version(), Some(fork_name),); } self @@ -1236,12 +1355,14 @@ impl ApiTester { .ok() .map(|(state, _execution_optimistic, _finalized)| state); - let result = self + let result = match self .client .get_beacon_states_pending_partial_withdrawals(state_id.0) .await - .unwrap() - .map(|res| res.data); + { + Ok(response) => response, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; if result.is_none() && state_opt.is_none() { continue; @@ -1250,7 +1371,12 @@ impl ApiTester { let state = state_opt.as_mut().expect("result should be none"); let expected = state.pending_partial_withdrawals().unwrap(); - assert_eq!(result.unwrap(), expected.to_vec()); + let response = result.unwrap(); + assert_eq!(response.data(), &expected.to_vec()); + + // Check that the version header is returned in the response + let fork_name = state.fork_name(&self.chain.spec).unwrap(); + assert_eq!(response.version(), Some(fork_name),); } self @@ -1263,12 +1389,14 @@ impl ApiTester { .ok() .map(|(state, _execution_optimistic, _finalized)| state); - let result = self + let result = match self .client .get_beacon_states_pending_consolidations(state_id.0) .await - .unwrap() - .map(|res| res.data); + { + Ok(response) => response, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; if result.is_none() && state_opt.is_none() { continue; @@ -1277,7 +1405,12 @@ impl ApiTester { let state = state_opt.as_mut().expect("result should be none"); let expected = state.pending_consolidations().unwrap(); - assert_eq!(result.unwrap(), expected.to_vec()); + let response = result.unwrap(); + assert_eq!(response.data(), &expected.to_vec()); + + // Check that the version header is returned in the response + let fork_name = state.fork_name(&self.chain.spec).unwrap(); + assert_eq!(response.version(), Some(fork_name),); } self @@ -1445,7 +1578,10 @@ impl ApiTester { pub async fn test_post_beacon_blocks_valid(mut self) -> Self { let next_block = self.next_block.clone(); - self.client.post_beacon_blocks(&next_block).await.unwrap(); + self.client + .post_beacon_blocks_v2(&next_block, None) + .await + .unwrap(); assert!( self.network_rx.network_recv.recv().await.is_some(), @@ -1459,7 +1595,7 @@ impl ApiTester { let next_block = &self.next_block; self.client - .post_beacon_blocks_ssz(next_block) + .post_beacon_blocks_v2_ssz(next_block, None) .await .unwrap(); @@ -1484,11 +1620,14 @@ impl ApiTester { .await .0; - assert!(self + let response: Result = self .client - .post_beacon_blocks(&PublishBlockRequest::from(block)) - .await - .is_err()); + .post_beacon_blocks_v2(&PublishBlockRequest::from(block), None) + .await; + + assert!(response.is_ok()); + + assert_eq!(response.unwrap().status(), StatusCode::ACCEPTED); assert!( self.network_rx.network_recv.recv().await.is_some(), @@ -1511,12 +1650,13 @@ impl ApiTester { .await .0; - assert!(self + let response: Result = self .client - .post_beacon_blocks_ssz(&PublishBlockRequest::from(block)) - .await - .is_err()); + .post_beacon_blocks_v2(&PublishBlockRequest::from(block), None) + .await; + assert!(response.is_ok()); + assert_eq!(response.unwrap().status(), StatusCode::ACCEPTED); assert!( self.network_rx.network_recv.recv().await.is_some(), "gossip valid blocks should be sent to network" @@ -1536,56 +1676,37 @@ impl ApiTester { .0 .into(); - assert!(self - .client - .post_beacon_blocks(&block_contents) - .await - .is_ok()); + assert!( + self.client + .post_beacon_blocks_v2(&block_contents, None) + .await + .is_ok() + ); // Blinded deneb block contents is just the blinded block let blinded_block_contents = block_contents.signed_block().clone_as_blinded(); // Test all the POST methods in sequence, they should all behave the same. let responses = vec![ - self.client - .post_beacon_blocks(&block_contents) - .await - .unwrap_err(), self.client .post_beacon_blocks_v2(&block_contents, None) .await - .unwrap_err(), - self.client - .post_beacon_blocks_ssz(&block_contents) - .await - .unwrap_err(), + .unwrap(), self.client .post_beacon_blocks_v2_ssz(&block_contents, None) .await - .unwrap_err(), - self.client - .post_beacon_blinded_blocks(&blinded_block_contents) - .await - .unwrap_err(), + .unwrap(), self.client .post_beacon_blinded_blocks_v2(&blinded_block_contents, None) .await - .unwrap_err(), - self.client - .post_beacon_blinded_blocks_ssz(&blinded_block_contents) - .await - .unwrap_err(), + .unwrap(), self.client .post_beacon_blinded_blocks_v2_ssz(&blinded_block_contents, None) .await - .unwrap_err(), + .unwrap(), ]; for (i, response) in responses.into_iter().enumerate() { - assert_eq!( - response.status().unwrap(), - StatusCode::ACCEPTED, - "response {i}" - ); + assert_eq!(response.status(), StatusCode::ACCEPTED, "response {i}"); } self @@ -1753,7 +1874,7 @@ impl ApiTester { } pub async fn test_get_blob_sidecars(self, use_indices: bool) -> Self { - let block_id = BlockId(CoreBlockId::Finalized); + let block_id = BlockId(CoreBlockId::Head); let (block_root, _, _) = block_id.root(&self.chain).unwrap(); let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); let num_blobs = block.num_expected_blobs(); @@ -1764,7 +1885,7 @@ impl ApiTester { }; let result = match self .client - .get_blobs::( + .get_blob_sidecars::( CoreBlockId::Root(block_root), blob_indices.as_deref(), &self.chain.spec, @@ -1785,6 +1906,77 @@ impl ApiTester { self } + pub async fn test_get_blobs(self, versioned_hashes: bool) -> Self { + let block_id = BlockId(CoreBlockId::Head); + let (block_root, _, _) = block_id.root(&self.chain).unwrap(); + let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); + let num_blobs = block.num_expected_blobs(); + + let versioned_hashes: Option> = if versioned_hashes { + Some( + block + .message() + .body() + .blob_kzg_commitments() + .unwrap() + .iter() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect(), + ) + } else { + None + }; + + let result = match self + .client + .get_blobs::(CoreBlockId::Root(block_root), versioned_hashes.as_deref()) + .await + { + Ok(response) => response.unwrap().into_data(), + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + assert_eq!( + result.len(), + versioned_hashes.map_or(num_blobs, |versioned_hashes| versioned_hashes.len()) + ); + + self + } + + pub async fn test_get_blobs_post_fulu_full_node(self, versioned_hashes: bool) -> Self { + let block_id = BlockId(CoreBlockId::Head); + let (block_root, _, _) = block_id.root(&self.chain).unwrap(); + let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); + + let versioned_hashes: Option> = if versioned_hashes { + Some( + block + .message() + .body() + .blob_kzg_commitments() + .unwrap() + .iter() + .map(|commitment| commitment.calculate_versioned_hash()) + .collect(), + ) + } else { + None + }; + + match self + .client + .get_blobs::(CoreBlockId::Root(block_root), versioned_hashes.as_deref()) + .await + { + Ok(result) => panic!("Full node are unable to return blobs post-Fulu: {result:?}"), + // Post-Fulu, full nodes don't store blobs and return error 500 + Err(e) => assert_eq!(e.status().unwrap(), 500), + }; + + self + } + /// Test fetching of blob sidecars that are not available in the database due to pruning. /// /// If `zero_blobs` is false, test a block with >0 blobs, which should be unavailable. @@ -1824,7 +2016,7 @@ impl ApiTester { match self .client - .get_blobs::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) + .get_blob_sidecars::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) .await { Ok(result) => { @@ -1862,7 +2054,7 @@ impl ApiTester { match self .client - .get_blobs::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) + .get_blob_sidecars::(CoreBlockId::Slot(test_slot), None, &self.chain.spec) .await { Ok(result) => panic!("queries for pre-Deneb slots should fail. got: {result:?}"), @@ -1907,18 +2099,46 @@ impl ApiTester { } pub async fn test_post_beacon_pool_attestations_valid(mut self) -> Self { - self.client - .post_beacon_pool_attestations_v1(self.attestations.as_slice()) - .await - .unwrap(); - let fork_name = self .attestations .first() .map(|att| self.chain.spec.fork_name_at_slot::(att.data().slot)) .unwrap(); - let attestations = Either::Left(self.attestations.clone()); + let state = &self.chain.head_snapshot().beacon_state; + + let attestations = self + .attestations + .clone() + .into_iter() + .map(|attn| { + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + attn.to_single_attestation_with_attester_index(attester_index as u64) + .unwrap() + }) + .collect::>(); self.client .post_beacon_pool_attestations_v2::(attestations, fork_name) @@ -1943,9 +2163,8 @@ impl ApiTester { .map(|att| self.chain.spec.fork_name_at_slot::(att.data.slot)) .unwrap(); - let attestations = Either::Right(self.single_attestations.clone()); self.client - .post_beacon_pool_attestations_v2::(attestations, fork_name) + .post_beacon_pool_attestations_v2::(self.single_attestations.clone(), fork_name) .await .unwrap(); assert!( @@ -1958,18 +2177,87 @@ impl ApiTester { pub async fn test_post_beacon_pool_attestations_invalid_v1(mut self) -> Self { let mut attestations = Vec::new(); + let state = &self.chain.head_snapshot().beacon_state; for attestation in &self.attestations { let mut invalid_attestation = attestation.clone(); invalid_attestation.data_mut().slot += 1; + // Convert valid attestation into valid `SingleAttestation` + let aggregation_bits = attestation.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state + .get_beacon_committee( + attestation.data().slot, + attestation.committee_index().unwrap(), + ) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + let attestation = attestation + .to_single_attestation_with_attester_index(attester_index as u64) + .unwrap(); + + // Convert invalid attestation to invalid `SingleAttestation` + let aggregation_bits = invalid_attestation.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state + .get_beacon_committee( + invalid_attestation.data().slot, + invalid_attestation.committee_index().unwrap(), + ) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + let invalid_attestation = invalid_attestation + .to_single_attestation_with_attester_index(attester_index as u64) + .unwrap(); + // add both to ensure we only fail on invalid attestations attestations.push(attestation.clone()); attestations.push(invalid_attestation); } + let fork_name = self + .attestations + .first() + .map(|att| self.chain.spec.fork_name_at_slot::(att.data().slot)) + .unwrap(); + let err = self .client - .post_beacon_pool_attestations_v1(attestations.as_slice()) + .post_beacon_pool_attestations_v2::(attestations, fork_name) .await .unwrap_err(); @@ -2011,7 +2299,6 @@ impl ApiTester { .first() .map(|att| self.chain.spec.fork_name_at_slot::(att.data().slot)) .unwrap(); - let attestations = Either::Right(attestations); let err_v2 = self .client .post_beacon_pool_attestations_v2::(attestations, fork_name) @@ -2038,6 +2325,24 @@ impl ApiTester { self } + pub async fn test_get_beacon_light_client_updates_ssz(self) -> Self { + let current_epoch = self.chain.epoch().unwrap(); + let current_sync_committee_period = current_epoch + .sync_committee_period(&self.chain.spec) + .unwrap(); + + match self + .client + .get_beacon_light_client_updates_ssz::(current_sync_committee_period, 1) + .await + { + Ok(result) => result, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + self + } + pub async fn test_get_beacon_light_client_updates(self) -> Self { let current_epoch = self.chain.epoch().unwrap(); let current_sync_committee_period = current_epoch @@ -2263,10 +2568,10 @@ impl ApiTester { pub async fn test_post_beacon_pool_attester_slashings_invalid_v1(mut self) -> Self { let mut slashing = self.attester_slashing.clone(); match &mut slashing { - AttesterSlashing::Base(ref mut slashing) => { + AttesterSlashing::Base(slashing) => { slashing.attestation_1.data.slot += 1; } - AttesterSlashing::Electra(ref mut slashing) => { + AttesterSlashing::Electra(slashing) => { slashing.attestation_1.data.slot += 1; } } @@ -2287,10 +2592,10 @@ impl ApiTester { pub async fn test_post_beacon_pool_attester_slashings_invalid_v2(mut self) -> Self { let mut slashing = self.attester_slashing.clone(); match &mut slashing { - AttesterSlashing::Base(ref mut slashing) => { + AttesterSlashing::Base(slashing) => { slashing.attestation_1.data.slot += 1; } - AttesterSlashing::Electra(ref mut slashing) => { + AttesterSlashing::Electra(slashing) => { slashing.attestation_1.data.slot += 1; } } @@ -2441,13 +2746,24 @@ impl ApiTester { } pub async fn test_get_config_spec(self) -> Self { - let result = self - .client - .get_config_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); + let result = if self.chain.spec.is_gloas_scheduled() { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Gloas(res.data)) + } else if self.chain.spec.is_fulu_scheduled() { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec); assert_eq!(result, expected); @@ -2539,9 +2855,19 @@ impl ApiTester { let expected = IdentityData { peer_id: self.local_enr.peer_id().to_string(), - enr: self.local_enr.clone(), - p2p_addresses: self.local_enr.multiaddr_p2p_tcp(), - discovery_addresses: self.local_enr.multiaddr_p2p_udp(), + enr: self.local_enr.to_base64(), + p2p_addresses: self + .local_enr + .multiaddr_p2p_tcp() + .iter() + .map(|a| a.to_string()) + .collect(), + discovery_addresses: self + .local_enr + .multiaddr_p2p_udp() + .iter() + .map(|a| a.to_string()) + .collect(), metadata: MetaData::V2(MetaDataV2 { seq_number: 0, attnets: "0x0000000000000000".to_string(), @@ -2570,7 +2896,7 @@ impl ApiTester { pub async fn test_get_node_peers_by_id(self) -> Self { let result = self .client - .get_node_peers_by_id(self.external_peer_id) + .get_node_peers_by_id(&self.external_peer_id.to_string()) .await .unwrap() .data; @@ -2743,11 +3069,11 @@ impl ApiTester { assert_eq!( result.justified_checkpoint, - expected_proto_array.justified_checkpoint + beacon_fork_choice.justified_checkpoint() ); assert_eq!( result.finalized_checkpoint, - expected_proto_array.finalized_checkpoint + beacon_fork_choice.finalized_checkpoint() ); let expected_fork_choice_nodes: Vec = expected_proto_array @@ -2774,6 +3100,32 @@ impl ApiTester { .execution_status .block_hash() .map(|block_hash| block_hash.into_root()), + extra_data: ForkChoiceExtraData { + target_root: node.target_root, + justified_root: node.justified_checkpoint.root, + finalized_root: node.finalized_checkpoint.root, + unrealized_justified_root: node + .unrealized_justified_checkpoint + .map(|checkpoint| checkpoint.root), + unrealized_finalized_root: node + .unrealized_finalized_checkpoint + .map(|checkpoint| checkpoint.root), + unrealized_justified_epoch: node + .unrealized_justified_checkpoint + .map(|checkpoint| checkpoint.epoch), + unrealized_finalized_epoch: node + .unrealized_finalized_checkpoint + .map(|checkpoint| checkpoint.epoch), + execution_status: node.execution_status.to_string(), + best_child: node + .best_child + .and_then(|index| expected_proto_array.nodes.get(index)) + .map(|child| child.root), + best_descendant: node + .best_descendant + .and_then(|index| expected_proto_array.nodes.get(index)) + .map(|descendant| descendant.root), + }, } }) .collect(); @@ -3199,7 +3551,7 @@ impl ApiTester { PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); self.client - .post_beacon_blocks(&signed_block_contents) + .post_beacon_blocks_v2(&signed_block_contents, None) .await .unwrap(); @@ -3264,7 +3616,7 @@ impl ApiTester { block_contents.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); self.client - .post_beacon_blocks_ssz(&signed_block_contents) + .post_beacon_blocks_v2_ssz(&signed_block_contents, None) .await .unwrap(); @@ -3344,7 +3696,7 @@ impl ApiTester { let (response, metadata) = self .client - .get_validator_blocks_v3_ssz::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3_ssz::(slot, &randao_reveal, None, None, None) .await .unwrap(); @@ -3382,7 +3734,7 @@ impl ApiTester { block_contents.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); self.client - .post_beacon_blocks_ssz(&signed_block_contents) + .post_beacon_blocks_v2_ssz(&signed_block_contents, None) .await .unwrap(); @@ -3900,10 +4252,10 @@ impl ApiTester { pub async fn test_get_validator_aggregate_and_proofs_invalid_v1(mut self) -> Self { let mut aggregate = self.get_aggregate().await; match &mut aggregate { - SignedAggregateAndProof::Base(ref mut aggregate) => { + SignedAggregateAndProof::Base(aggregate) => { aggregate.message.aggregate.data.slot += 1; } - SignedAggregateAndProof::Electra(ref mut aggregate) => { + SignedAggregateAndProof::Electra(aggregate) => { aggregate.message.aggregate.data.slot += 1; } } @@ -3937,10 +4289,10 @@ impl ApiTester { pub async fn test_get_validator_aggregate_and_proofs_invalid_v2(mut self) -> Self { let mut aggregate = self.get_aggregate().await; match &mut aggregate { - SignedAggregateAndProof::Base(ref mut aggregate) => { + SignedAggregateAndProof::Base(aggregate) => { aggregate.message.aggregate.data.slot += 1; } - SignedAggregateAndProof::Electra(ref mut aggregate) => { + SignedAggregateAndProof::Electra(aggregate) => { aggregate.message.aggregate.data.slot += 1; } } @@ -4192,9 +4544,47 @@ impl ApiTester { assert_eq!(result, expected); + let attestations = self + .attestations + .clone() + .into_iter() + .map(|attn| { + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = head_state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + attn.to_single_attestation_with_attester_index(attester_index as u64) + .unwrap() + }) + .collect::>(); + + let fork_name = self + .chain + .spec + .fork_name_at_slot::(attestations.first().unwrap().data.slot); + // Attest to the current slot self.client - .post_beacon_pool_attestations_v1(self.attestations.as_slice()) + .post_beacon_pool_attestations_v2::(attestations, fork_name) .await .unwrap(); @@ -4271,7 +4661,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4298,7 +4688,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(0)) + .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(0), None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4326,7 +4716,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(u64::MAX)) + .get_validator_blocks_v3::(slot, &randao_reveal, None, Some(u64::MAX), None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4368,13 +4758,14 @@ impl ApiTester { // If this cache is empty, it indicates fallback was not used, so the payload came from the // mock builder. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); self } @@ -4413,13 +4804,14 @@ impl ApiTester { assert_eq!(payload.gas_limit(), builder_limit); // This cache should not be populated because fallback should not have been used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -4453,13 +4845,14 @@ impl ApiTester { .into(); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4471,7 +4864,7 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .add_operation(Operation::GasLimit(30_000_000)); + .add_operation(Operation::GasLimit(DEFAULT_GAS_LIMIT as usize)); let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -4480,7 +4873,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4494,7 +4887,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 30_000_000); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } @@ -4529,13 +4922,14 @@ impl ApiTester { assert_eq!(payload.fee_recipient(), test_fee_recipient); // This cache should not be populated because fallback should not have been used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -4560,7 +4954,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4615,13 +5009,14 @@ impl ApiTester { assert_eq!(payload.parent_hash(), expected_parent_hash); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4654,7 +5049,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4707,13 +5102,14 @@ impl ApiTester { assert_eq!(payload.prev_randao(), expected_prev_randao); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4744,7 +5140,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4797,13 +5193,14 @@ impl ApiTester { assert_eq!(payload.block_number(), expected_block_number); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4834,7 +5231,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4886,13 +5283,14 @@ impl ApiTester { assert!(payload.timestamp() > min_expected_timestamp); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4922,7 +5320,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -4959,13 +5357,14 @@ impl ApiTester { .into(); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4982,7 +5381,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5022,13 +5421,14 @@ impl ApiTester { .into(); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5052,7 +5452,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5098,13 +5498,14 @@ impl ApiTester { .into(); // This cache should not be populated because fallback should not have been used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -5129,13 +5530,14 @@ impl ApiTester { .into(); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5165,7 +5567,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5186,7 +5588,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5237,13 +5639,14 @@ impl ApiTester { .into(); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5278,13 +5681,14 @@ impl ApiTester { .into(); // This cache should not be populated because fallback should not have been used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -5319,7 +5723,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5350,7 +5754,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(next_slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5397,13 +5801,14 @@ impl ApiTester { assert_eq!(payload.fee_recipient(), expected_fee_recipient); // If this cache is populated, it indicates fallback to the local EE was correctly used. - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5431,7 +5836,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5475,13 +5880,14 @@ impl ApiTester { .into(); // The builder's payload should've been chosen, so this cache should not be populated - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -5504,7 +5910,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5543,13 +5949,14 @@ impl ApiTester { .into(); // The local payload should've been chosen, so this cache should be populated - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5572,7 +5979,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5611,13 +6018,14 @@ impl ApiTester { .into(); // The local payload should've been chosen, so this cache should be populated - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); // another way is to check for the extra data of the local EE assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -5640,7 +6048,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5678,13 +6086,14 @@ impl ApiTester { .into(); // The builder's payload should've been chosen, so this cache should not be populated - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_none() + ); // Another way is to check for the extra data of the mock builder assert_eq!(payload.extra_data(), mock_builder_extra_data::()); @@ -5706,7 +6115,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5749,13 +6158,14 @@ impl ApiTester { .into(); // The local payload should've been chosen because the builder's was invalid - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_some()); + assert!( + self.chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some() + ); self } @@ -5779,7 +6189,7 @@ impl ApiTester { let (payload_type, metadata) = self .client - .get_validator_blocks_v3::(slot, &randao_reveal, None, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None, None, None) .await .unwrap(); Self::check_block_v3_metadata(&metadata, &payload_type); @@ -5838,40 +6248,6 @@ impl ApiTester { self } - pub async fn test_get_lighthouse_eth1_syncing(self) -> Self { - self.client.get_lighthouse_eth1_syncing().await.unwrap(); - - self - } - - pub async fn test_get_lighthouse_eth1_block_cache(self) -> Self { - let blocks = self.client.get_lighthouse_eth1_block_cache().await.unwrap(); - - assert!(blocks.data.is_empty()); - - self - } - - pub async fn test_get_lighthouse_eth1_deposit_cache(self) -> Self { - let deposits = self - .client - .get_lighthouse_eth1_deposit_cache() - .await - .unwrap(); - - assert!(deposits.data.is_empty()); - - self - } - - pub async fn test_get_lighthouse_staking(self) -> Self { - let result = self.client.get_lighthouse_staking().await.unwrap(); - - assert_eq!(result, self.chain.eth1_chain.is_some()); - - self - } - pub async fn test_post_lighthouse_database_reconstruct(self) -> Self { let response = self .client @@ -5931,9 +6307,47 @@ impl ApiTester { assert_eq!(result, expected); + let attestations = self + .attestations + .clone() + .into_iter() + .map(|attn| { + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = head_state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + attn.to_single_attestation_with_attester_index(attester_index as u64) + .unwrap() + }) + .collect::>(); + + let fork_name = self + .chain + .spec + .fork_name_at_slot::(attestations.first().unwrap().data.slot); + // Attest to the current slot self.client - .post_beacon_pool_attestations_v1(self.attestations.as_slice()) + .post_beacon_pool_attestations_v2::(attestations, fork_name) .await .unwrap(); @@ -5988,8 +6402,47 @@ impl ApiTester { let expected_attestation_len = self.attestations.len(); + let state = self.harness.get_current_state(); + let attestations = self + .attestations + .clone() + .into_iter() + .map(|attn| { + let aggregation_bits = attn.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state + .get_beacon_committee(attn.data().slot, attn.committee_index().unwrap()) + .unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + attn.to_single_attestation_with_attester_index(attester_index as u64) + .unwrap() + }) + .collect::>(); + + let fork_name = self + .chain + .spec + .fork_name_at_slot::(attestations.first().unwrap().data.slot); + self.client - .post_beacon_pool_attestations_v1(self.attestations.as_slice()) + .post_beacon_pool_attestations_v2::(attestations, fork_name) .await .unwrap(); @@ -6023,7 +6476,9 @@ impl ApiTester { // Produce a BLS to execution change event self.client - .post_beacon_pool_bls_to_execution_changes(&[self.bls_to_execution_change.clone()]) + .post_beacon_pool_bls_to_execution_changes(std::slice::from_ref( + &self.bls_to_execution_change, + )) .await .unwrap(); @@ -6085,7 +6540,7 @@ impl ApiTester { }); self.client - .post_beacon_blocks(&self.next_block) + .post_beacon_blocks_v2(&self.next_block, None) .await .unwrap(); @@ -6130,7 +6585,7 @@ impl ApiTester { self.harness.advance_slot(); self.client - .post_beacon_blocks(&self.reorg_block) + .post_beacon_blocks_v2(&self.reorg_block, None) .await .unwrap(); @@ -6278,9 +6733,9 @@ impl ApiTester { .chain .spec .fork_name_at_slot::(self.chain.slot().unwrap()); - let attestations = Either::Right(self.single_attestations.clone()); + self.client - .post_beacon_pool_attestations_v2::(attestations, fork_name) + .post_beacon_pool_attestations_v2::(self.single_attestations.clone(), fork_name) .await .unwrap(); @@ -6368,7 +6823,7 @@ impl ApiTester { }); self.client - .post_beacon_blocks(&self.next_block) + .post_beacon_blocks_v2(&self.next_block, None) .await .unwrap(); @@ -6440,6 +6895,82 @@ impl ApiTester { } self } + + async fn get_validator_blocks_v3_path_graffiti_policy(self) -> Self { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + let graffiti = Some(Graffiti::from([0; GRAFFITI_BYTES_LEN])); + let builder_boost_factor = None; + + // Default case where GraffitiPolicy is None + let default_path = self + .client + .get_validator_blocks_v3_path( + slot, + &randao_reveal, + graffiti.as_ref(), + SkipRandaoVerification::Yes, + builder_boost_factor, + None, + ) + .await + .unwrap(); + + let query_default_path = default_path.query().unwrap_or(""); + // When GraffitiPolicy is None, the HTTP API query path should not contain "graffiti_policy" + assert!( + !query_default_path.contains("graffiti_policy"), + "URL should not contain graffiti_policy parameter (same as PreserveUserGraffiti). URL is: {}", + query_default_path + ); + + let preserve_path = self + .client + .get_validator_blocks_v3_path( + slot, + &randao_reveal, + graffiti.as_ref(), + SkipRandaoVerification::Yes, + builder_boost_factor, + Some(GraffitiPolicy::PreserveUserGraffiti), + ) + .await + .unwrap(); + + let query_preserve_path = preserve_path.query().unwrap_or(""); + // When GraffitiPolicy is set to PreserveUserGraffiti, the HTTP API query path should not contain "graffiti_policy" + assert!( + !query_preserve_path.contains("graffiti_policy"), + "URL should not contain graffiti_policy parameter when using PreserveUserGraffiti. URL is: {}", + query_preserve_path + ); + + // The HTTP API query path for PreserveUserGraffiti should be the same as the default + assert_eq!(query_default_path, query_preserve_path); + + let append_path = self + .client + .get_validator_blocks_v3_path( + slot, + &randao_reveal, + graffiti.as_ref(), + SkipRandaoVerification::No, + builder_boost_factor, + Some(GraffitiPolicy::AppendClientVersions), + ) + .await + .unwrap(); + + let query_append_path = append_path.query().unwrap_or(""); + // When GraffitiPolicy is AppendClientVersions, the HTTP API query path should contain "graffiti_policy" + assert!( + query_append_path.contains("graffiti_policy"), + "URL should contain graffiti_policy=AppendClientVersions parameter. URL is: {}", + query_append_path + ); + self + } } async fn poll_events, eth2::Error>> + Unpin, E: EthSpec>( @@ -6541,6 +7072,8 @@ async fn beacon_get_state_info() { .await .test_beacon_states_validator_balances() .await + .test_beacon_states_validator_identities() + .await .test_beacon_states_committees() .await .test_beacon_states_validator_id() @@ -6815,6 +7348,8 @@ async fn get_light_client_updates() { ApiTester::new_from_config(config) .await .test_get_beacon_light_client_updates() + .await + .test_get_beacon_light_client_updates_ssz() .await; } @@ -7410,10 +7945,7 @@ async fn builder_payload_chosen_by_profit_v3() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_works_post_capella() { - let mut config = ApiTesterConfig { - retain_historic_states: false, - spec: E::default_spec(), - }; + let mut config = ApiTesterConfig::default(); config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); @@ -7430,10 +7962,7 @@ async fn builder_works_post_capella() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_works_post_deneb() { - let mut config = ApiTesterConfig { - retain_historic_states: false, - spec: E::default_spec(), - }; + let mut config = ApiTesterConfig::default(); config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); @@ -7451,10 +7980,7 @@ async fn builder_works_post_deneb() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blob_sidecars() { - let mut config = ApiTesterConfig { - retain_historic_states: false, - spec: E::default_spec(), - }; + let mut config = ApiTesterConfig::default(); config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); @@ -7467,6 +7993,56 @@ async fn get_blob_sidecars() { .test_get_blob_sidecars(false) .await .test_get_blob_sidecars(true) + .await + .test_get_blobs(false) + .await + .test_get_blobs(true) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_blobs_post_fulu_supernode() { + let mut config = ApiTesterConfig { + retain_historic_states: false, + spec: E::default_spec(), + node_custody_type: NodeCustodyType::Supernode, + }; + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + + ApiTester::new_from_config(config) + .await + .test_post_beacon_blocks_valid() + .await + // We can call the same get_blobs function in this test + // because the function will call get_blobs_by_versioned_hashes which handles peerDAS post-Fulu + .test_get_blobs(false) + .await + .test_get_blobs(true) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_blobs_post_fulu_full_node() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); + + ApiTester::new_from_config(config) + .await + .test_post_beacon_blocks_valid() + .await + .test_get_blobs_post_fulu_full_node(false) + .await + .test_get_blobs_post_fulu_full_node(true) .await; } @@ -7522,14 +8098,6 @@ async fn lighthouse_endpoints() { .await .test_get_lighthouse_validator_inclusion_global() .await - .test_get_lighthouse_eth1_syncing() - .await - .test_get_lighthouse_eth1_block_cache() - .await - .test_get_lighthouse_eth1_deposit_cache() - .await - .test_get_lighthouse_staking() - .await .test_post_lighthouse_database_reconstruct() .await .test_post_lighthouse_liveness() @@ -7540,7 +8108,7 @@ async fn lighthouse_endpoints() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn optimistic_responses() { - ApiTester::new_with_hard_forks(true, true) + ApiTester::new_with_hard_forks() .await .test_check_optimistic_responses() .await; @@ -7602,6 +8170,7 @@ async fn create_signed_inclusion_lists() { config.spec.capella_fork_epoch = Some(Epoch::new(0)); config.spec.deneb_fork_epoch = Some(Epoch::new(0)); config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); config.spec.eip7805_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await @@ -7617,9 +8186,17 @@ async fn test_post_validator_duties_inclusion_list() { config.spec.capella_fork_epoch = Some(Epoch::new(0)); config.spec.deneb_fork_epoch = Some(Epoch::new(0)); config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); config.spec.eip7805_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await .test_post_validator_duties_inclusion_list() .await; } + +async fn get_validator_blocks_v3_http_api_path() { + ApiTester::new() + .await + .get_validator_blocks_v3_path_graffiti_policy() + .await; +} diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index e12053ac43..b74c04a4cb 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -13,6 +13,7 @@ lighthouse_version = { workspace = true } logging = { workspace = true } malloc_utils = { workspace = true } metrics = { workspace = true } +network_utils = { workspace = true } serde = { workspace = true } slot_clock = { workspace = true } store = { workspace = true } diff --git a/beacon_node/http_metrics/src/lib.rs b/beacon_node/http_metrics/src/lib.rs index 6cbb485d71..cfa55b54eb 100644 --- a/beacon_node/http_metrics/src/lib.rs +++ b/beacon_node/http_metrics/src/lib.rs @@ -13,7 +13,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use tracing::info; -use warp::{http::Response, Filter}; +use warp::{Filter, http::Response}; #[derive(Debug)] pub enum Error { diff --git a/beacon_node/http_metrics/src/metrics.rs b/beacon_node/http_metrics/src/metrics.rs index bcfb8e4c9c..c19fa8fd3b 100644 --- a/beacon_node/http_metrics/src/metrics.rs +++ b/beacon_node/http_metrics/src/metrics.rs @@ -37,7 +37,7 @@ pub fn gather_prometheus_metrics( store::scrape_for_metrics(db_path, freezer_db_path); } - lighthouse_network::scrape_discovery_metrics(); + network_utils::discovery_metrics::scrape_discovery_metrics(); health_metrics::metrics::scrape_health_metrics(); @@ -51,10 +51,10 @@ pub fn gather_prometheus_metrics( .encode_utf8(&metrics::gather(), &mut buffer) .unwrap(); // encode gossipsub metrics also if they exist - if let Some(registry) = ctx.gossipsub_registry.as_ref() { - if let Ok(registry_locked) = registry.lock() { - let _ = encode(&mut buffer, ®istry_locked); - } + if let Some(registry) = ctx.gossipsub_registry.as_ref() + && let Ok(registry_locked) = registry.lock() + { + let _ = encode(&mut buffer, ®istry_locked); } Ok(buffer) diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs index 2de2fd96f8..2ce21a62b3 100644 --- a/beacon_node/http_metrics/tests/tests.rs +++ b/beacon_node/http_metrics/tests/tests.rs @@ -1,8 +1,8 @@ use beacon_chain::test_utils::EphemeralHarnessType; use http_metrics::Config; use logging::create_test_tracing_subscriber; -use reqwest::header::HeaderValue; use reqwest::StatusCode; +use reqwest::header::HeaderValue; use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use tokio::sync::oneshot; diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 4f1825af20..efb6f27dc5 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -3,19 +3,25 @@ name = "lighthouse_network" version = "0.2.0" authors = ["Sigma Prime "] edition = { workspace = true } +autotests = false + +[features] +libp2p-websocket = [] [dependencies] alloy-primitives = { workspace = true } alloy-rlp = { workspace = true } +bls = { workspace = true } bytes = { workspace = true } delay_map = { workspace = true } directory = { workspace = true } dirs = { workspace = true } discv5 = { workspace = true } either = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["lighthouse"] } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } fnv = { workspace = true } futures = { workspace = true } gossipsub = { workspace = true } @@ -28,8 +34,9 @@ logging = { workspace = true } lru = { workspace = true } lru_cache = { workspace = true } metrics = { workspace = true } +network_utils = { workspace = true } parking_lot = { workspace = true } -prometheus-client = "0.22.0" +prometheus-client = "0.23.0" rand = { workspace = true } regex = { workspace = true } serde = { workspace = true } @@ -40,27 +47,38 @@ ssz_types = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } task_executor = { workspace = true } -tiny-keccak = "2" tokio = { workspace = true } -tokio-io-timeout = "1" tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +typenum = { workspace = true } types = { workspace = true } unsigned-varint = { version = "0.8", features = ["codec"] } -unused_port = { workspace = true } [dependencies.libp2p] -version = "0.55" +version = "0.56" default-features = false -features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa", "metrics", "quic", "upnp"] +features = [ + "identify", + "yamux", + "noise", + "dns", + "tcp", + "tokio", + "plaintext", + "secp256k1", + "macros", + "metrics", + "quic", + "upnp", +] [dev-dependencies] async-channel = { workspace = true } logging = { workspace = true } -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } +proptest = { workspace = true } tempfile = { workspace = true } -[features] -libp2p-websocket = [] +[[test]] +name = "lighthouse_network_tests" +path = "tests/main.rs" diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 89d260569a..416ca73e08 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -1,4 +1,4 @@ -use crate::listen_addr::{ListenAddr, ListenAddress}; +use crate::peer_manager::config::DEFAULT_TARGET_PEERS; use crate::rpc::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; use crate::types::GossipKind; use crate::{Enr, PeerIdSerialized}; @@ -7,6 +7,7 @@ use directory::{ }; use libp2p::Multiaddr; use local_ip_address::local_ipv6; +use network_utils::listen_addr::{ListenAddr, ListenAddress}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -93,9 +94,6 @@ pub struct Config { /// Attempt to construct external port mappings with UPnP. pub upnp_enabled: bool, - /// Subscribe to all data column subnets for the duration of the runtime. - pub subscribe_all_data_column_subnets: bool, - /// Subscribe to all subnets for the duration of the runtime. pub subscribe_all_subnets: bool, @@ -139,6 +137,9 @@ pub struct Config { /// Configuration for the minimum message size for which IDONTWANT messages are send in the mesh. /// Lower the value reduces the optimization effect of the IDONTWANT messages. pub idontwant_message_size_threshold: usize, + + /// Flag for advertising a fake CGC to peers for testing ONLY. + pub advertise_false_custody_group_count: Option, } impl Config { @@ -338,7 +339,7 @@ impl Default for Config { enr_udp6_port: None, enr_quic6_port: None, enr_tcp6_port: None, - target_peers: 100, + target_peers: DEFAULT_TARGET_PEERS, discv5_config, boot_nodes_enr: vec![], boot_nodes_multiaddr: vec![], @@ -349,9 +350,8 @@ impl Default for Config { disable_discovery: false, disable_quic_support: false, upnp_enabled: true, - network_load: 4, + network_load: 3, private: false, - subscribe_all_data_column_subnets: false, subscribe_all_subnets: false, import_all_attestations: false, shutdown_after_sync: false, @@ -363,6 +363,7 @@ impl Default for Config { invalid_block_storage: None, inbound_rate_limiter_config: None, idontwant_message_size_threshold: DEFAULT_IDONTWANT_MESSAGE_SIZE_THRESHOLD, + advertise_false_custody_group_count: None, } } } @@ -417,8 +418,8 @@ impl From for NetworkLoad { mesh_n_low: 4, outbound_min: 3, mesh_n: 8, - mesh_n_high: 12, - gossip_lazy: 3, + mesh_n_high: 10, + gossip_lazy: 2, history_gossip: 3, heartbeat_interval: Duration::from_millis(1000), }, @@ -453,7 +454,7 @@ pub fn gossipsub_config( ) -> Vec { let topic_bytes = message.topic.as_str().as_bytes(); - if fork_context.current_fork().altair_enabled() { + if fork_context.current_fork_name().altair_enabled() { let topic_len_bytes = topic_bytes.len().to_le_bytes(); let mut vec = Vec::with_capacity( prefix.len() + topic_len_bytes.len() + topic_bytes.len() + message.data.len(), diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index e70c8047e0..4c285ea86c 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -2,13 +2,14 @@ pub use discv5::enr::CombinedKey; -use super::enr_ext::CombinedKeyExt; use super::ENR_FILENAME; -use crate::types::{Enr, EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use crate::NetworkConfig; +use crate::types::{Enr, EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use alloy_rlp::bytes::Bytes; use libp2p::identity::Keypair; use lighthouse_version::{client_name, version}; +use network_utils::enr_ext::CombinedKeyExt; +use network_utils::enr_ext::{EnrExt, QUIC_ENR_KEY, QUIC6_ENR_KEY}; use ssz::{Decode, Encode}; use ssz_types::BitVector; use std::fs::File; @@ -18,10 +19,10 @@ use std::str::FromStr; use tracing::{debug, warn}; use types::{ChainSpec, EnrForkId, EthSpec}; -use super::enr_ext::{EnrExt, QUIC6_ENR_KEY, QUIC_ENR_KEY}; - /// The ENR field specifying the fork id. pub const ETH2_ENR_KEY: &str = "eth2"; +/// The ENR field specifying the next fork digest. +pub const NEXT_FORK_DIGEST_ENR_KEY: &str = "nfd"; /// The ENR field specifying the attestation subnet bitfield. pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets"; /// The ENR field specifying the sync committee subnet bitfield. @@ -42,6 +43,9 @@ pub trait Eth2Enr { /// The peerdas custody group count associated with the ENR. fn custody_group_count(&self, spec: &ChainSpec) -> Result; + /// The next fork digest associated with the ENR. + fn next_fork_digest(&self) -> Result<[u8; 4], &'static str>; + fn eth2(&self) -> Result; } @@ -81,6 +85,12 @@ impl Eth2Enr for Enr { } } + fn next_fork_digest(&self) -> Result<[u8; 4], &'static str> { + self.get_decodable::<[u8; 4]>(NEXT_FORK_DIGEST_ENR_KEY) + .ok_or("ENR next fork digest non-existent")? + .map_err(|_| "Could not decode the ENR next fork digest") + } + fn eth2(&self) -> Result { let eth2_bytes: Bytes = self .get_decodable(ETH2_ENR_KEY) @@ -149,13 +159,22 @@ pub fn build_or_load_enr( local_key: Keypair, config: &NetworkConfig, enr_fork_id: &EnrForkId, + custody_group_count: u64, + next_fork_digest: [u8; 4], spec: &ChainSpec, ) -> Result { // Build the local ENR. // Note: Discovery should update the ENR record's IP to the external IP as seen by the // majority of our peers, if the CLI doesn't expressly forbid it. let enr_key = CombinedKey::from_libp2p(local_key)?; - let mut local_enr = build_enr::(&enr_key, config, enr_fork_id, spec)?; + let mut local_enr = build_enr::( + &enr_key, + config, + enr_fork_id, + custody_group_count, + next_fork_digest, + spec, + )?; use_or_load_enr(&enr_key, &mut local_enr, config)?; Ok(local_enr) @@ -166,6 +185,8 @@ pub fn build_enr( enr_key: &CombinedKey, config: &NetworkConfig, enr_fork_id: &EnrForkId, + custody_group_count: u64, + next_fork_digest: [u8; 4], spec: &ChainSpec, ) -> Result { let mut builder = discv5::enr::Enr::builder(); @@ -257,14 +278,10 @@ pub fn build_enr( &bitfield.as_ssz_bytes().into(), ); - // only set `cgc` if PeerDAS fork epoch has been scheduled + // only set `cgc` and `nfd` if PeerDAS fork (Fulu) epoch has been scheduled if spec.is_peer_das_scheduled() { - let custody_group_count = if config.subscribe_all_data_column_subnets { - spec.number_of_custody_groups - } else { - spec.custody_requirement - }; builder.add_value(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY, &custody_group_count); + builder.add_value(NEXT_FORK_DIGEST_ENR_KEY, &next_fork_digest); } builder @@ -337,6 +354,7 @@ mod test { use types::{Epoch, MainnetEthSpec}; type E = MainnetEthSpec; + const TEST_NFD: [u8; 4] = [0x01, 0x02, 0x03, 0x04]; fn make_fulu_spec() -> ChainSpec { let mut spec = E::default_spec(); @@ -344,48 +362,38 @@ mod test { spec } - fn build_enr_with_config(config: NetworkConfig, spec: &ChainSpec) -> (Enr, CombinedKey) { + fn build_enr_with_config( + config: NetworkConfig, + cgc: u64, + spec: &ChainSpec, + ) -> (Enr, CombinedKey) { let keypair = libp2p::identity::secp256k1::Keypair::generate(); let enr_key = CombinedKey::from_secp256k1(&keypair); let enr_fork_id = EnrForkId::default(); - let enr = build_enr::(&enr_key, &config, &enr_fork_id, spec).unwrap(); + let enr = build_enr::(&enr_key, &config, &enr_fork_id, cgc, TEST_NFD, spec).unwrap(); (enr, enr_key) } #[test] - fn custody_group_count_default() { - let config = NetworkConfig { - subscribe_all_data_column_subnets: false, - ..NetworkConfig::default() - }; + fn test_nfd_enr_encoding() { let spec = make_fulu_spec(); - - let enr = build_enr_with_config(config, &spec).0; - - assert_eq!( - enr.custody_group_count::(&spec).unwrap(), - spec.custody_requirement, - ); + let enr = + build_enr_with_config(NetworkConfig::default(), spec.custody_requirement, &spec).0; + assert_eq!(enr.next_fork_digest().unwrap(), TEST_NFD); } #[test] - fn custody_group_count_all() { - let config = NetworkConfig { - subscribe_all_data_column_subnets: true, - ..NetworkConfig::default() - }; + fn custody_group_value() { + let config = NetworkConfig::default(); let spec = make_fulu_spec(); - let enr = build_enr_with_config(config, &spec).0; + let enr = build_enr_with_config(config, 42, &spec).0; - assert_eq!( - enr.custody_group_count::(&spec).unwrap(), - spec.number_of_custody_groups, - ); + assert_eq!(enr.custody_group_count::(&spec).unwrap(), 42); } #[test] fn test_encode_decode_eth2_enr() { - let (enr, _key) = build_enr_with_config(NetworkConfig::default(), &E::default_spec()); + let (enr, _key) = build_enr_with_config(NetworkConfig::default(), 4, &E::default_spec()); // Check all Eth2 Mappings are decodeable enr.eth2().unwrap(); enr.attestation_bitfield::().unwrap(); diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index ad54c6b8b1..a8c87523a5 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -4,16 +4,15 @@ //! queries and manages access to the discovery routing table. pub(crate) mod enr; -pub mod enr_ext; // Allow external use of the lighthouse ENR builder use crate::service::TARGET_SUBNET_PEERS; -use crate::{metrics, ClearDialError}; +use crate::{ClearDialError, metrics}; use crate::{Enr, NetworkConfig, NetworkGlobals, Subnet, SubnetDiscovery}; -use discv5::{enr::NodeId, Discv5}; -pub use enr::{build_enr, load_enr_from_disk, use_or_load_enr, CombinedKey, Eth2Enr}; -pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt}; +use discv5::{Discv5, enr::NodeId}; +pub use enr::{CombinedKey, Eth2Enr, build_enr, load_enr_from_disk, use_or_load_enr}; pub use libp2p::identity::{Keypair, PublicKey}; +use network_utils::enr_ext::{CombinedKeyExt, EnrExt, peer_id_to_node_id}; use alloy_rlp::bytes::Bytes; use enr::{ATTESTATION_BITFIELD_ENR_KEY, ETH2_ENR_KEY, SYNC_COMMITTEE_BITFIELD_ENR_KEY}; @@ -21,18 +20,19 @@ use futures::prelude::*; use futures::stream::FuturesUnordered; use libp2p::core::transport::PortUse; use libp2p::multiaddr::Protocol; -use libp2p::swarm::behaviour::{DialFailure, FromSwarm}; use libp2p::swarm::THandlerInEvent; +use libp2p::swarm::behaviour::{DialFailure, FromSwarm}; pub use libp2p::{ - core::{transport::ListenerId, ConnectedPoint, Multiaddr}, + core::{ConnectedPoint, Multiaddr, transport::ListenerId}, identity::PeerId, swarm::{ - dummy::ConnectionHandler, ConnectionId, DialError, NetworkBehaviour, NotifyHandler, - SubstreamProtocol, ToSwarm, + ConnectionId, DialError, NetworkBehaviour, NotifyHandler, SubstreamProtocol, ToSwarm, + dummy::ConnectionHandler, }, }; use logging::crit; use lru::LruCache; +use network_utils::discovery_metrics; use ssz::Encode; use std::num::NonZeroUsize; use std::{ @@ -49,6 +49,7 @@ use tracing::{debug, error, info, trace, warn}; use types::{ChainSpec, EnrForkId, EthSpec}; mod subnet_predicate; +use crate::discovery::enr::{NEXT_FORK_DIGEST_ENR_KEY, PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY}; pub use subnet_predicate::subnet_predicate; use types::non_zero_usize::new_non_zero_usize; @@ -476,6 +477,15 @@ impl Discovery { Ok(()) } + pub fn update_enr_cgc(&mut self, custody_group_count: u64) -> Result<(), String> { + self.discv5 + .enr_insert(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY, &custody_group_count) + .map_err(|e| format!("{:?}", e))?; + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + Ok(()) + } + /// Adds/Removes a subnet from the ENR attnets/syncnets Bitfield pub fn update_enr_bitfield(&mut self, subnet: Subnet, value: bool) -> Result<(), String> { let local_enr = self.discv5.local_enr(); @@ -560,6 +570,19 @@ impl Discovery { Ok(()) } + pub fn update_enr_nfd(&mut self, nfd: [u8; 4]) -> Result<(), String> { + self.discv5 + .enr_insert::(NEXT_FORK_DIGEST_ENR_KEY, &nfd.as_ssz_bytes().into()) + .map_err(|e| format!("{:?}", e))?; + info!( + next_fork_digest = ?nfd, + "Updating the ENR nfd" + ); + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr()); + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + Ok(()) + } + /// Updates the `eth2` field of our local ENR. pub fn update_eth2_enr(&mut self, enr_fork_id: EnrForkId) { // to avoid having a reference to the spec constant, for the logging we assume @@ -664,7 +687,10 @@ impl Discovery { min_ttl, retries, }); - metrics::set_gauge(&metrics::DISCOVERY_QUEUE, self.queued_queries.len() as i64); + metrics::set_gauge( + &discovery_metrics::DISCOVERY_QUEUE, + self.queued_queries.len() as i64, + ); } } @@ -699,7 +725,10 @@ impl Discovery { } } // Update the queue metric - metrics::set_gauge(&metrics::DISCOVERY_QUEUE, self.queued_queries.len() as i64); + metrics::set_gauge( + &discovery_metrics::DISCOVERY_QUEUE, + self.queued_queries.len() as i64, + ); processed } @@ -1109,7 +1138,10 @@ impl NetworkBehaviour for Discovery { self.update_enr_quic_port(port, false) } _ => { - debug!(?addr, "Encountered unacceptable multiaddr for listening (unsupported transport)"); + debug!( + ?addr, + "Encountered unacceptable multiaddr for listening (unsupported transport)" + ); return; } }, @@ -1131,7 +1163,10 @@ impl NetworkBehaviour for Discovery { self.update_enr_quic_port(port, true) } _ => { - debug!(?addr, "Encountered unacceptable multiaddr for listening (unsupported transport)"); + debug!( + ?addr, + "Encountered unacceptable multiaddr for listening (unsupported transport)" + ); return; } }, @@ -1194,9 +1229,10 @@ impl Discovery { #[cfg(test)] mod tests { use super::*; - use crate::rpc::methods::{MetaData, MetaDataV2}; + use crate::rpc::methods::{MetaData, MetaDataV3}; use libp2p::identity::secp256k1; - use types::{BitVector, MinimalEthSpec, SubnetId}; + use ssz_types::BitVector; + use types::{MinimalEthSpec, SubnetId}; type E = MinimalEthSpec; @@ -1204,16 +1240,27 @@ mod tests { let spec = Arc::new(ChainSpec::default()); let keypair = secp256k1::Keypair::generate(); let mut config = NetworkConfig::default(); - config.set_listening_addr(crate::ListenAddress::unused_v4_ports()); + config.set_listening_addr(network_utils::listen_addr::ListenAddress::unused_v4_ports()); let config = Arc::new(config); let enr_key: CombinedKey = CombinedKey::from_secp256k1(&keypair); - let enr: Enr = build_enr::(&enr_key, &config, &EnrForkId::default(), &spec).unwrap(); + let next_fork_digest = [0; 4]; + let custody_group_count = spec.custody_requirement; + let enr: Enr = build_enr::( + &enr_key, + &config, + &EnrForkId::default(), + custody_group_count, + next_fork_digest, + &spec, + ) + .unwrap(); let globals = NetworkGlobals::new( enr, - MetaData::V2(MetaDataV2 { + MetaData::V3(MetaDataV3 { seq_number: 0, attnets: Default::default(), syncnets: Default::default(), + custody_group_count, }), vec![], false, diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 735ef5b0f2..6e841c25a5 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -3,8 +3,8 @@ use super::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use std::ops::Deref; use tracing::trace; -use types::data_column_custody_group::compute_subnets_for_node; use types::ChainSpec; +use types::data_column_custody_group::compute_subnets_for_node; /// Returns the predicate for a given subnet. pub fn subnet_predicate( @@ -35,7 +35,7 @@ where .is_ok_and(|b| b.get(*s.deref() as usize).unwrap_or(false)), Subnet::DataColumn(s) => { if let Ok(custody_group_count) = enr.custody_group_count::(&spec) { - compute_subnets_for_node(enr.node_id().raw(), custody_group_count, &spec) + compute_subnets_for_node::(enr.node_id().raw(), custody_group_count, &spec) .is_ok_and(|subnets| subnets.contains(s)) } else { false diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index 40fdd71b38..3d96a08357 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -6,16 +6,14 @@ mod config; pub mod service; pub mod discovery; -pub mod listen_addr; pub mod metrics; pub mod peer_manager; pub mod rpc; pub mod types; use libp2p::swarm::DialError; -pub use listen_addr::*; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::str::FromStr; /// Wrapper over a libp2p `PeerId` which implements `Serialize` and `Deserialize` @@ -63,7 +61,7 @@ impl<'de> Deserialize<'de> for PeerIdSerialized { struct ClearDialError<'a>(&'a DialError); impl ClearDialError<'_> { - fn most_inner_error(err: &(dyn std::error::Error)) -> &(dyn std::error::Error) { + fn most_inner_error(err: &dyn std::error::Error) -> &dyn std::error::Error { let mut current = err; while let Some(source) = current.source() { current = source; @@ -107,18 +105,17 @@ pub use crate::types::{ pub use prometheus_client; pub use config::Config as NetworkConfig; -pub use discovery::{CombinedKeyExt, EnrExt, Eth2Enr}; +pub use discovery::Eth2Enr; pub use discv5; pub use gossipsub::{IdentTopic, MessageAcceptance, MessageId, Topic, TopicHash}; pub use libp2p; -pub use libp2p::{core::ConnectedPoint, PeerId, Swarm}; -pub use libp2p::{multiaddr, Multiaddr}; -pub use metrics::scrape_discovery_metrics; +pub use libp2p::{Multiaddr, identity, multiaddr}; +pub use libp2p::{PeerId, Swarm, core::ConnectedPoint}; pub use peer_manager::{ + ConnectionDirection, PeerConnectionStatus, PeerInfo, PeerManager, SyncInfo, SyncStatus, + peerdb::PeerDB, peerdb::client::Client, peerdb::score::{PeerAction, ReportSource}, - peerdb::PeerDB, - ConnectionDirection, PeerConnectionStatus, PeerInfo, PeerManager, SyncInfo, SyncStatus, }; // pub use service::{load_private_key, Context, Libp2pEvent, Service, NETWORK_KEY_FILENAME}; pub use service::api_types::Response; diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index da986f2884..623d43a727 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -1,14 +1,6 @@ pub use metrics::*; use std::sync::LazyLock; -pub static NAT_OPEN: LazyLock> = LazyLock::new(|| { - try_create_int_gauge_vec( - "nat_open", - "An estimate indicating if the local node is reachable from external nodes", - &["protocol"], - ) -}); - pub static ADDRESS_UPDATE_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter( "libp2p_address_update_total", @@ -53,31 +45,6 @@ pub static PEER_DISCONNECT_EVENT_COUNT: LazyLock> = LazyLock: "Count of libp2p peer disconnect events", ) }); -pub static DISCOVERY_BYTES: LazyLock> = LazyLock::new(|| { - try_create_int_gauge_vec( - "discovery_bytes", - "The number of bytes sent and received in discovery", - &["direction"], - ) -}); -pub static DISCOVERY_QUEUE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "discovery_queue_size", - "The number of discovery queries awaiting execution", - ) -}); -pub static DISCOVERY_REQS: LazyLock> = LazyLock::new(|| { - try_create_float_gauge( - "discovery_requests", - "The number of unsolicited discovery requests per second", - ) -}); -pub static DISCOVERY_SESSIONS: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "discovery_sessions", - "The number of active discovery sessions with peers", - ) -}); pub static DISCOVERY_NO_USEFUL_ENRS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "discovery_no_useful_enrs_found", @@ -219,14 +186,3 @@ pub static RESPONSE_IDLING: LazyLock> = LazyLock::new(|| { "The time our response remained idle in the response limiter", ) }); - -pub fn scrape_discovery_metrics() { - let metrics = - discv5::metrics::Metrics::from(discv5::Discv5::::raw_metrics()); - set_float_gauge(&DISCOVERY_REQS, metrics.unsolicited_requests_per_second); - set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); - set_gauge_vec(&DISCOVERY_BYTES, &["inbound"], metrics.bytes_recv as i64); - set_gauge_vec(&DISCOVERY_BYTES, &["outbound"], metrics.bytes_sent as i64); - set_gauge_vec(&NAT_OPEN, &["discv5_ipv4"], metrics.ipv4_contactable as i64); - set_gauge_vec(&NAT_OPEN, &["discv5_ipv6"], metrics.ipv6_contactable as i64); -} diff --git a/beacon_node/lighthouse_network/src/peer_manager/config.rs b/beacon_node/lighthouse_network/src/peer_manager/config.rs index d2fc7a8abd..b2ed652486 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/config.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/config.rs @@ -8,7 +8,7 @@ pub const DEFAULT_PING_INTERVAL_OUTBOUND: u64 = 15; pub const DEFAULT_PING_INTERVAL_INBOUND: u64 = 20; /// Default number of peers to connect to. -pub const DEFAULT_TARGET_PEERS: usize = 50; +pub const DEFAULT_TARGET_PEERS: usize = 200; /// Configurations for the PeerManager. #[derive(Debug)] diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 01cc161105..3cfe2b3c3b 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -1,10 +1,8 @@ //! Implementation of Lighthouse's peer management system. -use crate::discovery::enr_ext::EnrExt; -use crate::discovery::peer_id_to_node_id; use crate::rpc::{GoodbyeReason, MetaData, Protocol, RPCError, RpcErrorResponse}; use crate::service::TARGET_SUBNET_PEERS; -use crate::{metrics, Gossipsub, NetworkGlobals, PeerId, Subnet, SubnetDiscovery}; +use crate::{Gossipsub, NetworkGlobals, PeerId, Subnet, SubnetDiscovery, metrics}; use delay_map::HashSetDelay; use discv5::Enr; use libp2p::identify::Info as IdentifyInfo; @@ -17,7 +15,7 @@ use std::{ time::{Duration, Instant}, }; use tracing::{debug, error, trace, warn}; -use types::{DataColumnSubnetId, EthSpec, SyncSubnetId}; +use types::{DataColumnSubnetId, EthSpec, SubnetId, SyncSubnetId}; pub use libp2p::core::Multiaddr; pub use libp2p::identity::Keypair; @@ -25,19 +23,28 @@ pub use libp2p::identity::Keypair; pub mod peerdb; use crate::peer_manager::peerdb::client::ClientKind; +use crate::types::GossipKind; use libp2p::multiaddr; -pub use peerdb::peer_info::{ - ConnectionDirection, PeerConnectionStatus, PeerConnectionStatus::*, PeerInfo, -}; +use network_utils::discovery_metrics; +use network_utils::enr_ext::{EnrExt, peer_id_to_node_id}; +pub use peerdb::peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use peerdb::score::{PeerAction, ReportSource}; pub use peerdb::sync_status::{SyncInfo, SyncStatus}; -use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::collections::{HashMap, HashSet, hash_map::Entry}; use std::net::IpAddr; use strum::IntoEnumIterator; use types::data_column_custody_group::{ - compute_subnets_from_custody_group, get_custody_groups, CustodyIndex, + CustodyIndex, compute_subnets_from_custody_group, get_custody_groups, }; +/// Unified peer subnet information structure for pruning logic. +struct PeerSubnetInfo { + info: PeerInfo, + attestation_subnets: HashSet, + sync_committees: HashSet, + custody_subnets: HashSet, +} + pub mod config; mod network_behaviour; @@ -52,6 +59,8 @@ pub const PEER_RECONNECTION_TIMEOUT: Duration = Duration::from_secs(600); /// lower our peer count below this number. Instead we favour a non-uniform distribution of subnet /// peers. pub const MIN_SYNC_COMMITTEE_PEERS: u64 = 2; +/// Avoid pruning sampling peers if subnet peer count is below this number. +pub const MIN_SAMPLING_COLUMN_SUBNET_PEERS: u64 = 2; /// A fraction of `PeerManager::target_peers` that we allow to connect to us in excess of /// `PeerManager::target_peers`. For clarity, if `PeerManager::target_peers` is 50 and /// PEER_EXCESS_FACTOR = 0.1 we allow 10% more nodes, i.e 55. @@ -161,16 +170,18 @@ impl PeerManager { } = cfg; // Set up the peer manager heartbeat interval - let heartbeat = tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL)); + let heartbeat = tokio::time::interval(Duration::from_secs(HEARTBEAT_INTERVAL)); // Compute subnets for all custody groups let subnets_by_custody_group = if network_globals.spec.is_peer_das_scheduled() { (0..network_globals.spec.number_of_custody_groups) .map(|custody_index| { - let subnets = - compute_subnets_from_custody_group(custody_index, &network_globals.spec) - .expect("Should compute subnets for all custody groups") - .collect(); + let subnets = compute_subnets_from_custody_group::( + custody_index, + &network_globals.spec, + ) + .expect("Should compute subnets for all custody groups") + .collect(); (custody_index, subnets) }) .collect::>>() @@ -727,7 +738,16 @@ impl PeerManager { } } else { // we have no meta-data for this peer, update - debug!(%peer_id, new_seq_no = meta_data.seq_number(), "Obtained peer's metadata"); + let cgc = meta_data + .custody_group_count() + .map(|&count| count.to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + debug!( + %peer_id, + new_seq_no = meta_data.seq_number(), + cgc, + "Obtained peer's metadata" + ); } let known_custody_group_count = peer_info @@ -947,6 +967,43 @@ impl PeerManager { } } + /// Run discovery query for additional custody peers if we fall below `MIN_SAMPLING_COLUMN_SUBNET_PEERS`. + fn maintain_custody_peers(&mut self) { + let subnets_to_discover: Vec = self + .network_globals + .sampling_subnets() + .iter() + .filter_map(|custody_subnet| { + if self + .network_globals + .peers + .read() + .has_good_peers_in_custody_subnet( + custody_subnet, + MIN_SAMPLING_COLUMN_SUBNET_PEERS as usize, + ) + { + None + } else { + Some(SubnetDiscovery { + subnet: Subnet::DataColumn(*custody_subnet), + min_ttl: None, + }) + } + }) + .collect(); + + // request the subnet query from discovery + if !subnets_to_discover.is_empty() { + debug!( + subnets = ?subnets_to_discover.iter().map(|s| s.subnet).collect::>(), + "Making subnet queries for maintaining custody peers" + ); + self.events + .push(PeerManagerEvent::DiscoverSubnetPeers(subnets_to_discover)); + } + } + fn maintain_trusted_peers(&mut self) { let trusted_peers = self.trusted_peers.clone(); for trusted_peer in trusted_peers { @@ -989,9 +1046,204 @@ impl PeerManager { } } + /// Build unified peer subnet information from connected peers. + /// + /// This creates a unified structure containing all subnet information for each peer, + /// excluding trusted peers and peers already marked for pruning. + fn build_peer_subnet_info( + &self, + peers_to_prune: &HashSet, + ) -> HashMap> { + let mut peer_subnet_info: HashMap> = HashMap::new(); + + for (peer_id, info) in self.network_globals.peers.read().connected_peers() { + // Ignore peers we trust or that we are already pruning + if info.is_trusted() || peers_to_prune.contains(peer_id) { + continue; + } + + let mut peer_info = PeerSubnetInfo { + info: info.clone(), + attestation_subnets: HashSet::new(), + sync_committees: HashSet::new(), + custody_subnets: HashSet::new(), + }; + + // Populate subnet information from long-lived subnets + for subnet in info.long_lived_subnets() { + match subnet { + Subnet::Attestation(subnet_id) => { + peer_info.attestation_subnets.insert(subnet_id); + } + Subnet::SyncCommittee(id) => { + peer_info.sync_committees.insert(id); + } + Subnet::DataColumn(id) => { + peer_info.custody_subnets.insert(id); + } + } + } + + peer_subnet_info.insert(*peer_id, peer_info); + } + + peer_subnet_info + } + + /// Build reverse lookup from custody subnets to peer lists. + fn build_custody_subnet_lookup( + peer_subnet_info: &HashMap>, + ) -> HashMap> { + let mut custody_subnet_to_peers: HashMap> = HashMap::new(); + + for (peer_id, peer_info) in peer_subnet_info { + for &custody_subnet in &peer_info.custody_subnets { + custody_subnet_to_peers + .entry(custody_subnet) + .or_default() + .push(*peer_id); + } + } + + custody_subnet_to_peers + } + + /// Determine if a peer should be protected from pruning based on various criteria. + /// + /// Protection criteria: + /// - Outbound peers: don't prune if it would drop below target outbound peer count + /// - Data column sampling: ≤ MIN_SAMPLING_COLUMN_SUBNET_PEERS (2) peers per subnet + /// - Sync committees: ≤ MIN_SYNC_COMMITTEE_PEERS (2) peers per committee + /// - Attestation subnets: protect peers on the scarcest attestation subnets + /// + /// Returns true if the peer should be protected (not pruned). + fn should_protect_peer( + &self, + candidate_info: &PeerSubnetInfo, + sampling_subnets: &HashSet, + custody_subnet_to_peers: &HashMap>, + peer_subnet_info: &HashMap>, + connected_outbound_peer_count: usize, + outbound_peers_pruned: usize, + ) -> bool { + // Ensure we don't remove too many outbound peers + if candidate_info.info.is_outbound_only() + && self.target_outbound_peers() + >= connected_outbound_peer_count.saturating_sub(outbound_peers_pruned) + { + return true; + } + + // Check data column sampling subnets + // If the peer exists in a sampling subnet that is less than or equal to MIN_SAMPLING_COLUMN_SUBNET_PEERS, we keep it + let should_protect_sampling = candidate_info + .custody_subnets + .iter() + .filter(|subnet| sampling_subnets.contains(subnet)) + .any(|subnet| { + let count = custody_subnet_to_peers + .get(subnet) + .map(|peers| peers.len()) + .unwrap_or(0); + count <= MIN_SAMPLING_COLUMN_SUBNET_PEERS as usize + }); + + if should_protect_sampling { + return true; + } + + // Check sync committee protection + let should_protect_sync = candidate_info.sync_committees.iter().any(|sync_committee| { + let count = peer_subnet_info + .values() + .filter(|p| p.sync_committees.contains(sync_committee)) + .count(); + count <= MIN_SYNC_COMMITTEE_PEERS as usize + }); + + if should_protect_sync { + return true; + } + + // Check attestation subnet to avoid pruning from subnets with the lowest peer count + let attestation_subnet_counts: HashMap = peer_subnet_info + .values() + .flat_map(|p| &p.attestation_subnets) + .fold(HashMap::new(), |mut acc, &subnet| { + *acc.entry(subnet).or_insert(0) += 1; + acc + }); + + if let Some(&least_dense_size) = attestation_subnet_counts.values().min() { + let is_on_least_dense = candidate_info + .attestation_subnets + .iter() + .any(|subnet| attestation_subnet_counts.get(subnet) == Some(&least_dense_size)); + + if is_on_least_dense { + return true; + } + } + + false + } + + /// Find the best candidate for removal from the densest custody subnet. + /// + /// Returns the PeerId of the candidate to remove, or None if no suitable candidate found. + fn find_prune_candidate( + &self, + column_subnet: DataColumnSubnetId, + column_subnet_to_peers: &HashMap>, + peer_subnet_info: &HashMap>, + sampling_subnets: &HashSet, + connected_outbound_peer_count: usize, + outbound_peers_pruned: usize, + ) -> Option { + let peers_on_subnet_clone = column_subnet_to_peers.get(&column_subnet)?.clone(); + + // Create a sorted list of peers prioritized for removal + let mut sorted_peers = peers_on_subnet_clone; + sorted_peers.shuffle(&mut rand::rng()); + sorted_peers.sort_by_key(|peer_id| { + if let Some(peer_info) = peer_subnet_info.get(peer_id) { + ( + peer_info.info.custody_subnet_count(), + peer_info.info.is_synced_or_advanced(), + ) + } else { + (0, false) + } + }); + + // Try and find a candidate peer to remove from the subnet + for candidate_peer in &sorted_peers { + let Some(candidate_info) = peer_subnet_info.get(candidate_peer) else { + continue; + }; + + // Check if this peer should be protected + if self.should_protect_peer( + candidate_info, + sampling_subnets, + column_subnet_to_peers, + peer_subnet_info, + connected_outbound_peer_count, + outbound_peers_pruned, + ) { + continue; + } + + // Found a suitable candidate + return Some(*candidate_peer); + } + + None + } + /// Remove excess peers back down to our target values. /// This prioritises peers with a good score and uniform distribution of peers across - /// subnets. + /// data column subnets. /// /// The logic for the peer pruning is as follows: /// @@ -1021,9 +1273,12 @@ impl PeerManager { /// Prune peers in the following order: /// 1. Remove worst scoring peers /// 2. Remove peers that are not subscribed to a subnet (they have less value) - /// 3. Remove peers that we have many on any particular subnet - /// 4. Randomly remove peers if all the above are satisfied - /// + /// 3. Remove peers that we have many on any particular subnet, with some exceptions + /// - Don't remove peers needed for data column sampling (≥ MIN_SAMPLING_COLUMN_SUBNET_PEERS) + /// - Don't remove peers needed for sync committees (>=MIN_SYNC_COMMITTEE_PEERS) + /// - Don't remove peers from the lowest density attestation subnets + /// 4. Randomly remove peers if all the above are satisfied until we reach `target_peers`, or + /// until we can't prune any more peers due to the above constraints. fn prune_excess_peers(&mut self) { // The current number of connected peers. let connected_peer_count = self.network_globals.connected_peers(); @@ -1033,7 +1288,7 @@ impl PeerManager { } // Keep a list of peers we are pruning. - let mut peers_to_prune = std::collections::HashSet::new(); + let mut peers_to_prune = HashSet::new(); let connected_outbound_peer_count = self.network_globals.connected_outbound_only_peers(); // Keep track of the number of outbound peers we are pruning. @@ -1085,146 +1340,57 @@ impl PeerManager { prune_peers!(|info: &PeerInfo| { !info.has_long_lived_subnet() }); } - // 3. and 4. Remove peers that are too grouped on any given subnet. If all subnets are + // 3. and 4. Remove peers that are too grouped on any given data column subnet. If all subnets are // uniformly distributed, remove random peers. if peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { - // Of our connected peers, build a map from subnet_id -> Vec<(PeerId, PeerInfo)> - let mut subnet_to_peer: HashMap)>> = HashMap::new(); - // These variables are used to track if a peer is in a long-lived sync-committee as we - // may wish to retain this peer over others when pruning. - let mut sync_committee_peer_count: HashMap = HashMap::new(); - let mut peer_to_sync_committee: HashMap< - PeerId, - std::collections::HashSet, - > = HashMap::new(); + let sampling_subnets = self.network_globals.sampling_subnets(); + let mut peer_subnet_info = self.build_peer_subnet_info(&peers_to_prune); + let mut custody_subnet_to_peers = Self::build_custody_subnet_lookup(&peer_subnet_info); - for (peer_id, info) in self.network_globals.peers.read().connected_peers() { - // Ignore peers we trust or that we are already pruning - if info.is_trusted() || peers_to_prune.contains(peer_id) { - continue; - } - - // Count based on long-lived subnets not short-lived subnets - // NOTE: There are only 4 sync committees. These are likely to be denser than the - // subnets, so our priority here to make the subnet peer count uniform, ignoring - // the dense sync committees. - for subnet in info.long_lived_subnets() { - match subnet { - Subnet::Attestation(_) => { - subnet_to_peer - .entry(subnet) - .or_default() - .push((*peer_id, info.clone())); - } - Subnet::SyncCommittee(id) => { - *sync_committee_peer_count.entry(id).or_default() += 1; - peer_to_sync_committee - .entry(*peer_id) - .or_default() - .insert(id); - } - // TODO(das) to be implemented. We're not pruning data column peers yet - // because data column topics are subscribed as core topics until we - // implement recomputing data column subnets. - Subnet::DataColumn(_) => {} - } - } - } - - // Add to the peers to prune mapping + // Attempt to prune peers to `target_peers`, or until we run out of peers to prune. while peers_to_prune.len() < connected_peer_count.saturating_sub(self.target_peers) { - if let Some((_, peers_on_subnet)) = subnet_to_peer - .iter_mut() + let custody_subnet_with_most_peers = custody_subnet_to_peers + .iter() + .filter(|(_, peers)| !peers.is_empty()) .max_by_key(|(_, peers)| peers.len()) - { - // and the subnet still contains peers - if !peers_on_subnet.is_empty() { - // Order the peers by the number of subnets they are long-lived - // subscribed too, shuffle equal peers. - peers_on_subnet.shuffle(&mut rand::thread_rng()); - peers_on_subnet.sort_by_key(|(_, info)| info.long_lived_subnet_count()); + .map(|(subnet_id, _)| *subnet_id); - // Try and find a candidate peer to remove from the subnet. - // We ignore peers that would put us below our target outbound peers - // and we currently ignore peers that would put us below our - // sync-committee threshold, if we can avoid it. - - let mut removed_peer_index = None; - for (index, (candidate_peer, info)) in peers_on_subnet.iter().enumerate() { - // Ensure we don't remove too many outbound peers - if info.is_outbound_only() - && self.target_outbound_peers() - >= connected_outbound_peer_count - .saturating_sub(outbound_peers_pruned) - { - // Restart the main loop with the outbound peer removed from - // the list. This will lower the peers per subnet count and - // potentially a new subnet may be chosen to remove peers. This - // can occur recursively until we have no peers left to choose - // from. - continue; - } - - // Check the sync committee - if let Some(subnets) = peer_to_sync_committee.get(candidate_peer) { - // The peer is subscribed to some long-lived sync-committees - // Of all the subnets this peer is subscribed too, the minimum - // peer count of all of them is min_subnet_count - if let Some(min_subnet_count) = subnets - .iter() - .filter_map(|v| sync_committee_peer_count.get(v).copied()) - .min() - { - // If the minimum count is our target or lower, we - // shouldn't remove this peer, because it drops us lower - // than our target - if min_subnet_count <= MIN_SYNC_COMMITTEE_PEERS { - // Do not drop this peer in this pruning interval - continue; - } - } - } - - if info.is_outbound_only() { - outbound_peers_pruned += 1; - } - // This peer is suitable to be pruned - removed_peer_index = Some(index); - break; + if let Some(densest_subnet) = custody_subnet_with_most_peers { + // If we have successfully found a candidate peer to prune, prune it, + // otherwise all peers on this subnet should not be removed due to our + // outbound limit or min_subnet_count. In this case, we remove all + // peers from the pruning logic and try another subnet. + if let Some(candidate_peer) = self.find_prune_candidate( + densest_subnet, + &custody_subnet_to_peers, + &peer_subnet_info, + &sampling_subnets, + connected_outbound_peer_count, + outbound_peers_pruned, + ) { + // Update outbound peer count if needed + if let Some(candidate_info) = peer_subnet_info.get(&candidate_peer) + && candidate_info.info.is_outbound_only() + { + outbound_peers_pruned += 1; } - // If we have successfully found a candidate peer to prune, prune it, - // otherwise all peers on this subnet should not be removed due to our - // outbound limit or min_subnet_count. In this case, we remove all - // peers from the pruning logic and try another subnet. - if let Some(index) = removed_peer_index { - let (candidate_peer, _) = peers_on_subnet.remove(index); - // Remove pruned peers from other subnet counts - for subnet_peers in subnet_to_peer.values_mut() { - subnet_peers.retain(|(peer_id, _)| peer_id != &candidate_peer); - } - // Remove pruned peers from all sync-committee counts - if let Some(known_sync_committes) = - peer_to_sync_committee.get(&candidate_peer) - { - for sync_committee in known_sync_committes { - if let Some(sync_committee_count) = - sync_committee_peer_count.get_mut(sync_committee) - { - *sync_committee_count = - sync_committee_count.saturating_sub(1); - } - } - } - peers_to_prune.insert(candidate_peer); - } else { - peers_on_subnet.clear(); + // Remove the candidate peer from the maps, so we don't account for them + // when finding the next prune candidate. + for subnet_peers in custody_subnet_to_peers.values_mut() { + subnet_peers.retain(|peer_id| peer_id != &candidate_peer); } - continue; + peer_subnet_info.remove(&candidate_peer); + + peers_to_prune.insert(candidate_peer); + } else if let Some(peers) = custody_subnet_to_peers.get_mut(&densest_subnet) { + // If we can't find a prune candidate in this subnet, remove peers in this subnet + peers.clear() } + } else { + // If there are no peers left to prune, exit. + break; } - // If there are no peers left to prune exit. - break; } } @@ -1269,6 +1435,17 @@ impl PeerManager { // Update peer score metrics; self.update_peer_score_metrics(); + // Maintain minimum count for custody peers if we are subscribed to any data column topics (i.e. PeerDAS activated) + let peerdas_enabled = self + .network_globals + .gossipsub_subscriptions + .read() + .iter() + .any(|topic| matches!(topic.kind(), &GossipKind::DataColumnSidecar(_))); + if peerdas_enabled { + self.maintain_custody_peers(); + } + // Maintain minimum count for sync committee peers. self.maintain_sync_committee_peers(); @@ -1418,16 +1595,16 @@ impl PeerManager { // Set ipv4 nat_open metric flag if threshold of peercount is met, unset if below threshold if inbound_ipv4_peers_connected >= LIBP2P_NAT_OPEN_THRESHOLD { - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv4"], 1); + metrics::set_gauge_vec(&discovery_metrics::NAT_OPEN, &["libp2p_ipv4"], 1); } else { - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv4"], 0); + metrics::set_gauge_vec(&discovery_metrics::NAT_OPEN, &["libp2p_ipv4"], 0); } // Set ipv6 nat_open metric flag if threshold of peercount is met, unset if below threshold if inbound_ipv6_peers_connected >= LIBP2P_NAT_OPEN_THRESHOLD { - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv6"], 1); + metrics::set_gauge_vec(&discovery_metrics::NAT_OPEN, &["libp2p_ipv6"], 1); } else { - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv6"], 0); + metrics::set_gauge_vec(&discovery_metrics::NAT_OPEN, &["libp2p_ipv6"], 0); } // PEERS_CONNECTED @@ -1525,8 +1702,8 @@ enum ConnectingType { #[cfg(test)] mod tests { use super::*; - use crate::rpc::MetaDataV3; use crate::NetworkConfig; + use crate::rpc::MetaDataV3; use types::{ChainSpec, ForkName, MainnetEthSpec as E}; async fn build_peer_manager(target_peer_count: usize) -> PeerManager { @@ -1559,6 +1736,22 @@ mod tests { PeerManager::new(config, Arc::new(globals)).unwrap() } + fn empty_synced_status() -> SyncStatus { + SyncStatus::Synced { + info: empty_sync_info(), + } + } + + fn empty_sync_info() -> SyncInfo { + SyncInfo { + head_slot: Default::default(), + head_root: Default::default(), + finalized_epoch: Default::default(), + finalized_root: Default::default(), + earliest_available_slot: None, + } + } + #[tokio::test] async fn test_peer_manager_disconnects_correctly_during_heartbeat() { // Create 6 peers to connect to with a target of 3. @@ -1619,32 +1812,40 @@ mod tests { // Check that one outbound-only peer was removed because it had the worst score // and that we did not disconnect the other outbound peer due to the minimum outbound quota. assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); - assert!(peer_manager - .network_globals - .peers - .read() - .is_connected(&outbound_only_peer1)); - assert!(!peer_manager - .network_globals - .peers - .read() - .is_connected(&outbound_only_peer2)); + assert!( + peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer1) + ); + assert!( + !peer_manager + .network_globals + .peers + .read() + .is_connected(&outbound_only_peer2) + ); // The trusted peer remains connected - assert!(peer_manager - .network_globals - .peers - .read() - .is_connected(&trusted_peer)); + assert!( + peer_manager + .network_globals + .peers + .read() + .is_connected(&trusted_peer) + ); peer_manager.heartbeat(); // The trusted peer remains connected, even after subsequent heartbeats. - assert!(peer_manager - .network_globals - .peers - .read() - .is_connected(&trusted_peer)); + assert!( + peer_manager + .network_globals + .peers + .read() + .is_connected(&trusted_peer) + ); // Check that if we are at target number of peers, we do not disconnect any. assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); @@ -1795,6 +1996,7 @@ mod tests { /// a priority over all else. async fn test_peer_manager_remove_non_subnet_peers_when_all_healthy() { let mut peer_manager = build_peer_manager(3).await; + let spec = peer_manager.network_globals.spec.clone(); // Create 5 peers to connect to. let peer0 = PeerId::random(); @@ -1818,10 +2020,11 @@ mod tests { // Have some of the peers be on a long-lived subnet let mut attnets = crate::types::EnrAttestationBitfield::::new(); attnets.set(1, true).unwrap(); - let metadata = crate::rpc::MetaDataV2 { + let metadata = MetaDataV3 { seq_number: 0, attnets, syncnets: Default::default(), + custody_group_count: spec.custody_requirement, }; peer_manager .network_globals @@ -1829,7 +2032,7 @@ mod tests { .write() .peer_info_mut(&peer0) .unwrap() - .set_meta_data(MetaData::V2(metadata)); + .set_meta_data(MetaData::V3(metadata)); peer_manager .network_globals .peers @@ -1838,10 +2041,11 @@ mod tests { let mut attnets = crate::types::EnrAttestationBitfield::::new(); attnets.set(10, true).unwrap(); - let metadata = crate::rpc::MetaDataV2 { + let metadata = MetaDataV3 { seq_number: 0, attnets, syncnets: Default::default(), + custody_group_count: spec.custody_requirement, }; peer_manager .network_globals @@ -1849,7 +2053,7 @@ mod tests { .write() .peer_info_mut(&peer2) .unwrap() - .set_meta_data(MetaData::V2(metadata)); + .set_meta_data(MetaData::V3(metadata)); peer_manager .network_globals .peers @@ -1858,10 +2062,11 @@ mod tests { let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); syncnets.set(3, true).unwrap(); - let metadata = crate::rpc::MetaDataV2 { + let metadata = MetaDataV3 { seq_number: 0, attnets: Default::default(), syncnets, + custody_group_count: spec.custody_requirement, }; peer_manager .network_globals @@ -1869,7 +2074,7 @@ mod tests { .write() .peer_info_mut(&peer4) .unwrap() - .set_meta_data(MetaData::V2(metadata)); + .set_meta_data(MetaData::V3(metadata)); peer_manager .network_globals .peers @@ -1883,7 +2088,7 @@ mod tests { assert_eq!(peer_manager.network_globals.connected_or_dialing_peers(), 3); // Check that we removed the peers that were not subscribed to any subnet - let mut peers_should_have_removed = std::collections::HashSet::new(); + let mut peers_should_have_removed = HashSet::new(); peers_should_have_removed.insert(peer1); peers_should_have_removed.insert(peer3); for (peer, _) in peer_manager @@ -1944,49 +2149,38 @@ mod tests { } #[tokio::test] - /// Test the pruning logic to remove grouped subnet peers - async fn test_peer_manager_prune_grouped_subnet_peers() { + /// Test the pruning logic to remove grouped data column subnet peers + async fn test_peer_manager_prune_grouped_data_column_subnet_peers() { let target = 9; let mut peer_manager = build_peer_manager(target).await; + // Override sampling subnets to prevent sampling peer protection from interfering with this test. + *peer_manager.network_globals.sampling_subnets.write() = HashSet::new(); - // Create 5 peers to connect to. + // Create 20 peers to connect to. let mut peers = Vec::new(); for x in 0..20 { // Make 20 peers and group peers as: // id mod % 4 // except for the last 5 peers which all go on their own subnets // So subnets 0-2 should have 4 peers subnet 3 should have 3 and 15-19 should have 1 - let subnet: u64 = { - if x < 15 { - x % 4 - } else { - x - } - }; + let subnet: u64 = { if x < 15 { x % 4 } else { x } }; let peer = PeerId::random(); peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); // Have some of the peers be on a long-lived subnet - let mut attnets = crate::types::EnrAttestationBitfield::::new(); - attnets.set(subnet as usize, true).unwrap(); - let metadata = crate::rpc::MetaDataV2 { - seq_number: 0, - attnets, - syncnets: Default::default(), - }; + { + let mut peers_db = peer_manager.network_globals.peers.write(); + let peer_info = peers_db.peer_info_mut(&peer).unwrap(); + peer_info.set_custody_subnets(HashSet::from([DataColumnSubnetId::new(subnet)])); + peer_info.update_sync_status(empty_synced_status()); + } + peer_manager .network_globals .peers .write() - .peer_info_mut(&peer) - .unwrap() - .set_meta_data(MetaData::V2(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer, Subnet::Attestation(subnet.into())); + .add_subscription(&peer, Subnet::DataColumn(subnet.into())); println!("{},{},{}", x, subnet, peer); peers.push(peer); } @@ -2058,7 +2252,7 @@ mod tests { /// most peers and have the least subscribed long-lived subnets. And peer 0 because it has no /// long-lived subnet. #[tokio::test] - async fn test_peer_manager_prune_subnet_peers_most_subscribed() { + async fn test_peer_manager_prune_data_column_subnet_peers_most_subscribed() { let target = 3; let mut peer_manager = build_peer_manager(target).await; @@ -2069,43 +2263,27 @@ mod tests { peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); // Have some of the peers be on a long-lived subnet - let mut attnets = crate::types::EnrAttestationBitfield::::new(); - - match x { - 0 => {} - 1 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); - attnets.set(3, true).unwrap(); - } - 2 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); - } - 3 => { - attnets.set(3, true).unwrap(); - } - 4 => { - attnets.set(1, true).unwrap(); - } - 5 => { - attnets.set(2, true).unwrap(); - } + let custody_subnets = match x { + 0 => HashSet::new(), + 1 => HashSet::from([ + DataColumnSubnetId::new(1), + DataColumnSubnetId::new(2), + DataColumnSubnetId::new(3), + ]), + 2 => HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(2)]), + 3 => HashSet::from([DataColumnSubnetId::new(3)]), + 4 => HashSet::from([DataColumnSubnetId::new(1)]), + 5 => HashSet::from([DataColumnSubnetId::new(2)]), _ => unreachable!(), + }; + + { + let mut peer_db = peer_manager.network_globals.peers.write(); + let peer_info = peer_db.peer_info_mut(&peer).unwrap(); + peer_info.set_custody_subnets(custody_subnets); + peer_info.update_sync_status(empty_synced_status()); } - let metadata = crate::rpc::MetaDataV2 { - seq_number: 0, - attnets, - syncnets: Default::default(), - }; - peer_manager - .network_globals - .peers - .write() - .peer_info_mut(&peer) - .unwrap() - .set_meta_data(MetaData::V2(metadata)); let long_lived_subnets = peer_manager .network_globals .peers @@ -2149,22 +2327,24 @@ mod tests { assert!(!connected_peers.contains(&peers[5])); } - /// Test the pruning logic to prioritise peers with the most subnets, but not at the expense of - /// removing our few sync-committee subnets. + /// Test the pruning logic to prioritise peers with the most data column subnets, but not at + /// the expense of removing our few sync-committee subnets. /// /// Create 6 peers. /// Peer0: None - /// Peer1 : Subnet 1,2,3, - /// Peer2 : Subnet 1,2, - /// Peer3 : Subnet 3 - /// Peer4 : Subnet 1,2, Sync-committee-1 - /// Peer5 : Subnet 1,2, Sync-committee-2 + /// Peer1 : Column subnet 1,2,3, + /// Peer2 : Column subnet 1,2, + /// Peer3 : Column subnet 3 + /// Peer4 : Column subnet 1,2, Sync-committee-1 + /// Peer5 : Column subnet 1,2, Sync-committee-2 /// /// Prune 3 peers: Should be Peer0, Peer1 and Peer2 because (4 and 5 are on a sync-committee) #[tokio::test] async fn test_peer_manager_prune_subnet_peers_sync_committee() { let target = 3; let mut peer_manager = build_peer_manager(target).await; + // Override sampling subnets to prevent sampling peer protection from interfering with this test. + *peer_manager.network_globals.sampling_subnets.write() = HashSet::new(); // Create 6 peers to connect to. let mut peers = Vec::new(); @@ -2173,48 +2353,40 @@ mod tests { peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); // Have some of the peers be on a long-lived subnet - let mut attnets = crate::types::EnrAttestationBitfield::::new(); let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); - - match x { - 0 => {} - 1 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); - attnets.set(3, true).unwrap(); - } - 2 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); - } - 3 => { - attnets.set(3, true).unwrap(); - } + let custody_subnets = match x { + 0 => HashSet::new(), + 1 => HashSet::from([ + DataColumnSubnetId::new(1), + DataColumnSubnetId::new(2), + DataColumnSubnetId::new(3), + ]), + 2 => HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(2)]), + 3 => HashSet::from([DataColumnSubnetId::new(3)]), 4 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); syncnets.set(1, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(2)]) } 5 => { - attnets.set(1, true).unwrap(); - attnets.set(2, true).unwrap(); syncnets.set(2, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(2)]) } _ => unreachable!(), + }; + + { + let mut peer_db = peer_manager.network_globals.peers.write(); + let peer_info = peer_db.peer_info_mut(&peer).unwrap(); + peer_info.set_meta_data(MetaData::V3(MetaDataV3 { + seq_number: 0, + attnets: Default::default(), + syncnets, + custody_group_count: 0, // unused in this test, as pruning logic uses `custody_subnets` + })); + peer_info.set_custody_subnets(custody_subnets); + peer_info.update_sync_status(empty_synced_status()); } - let metadata = crate::rpc::MetaDataV2 { - seq_number: 0, - attnets, - syncnets, - }; - peer_manager - .network_globals - .peers - .write() - .peer_info_mut(&peer) - .unwrap() - .set_meta_data(MetaData::V2(metadata)); let long_lived_subnets = peer_manager .network_globals .peers @@ -2258,10 +2430,111 @@ mod tests { assert!(!connected_peers.contains(&peers[2])); } + /// Test that custody subnet peer count below the `MIN_SAMPLING_COLUMN_SUBNET_PEERS`(2) + /// threshold are protected from pruning. + /// + /// Create 8 peers. + /// Peer0: None (can be pruned) + /// Peer1: Subnet 1,4,5 + /// Peer2: Subnet 1,4 + /// Peer3: Subnet 2 + /// Peer4: Subnet 2 + /// Peer5: Subnet 1 (can be pruned) + /// Peer6: Subnet 3 + /// Peer7: Subnet 5 (can be pruned) + /// + /// Sampling subnets: 1, 2 + /// + /// Prune 3 peers: Should be Peer0, Peer 5 and Peer 7 because + /// - Peer 0 because it has no long-lived subnet. + /// - Peer 5 is on the subnet with the most peers and have the least subscribed long-lived subnets. + /// - Peer 7 because it's on a non-sampling subnet and have the least subscribed long-lived subnets. + #[tokio::test] + async fn test_peer_manager_protect_sampling_subnet_peers_below_threshold() { + let target = 5; + let mut peer_manager = build_peer_manager(target).await; + + *peer_manager.network_globals.sampling_subnets.write() = + HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(2)]); + + // Create 8 peers to connect to. + let mut peers = Vec::new(); + for peer_idx in 0..8 { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + // Have some of the peers be on a long-lived subnet + let custody_subnets = match peer_idx { + 0 => HashSet::new(), + 1 => HashSet::from([ + DataColumnSubnetId::new(1), + DataColumnSubnetId::new(4), + DataColumnSubnetId::new(5), + ]), + 2 => HashSet::from([DataColumnSubnetId::new(1), DataColumnSubnetId::new(4)]), + 3 => HashSet::from([DataColumnSubnetId::new(2)]), + 4 => HashSet::from([DataColumnSubnetId::new(2)]), + 5 => HashSet::from([DataColumnSubnetId::new(1)]), + 6 => HashSet::from([DataColumnSubnetId::new(3)]), + 7 => HashSet::from([DataColumnSubnetId::new(5)]), + _ => unreachable!(), + }; + + { + let mut peer_db = peer_manager.network_globals.peers.write(); + let peer_info = peer_db.peer_info_mut(&peer).unwrap(); + peer_info.set_custody_subnets(custody_subnets); + peer_info.update_sync_status(empty_synced_status()); + } + + let long_lived_subnets = peer_manager + .network_globals + .peers + .read() + .peer_info(&peer) + .unwrap() + .long_lived_subnets(); + for subnet in long_lived_subnets { + println!("Subnet: {:?}", subnet); + peer_manager + .network_globals + .peers + .write() + .add_subscription(&peer, subnet); + } + println!("{},{}", peer_idx, peer); + peers.push(peer); + } + + // Perform the heartbeat. + peer_manager.heartbeat(); + + // Tests that when we are over the target peer limit, after disconnecting an unhealthy peer, + // the number of connected peers updates and we will not remove too many peers. + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + target + ); + + // Check that we removed peers 0, 5 and 7 + let connected_peers: std::collections::HashSet<_> = peer_manager + .network_globals + .peers + .read() + .connected_or_dialing_peers() + .cloned() + .collect(); + + println!("Connected peers: {:?}", connected_peers); + assert!(!connected_peers.contains(&peers[0])); + assert!(!connected_peers.contains(&peers[5])); + assert!(!connected_peers.contains(&peers[7])); + } + /// This test is for reproducing the issue: /// https://github.com/sigp/lighthouse/pull/3236#issue-1256432659 /// - /// Whether the issue happens depends on `subnet_to_peer` (HashMap), since HashMap doesn't + /// Whether the issue happens depends on `custody_subnet_to_peers` (HashMap), since HashMap doesn't /// guarantee a particular order of iteration. So we repeat the test case to try to reproduce /// the issue. #[tokio::test] @@ -2271,41 +2544,42 @@ mod tests { } } - /// Test the pruning logic to prioritize peers with the most subnets. This test specifies + /// Test the pruning logic to prioritize peers with the most column subnets. This test specifies /// the connection direction for the peers. /// Either Peer 4 or 5 is expected to be removed in this test case. /// /// Create 8 peers. - /// Peer0 (out) : Subnet 1, Sync-committee-1 - /// Peer1 (out) : Subnet 1, Sync-committee-1 - /// Peer2 (out) : Subnet 2, Sync-committee-2 - /// Peer3 (out) : Subnet 2, Sync-committee-2 - /// Peer4 (out) : Subnet 3 - /// Peer5 (out) : Subnet 3 - /// Peer6 (in) : Subnet 4 - /// Peer7 (in) : Subnet 5 + /// Peer0 (out) : Column subnet 1, Sync-committee-1 + /// Peer1 (out) : Column subnet 1, Sync-committee-1 + /// Peer2 (out) : Column subnet 2, Sync-committee-2 + /// Peer3 (out) : Column subnet 2, Sync-committee-2 + /// Peer4 (out) : Column subnet 3 + /// Peer5 (out) : Column subnet 3 + /// Peer6 (in) : Column subnet 4 + /// Peer7 (in) : Column subnet 5 async fn test_peer_manager_prune_based_on_subnet_count() { let target = 7; let mut peer_manager = build_peer_manager(target).await; + // Override sampling subnets to prevent sampling peer protection from interfering with this test. + *peer_manager.network_globals.sampling_subnets.write() = HashSet::new(); // Create 8 peers to connect to. let mut peers = Vec::new(); - for x in 0..8 { + for peer_idx in 0..8 { let peer = PeerId::random(); // Have some of the peers be on a long-lived subnet - let mut attnets = crate::types::EnrAttestationBitfield::::new(); let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); - match x { + let custody_subnets = match peer_idx { 0 => { peer_manager.inject_connect_outgoing( &peer, "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(1, true).unwrap(); syncnets.set(1, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(1)]) } 1 => { peer_manager.inject_connect_outgoing( @@ -2313,8 +2587,8 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(1, true).unwrap(); syncnets.set(1, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(1)]) } 2 => { peer_manager.inject_connect_outgoing( @@ -2322,8 +2596,8 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(2, true).unwrap(); syncnets.set(2, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(2)]) } 3 => { peer_manager.inject_connect_outgoing( @@ -2331,8 +2605,8 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(2, true).unwrap(); syncnets.set(2, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(2)]) } 4 => { peer_manager.inject_connect_outgoing( @@ -2340,7 +2614,7 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(3, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(3)]) } 5 => { peer_manager.inject_connect_outgoing( @@ -2348,7 +2622,7 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(3, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(3)]) } 6 => { peer_manager.inject_connect_ingoing( @@ -2356,7 +2630,7 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(4, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(4)]) } 7 => { peer_manager.inject_connect_ingoing( @@ -2364,23 +2638,26 @@ mod tests { "/ip4/0.0.0.0".parse().unwrap(), None, ); - attnets.set(5, true).unwrap(); + HashSet::from([DataColumnSubnetId::new(5)]) } _ => unreachable!(), + }; + + let metadata = MetaDataV3 { + seq_number: 0, + attnets: Default::default(), + syncnets, + custody_group_count: 0, // unused in this test, as pruning logic uses `custody_subnets` + }; + + { + let mut peer_db_write = peer_manager.network_globals.peers.write(); + let peer_info = peer_db_write.peer_info_mut(&peer).unwrap(); + peer_info.set_meta_data(MetaData::V3(metadata)); + peer_info.set_custody_subnets(custody_subnets); + peer_info.update_sync_status(empty_synced_status()); } - let metadata = crate::rpc::MetaDataV2 { - seq_number: 0, - attnets, - syncnets, - }; - peer_manager - .network_globals - .peers - .write() - .peer_info_mut(&peer) - .unwrap() - .set_meta_data(MetaData::V2(metadata)); let long_lived_subnets = peer_manager .network_globals .peers @@ -2388,7 +2665,7 @@ mod tests { .peer_info(&peer) .unwrap() .long_lived_subnets(); - println!("{},{}", x, peer); + println!("{},{}", peer_idx, peer); for subnet in long_lived_subnets { println!("Subnet: {:?}", subnet); peer_manager @@ -2424,17 +2701,285 @@ mod tests { assert!(connected_peers.contains(&peers[7])); } + /// Test that peers with the sparsest attestation subnets are protected from pruning. + /// + /// Create 7 peers: + /// - 4 on attnet 0 + /// - 1 on attnet 1 (least dense) + /// - 2 on attnet 2 + /// + /// Prune 3 peers: 2 peers from subnet 0 and 1 from either subnet 0 or 2, BUT never from attnet 1. + #[tokio::test] + async fn test_peer_manager_not_prune_sparsest_attestation_subnet() { + let target = 4; + let mut peer_manager = build_peer_manager(target).await; + let spec = peer_manager.network_globals.spec.clone(); + let mut peers = Vec::new(); + + let subnet_assignments = [0, 0, 0, 0, 1, 2, 2]; + + for &subnet in subnet_assignments.iter() { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + let mut attnets = crate::types::EnrAttestationBitfield::::new(); + attnets.set(subnet, true).unwrap(); + + let metadata = MetaDataV3 { + seq_number: 0, + attnets, + syncnets: Default::default(), + custody_group_count: spec.custody_requirement, + }; + peer_manager + .network_globals + .peers + .write() + .peer_info_mut(&peer) + .unwrap() + .set_meta_data(MetaData::V3(metadata)); + + peer_manager + .network_globals + .peers + .write() + .add_subscription(&peer, Subnet::Attestation((subnet as u64).into())); + + peers.push(peer); + } + + peer_manager.heartbeat(); + + // Check attestation subnet to avoid pruning from subnets with lowest peer count: + // Peer 4 (on least dense subnet 1) should be protected + // Should preferentially remove from subnet 0 (most dense) rather than subnet 1 (least dense) + let connected_peers: HashSet<_> = peer_manager + .network_globals + .peers + .read() + .connected_or_dialing_peers() + .cloned() + .collect(); + + // Peer 4 (on least dense attestation subnet 1) should be kept + assert!(connected_peers.contains(&peers[4])); + + // Attestation subnet uniformity should protect peers on least dense subnets + // Count peers on subnet 1 (least dense) + let subnet_1_count = peers + .iter() + .filter(|&peer| connected_peers.contains(peer)) + .filter(|&peer| { + peer_manager + .network_globals + .peers + .read() + .peer_info(peer) + .unwrap() + .long_lived_subnets() + .iter() + .any(|subnet| matches!(subnet, Subnet::Attestation(id) if id == &1u64.into())) + }) + .count(); + + assert!(subnet_1_count > 0, "Least dense subnet should be protected"); + } + + /// Test the pruning logic prioritizes synced and advanced peers over behind/unknown peers. + /// + /// Create 6 peers with different sync statuses: + /// Peer0: Behind + /// Peer1: Unknown + /// Peer2: Synced + /// Peer3: Advanced + /// Peer4: Synced + /// Peer5: Unknown + /// + /// Target: 3 peers. Should prune peers 0, 1, 5 (behind/unknown) and keep 2, 3, 4 (synced/advanced). + #[tokio::test] + async fn test_peer_manager_prune_should_prioritize_synced_advanced_peers() { + let target = 3; + let mut peer_manager = build_peer_manager(target).await; + // Override sampling subnets to prevent sampling peer protection from interfering with this test. + *peer_manager.network_globals.sampling_subnets.write() = HashSet::new(); + + let mut peers = Vec::new(); + let current_peer_count = 6; + for i in 0..current_peer_count { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + let sync_status = match i { + 0 => SyncStatus::Behind { + info: empty_sync_info(), + }, + 1 | 5 => SyncStatus::Unknown, + 2 | 4 => SyncStatus::Synced { + info: empty_sync_info(), + }, + 3 => SyncStatus::Advanced { + info: empty_sync_info(), + }, + _ => unreachable!(), + }; + + { + let mut peer_db = peer_manager.network_globals.peers.write(); + let peer_info = peer_db.peer_info_mut(&peer).unwrap(); + peer_info.update_sync_status(sync_status); + // make sure all the peers have some long live subnets that are not protected + peer_info.set_custody_subnets(HashSet::from([DataColumnSubnetId::new(2)])) + } + + let long_lived_subnets = peer_manager + .network_globals + .peers + .read() + .peer_info(&peer) + .unwrap() + .long_lived_subnets(); + for subnet in long_lived_subnets { + println!("Subnet: {:?}", subnet); + peer_manager + .network_globals + .peers + .write() + .add_subscription(&peer, subnet); + } + + peers.push(peer); + } + + // Perform the heartbeat to trigger pruning + peer_manager.heartbeat(); + + // Should have exactly target number of peers + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + target + ); + + let connected_peers: std::collections::HashSet<_> = peer_manager + .network_globals + .peers + .read() + .connected_or_dialing_peers() + .cloned() + .collect(); + + // Count how many synced/advanced peers are kept vs behind/unknown peers + let synced_advanced_kept = [&peers[2], &peers[3], &peers[4]] + .iter() + .filter(|peer| connected_peers.contains(peer)) + .count(); + + let behind_unknown_kept = [&peers[0], &peers[1], &peers[5]] + .iter() + .filter(|peer| connected_peers.contains(peer)) + .count(); + + assert_eq!(synced_advanced_kept, target); + assert_eq!(behind_unknown_kept, 0); + } + + /// Test that `peer_subnet_info` is properly cleaned up during pruning iterations. + /// + /// Without proper cleanup, stale peer data affects protection logic for sync committees and we + /// may end up pruning more than expected. + #[tokio::test] + async fn test_peer_manager_prune_mixed_custody_subnet_protection() { + let target = 6; + let mut peer_manager = build_peer_manager(target).await; + // Override sampling subnets to prevent sampling peer protection from interfering. + *peer_manager.network_globals.sampling_subnets.write() = HashSet::new(); + + // Create 12 peers: + // * 4 on custody subnet 0, all on sync committee 0 subnet as well (should only prune up to 2 peers) + // * 3 on subnet 1 + // * 2 on subnet 2 + // * 3 scattered. + let mut peers = Vec::new(); + for i in 0..12 { + let peer = PeerId::random(); + peer_manager.inject_connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); + + let custody_subnet = match i { + ..4 => 0, + 4..7 => 1, + 7..9 => 2, + _ => i - 6, + }; + let on_sync_committee = i < 4; + + { + let mut peers_db = peer_manager.network_globals.peers.write(); + let peer_info = peers_db.peer_info_mut(&peer).unwrap(); + peer_info + .set_custody_subnets(HashSet::from([DataColumnSubnetId::new(custody_subnet)])); + peer_info.update_sync_status(empty_synced_status()); + + if on_sync_committee { + let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); + syncnets.set(0, true).unwrap(); + peer_info.set_meta_data(MetaData::V3(MetaDataV3 { + seq_number: 0, + attnets: Default::default(), + syncnets, + custody_group_count: 0, + })); + } + + for subnet in peer_info.long_lived_subnets() { + peers_db.add_subscription(&peer, subnet); + } + + peers.push(peer); + } + } + + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + 12 + ); + + peer_manager.heartbeat(); + + assert_eq!( + peer_manager.network_globals.connected_or_dialing_peers(), + target + ); + + let connected_peers: HashSet = peer_manager + .network_globals + .peers + .read() + .connected_or_dialing_peers() + .cloned() + .collect(); + + // only 2 peers should be pruned from the 4 peers in subnet 0. + let remaining_sync_peers = connected_peers + .iter() + .filter(|peer| peers[0..4].contains(peer)) + .count(); + assert_eq!( + remaining_sync_peers, 2, + "Sync committee protection should preserve exactly MIN_SYNC_COMMITTEE_PEERS (2)" + ); + } + // Test properties PeerManager should have using randomly generated input. #[cfg(test)] mod property_based_tests { use crate::peer_manager::config::DEFAULT_TARGET_PEERS; use crate::peer_manager::tests::build_peer_manager_with_trusted_peers; - use crate::rpc::MetaData; + use crate::rpc::{MetaData, MetaDataV3}; use libp2p::PeerId; - use quickcheck::{Arbitrary, Gen, TestResult}; - use quickcheck_macros::quickcheck; + use proptest::prelude::*; + use std::collections::HashSet; use tokio::runtime::Runtime; - use types::Unsigned; + use typenum::Unsigned; + use types::DataColumnSubnetId; use types::{EthSpec, MainnetEthSpec as E}; #[derive(Clone, Debug)] @@ -2446,143 +2991,261 @@ mod tests { score: f64, trusted: bool, gossipsub_score: f64, + custody_subnets: HashSet, } - impl Arbitrary for PeerCondition { - fn arbitrary(g: &mut Gen) -> Self { - let attestation_net_bitfield = { - let len = ::SubnetBitfieldLength::to_usize(); - let mut bitfield = Vec::with_capacity(len); - for _ in 0..len { - bitfield.push(bool::arbitrary(g)); - } - bitfield - }; + fn peer_condition_strategy() -> impl Strategy { + let attestation_len = ::SubnetBitfieldLength::to_usize(); + let sync_committee_len = ::SyncCommitteeSubnetCount::to_usize(); + let spec = E::default_spec(); + let total_subnet_count = spec.data_column_sidecar_subnet_count; + let custody_requirement = spec.custody_requirement; - let sync_committee_net_bitfield = { - let len = ::SyncCommitteeSubnetCount::to_usize(); - let mut bitfield = Vec::with_capacity(len); - for _ in 0..len { - bitfield.push(bool::arbitrary(g)); - } - bitfield - }; + // Create the pool of available subnet IDs + let available_subnets: Vec = (custody_requirement..total_subnet_count).collect(); + let max_custody_subnets = available_subnets.len(); - PeerCondition { - peer_id: PeerId::random(), - outgoing: bool::arbitrary(g), - attestation_net_bitfield, - sync_committee_net_bitfield, - score: f64::arbitrary(g), - trusted: bool::arbitrary(g), - gossipsub_score: f64::arbitrary(g), - } - } + // Trusted peer probability constants - 1 in 5 peers should be trusted (20%) + const TRUSTED_PEER_WEIGHT_FALSE: u32 = 4; + const TRUSTED_PEER_WEIGHT_TRUE: u32 = 1; + + ( + proptest::collection::vec(any::(), attestation_len), + proptest::collection::vec(any::(), sync_committee_len), + any::(), + any::(), + any::(), + // Weight trusted peers to avoid test rejection due to too many trusted peers + prop_oneof![ + TRUSTED_PEER_WEIGHT_FALSE => Just(false), + TRUSTED_PEER_WEIGHT_TRUE => Just(true), + ], + 0..=max_custody_subnets, + ) + .prop_flat_map( + move |( + attestation_net_bitfield, + sync_committee_net_bitfield, + score, + outgoing, + gossipsub_score, + trusted, + custody_subnet_count, + )| { + // Use proptest's subsequence to select a random subset of subnets + let custody_subnets_strategy = proptest::sample::subsequence( + available_subnets.clone(), + custody_subnet_count, + ); + + ( + Just(attestation_net_bitfield), + Just(sync_committee_net_bitfield), + Just(score), + Just(outgoing), + Just(gossipsub_score), + Just(trusted), + custody_subnets_strategy, + ) + }, + ) + .prop_map( + |( + attestation_net_bitfield, + sync_committee_net_bitfield, + score, + outgoing, + gossipsub_score, + trusted, + custody_subnets_vec, + )| { + let custody_subnets: HashSet = custody_subnets_vec + .into_iter() + .map(DataColumnSubnetId::new) + .collect(); + + PeerCondition { + peer_id: PeerId::random(), + outgoing, + attestation_net_bitfield, + sync_committee_net_bitfield, + score, + trusted, + gossipsub_score, + custody_subnets, + } + }, + ) } - #[quickcheck] - fn prune_excess_peers(peer_conditions: Vec) -> TestResult { - let target_peer_count = DEFAULT_TARGET_PEERS; - if peer_conditions.len() < target_peer_count { - return TestResult::discard(); - } - let trusted_peers: Vec<_> = peer_conditions - .iter() - .filter_map(|p| if p.trusted { Some(p.peer_id) } else { None }) - .collect(); - // If we have a high percentage of trusted peers, it is very difficult to reason about - // the expected results of the pruning. - if trusted_peers.len() > peer_conditions.len() / 3_usize { - return TestResult::discard(); - } - let rt = Runtime::new().unwrap(); + // Upper bound for testing peer pruning - we test with at least the target number + // and up to 50% more than the target to verify pruning behavior. + const MAX_TEST_PEERS: usize = 300; - rt.block_on(async move { - // Collect all the trusted peers - let mut peer_manager = - build_peer_manager_with_trusted_peers(trusted_peers, target_peer_count).await; + proptest! { + #[test] + fn prune_excess_peers(peer_conditions in proptest::collection::vec(peer_condition_strategy(), DEFAULT_TARGET_PEERS..=MAX_TEST_PEERS)) { + let target_peer_count = DEFAULT_TARGET_PEERS; + let spec = E::default_spec(); - // Create peers based on the randomly generated conditions. - for condition in &peer_conditions { - let mut attnets = crate::types::EnrAttestationBitfield::::new(); - let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); - - if condition.outgoing { - peer_manager.inject_connect_outgoing( - &condition.peer_id, - "/ip4/0.0.0.0".parse().unwrap(), - None, - ); - } else { - peer_manager.inject_connect_ingoing( - &condition.peer_id, - "/ip4/0.0.0.0".parse().unwrap(), - None, - ); - } - - for (i, value) in condition.attestation_net_bitfield.iter().enumerate() { - attnets.set(i, *value).unwrap(); - } - - for (i, value) in condition.sync_committee_net_bitfield.iter().enumerate() { - syncnets.set(i, *value).unwrap(); - } - - let metadata = crate::rpc::MetaDataV2 { - seq_number: 0, - attnets, - syncnets, - }; - - let mut peer_db = peer_manager.network_globals.peers.write(); - let peer_info = peer_db.peer_info_mut(&condition.peer_id).unwrap(); - peer_info.set_meta_data(MetaData::V2(metadata)); - peer_info.set_gossipsub_score(condition.gossipsub_score); - peer_info.add_to_score(condition.score); - - for subnet in peer_info.long_lived_subnets() { - peer_db.add_subscription(&condition.peer_id, subnet); - } - } - - // Perform the heartbeat. - peer_manager.heartbeat(); - - // The minimum number of connected peers cannot be less than the target peer count - // or submitted peers. - - let expected_peer_count = target_peer_count.min(peer_conditions.len()); - // Trusted peers could make this larger however. - let no_of_trusted_peers = peer_conditions + let trusted_peers: Vec<_> = peer_conditions .iter() - .filter(|condition| condition.trusted) - .count(); - let expected_peer_count = expected_peer_count.max(no_of_trusted_peers); + .filter_map(|p| if p.trusted { Some(p.peer_id) } else { None }) + .collect(); + // If we have a high percentage of trusted peers, it is very difficult to reason about + // the expected results of the pruning. + prop_assume!(trusted_peers.len() <= peer_conditions.len() / 3_usize); - let target_peer_condition = - peer_manager.network_globals.connected_or_dialing_peers() - == expected_peer_count; + let rt = Runtime::new().unwrap(); - // It could be that we reach our target outbound limit and are unable to prune any - // extra, which violates the target_peer_condition. - let outbound_peers = peer_manager.network_globals.connected_outbound_only_peers(); - let hit_outbound_limit = outbound_peers == peer_manager.target_outbound_peers(); + let result = rt.block_on(async move { + // Collect all the trusted peers + let mut peer_manager = + build_peer_manager_with_trusted_peers(trusted_peers, target_peer_count).await; - // No trusted peers should be disconnected - let trusted_peer_disconnected = peer_conditions.iter().any(|condition| { - condition.trusted - && !peer_manager - .network_globals - .peers - .read() - .is_connected(&condition.peer_id) + // Create peers based on the randomly generated conditions. + for condition in &peer_conditions { + let mut attnets = crate::types::EnrAttestationBitfield::::new(); + let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); + + if condition.outgoing { + peer_manager.inject_connect_outgoing( + &condition.peer_id, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + } else { + peer_manager.inject_connect_ingoing( + &condition.peer_id, + "/ip4/0.0.0.0".parse().unwrap(), + None, + ); + } + + for (i, value) in condition.attestation_net_bitfield.iter().enumerate() { + attnets.set(i, *value).unwrap(); + } + + for (i, value) in condition.sync_committee_net_bitfield.iter().enumerate() { + syncnets.set(i, *value).unwrap(); + } + + let subnets_per_custody_group = + spec.data_column_sidecar_subnet_count / spec.number_of_custody_groups; + let metadata = MetaDataV3 { + seq_number: 0, + attnets, + syncnets, + custody_group_count: condition.custody_subnets.len() as u64 + / subnets_per_custody_group, + }; + + let mut peer_db = peer_manager.network_globals.peers.write(); + let peer_info = peer_db.peer_info_mut(&condition.peer_id).unwrap(); + peer_info.set_meta_data(MetaData::V3(metadata)); + peer_info.set_gossipsub_score(condition.gossipsub_score); + peer_info.add_to_score(condition.score); + peer_info.set_custody_subnets(condition.custody_subnets.clone()); + + for subnet in peer_info.long_lived_subnets() { + peer_db.add_subscription(&condition.peer_id, subnet); + } + } + + // Perform the heartbeat. + peer_manager.heartbeat(); + + // The minimum number of connected peers cannot be less than the target peer count + // or submitted peers. + + let expected_peer_count = target_peer_count.min(peer_conditions.len()); + // Trusted peers could make this larger however. + let no_of_trusted_peers = peer_conditions + .iter() + .filter(|condition| condition.trusted) + .count(); + let expected_peer_count = expected_peer_count.max(no_of_trusted_peers); + + let target_peer_condition = + peer_manager.network_globals.connected_or_dialing_peers() + == expected_peer_count; + + // It could be that we reach our target outbound limit and are unable to prune any + // extra, which violates the target_peer_condition. + let outbound_peers = peer_manager.network_globals.connected_outbound_only_peers(); + let hit_outbound_limit = outbound_peers == peer_manager.target_outbound_peers(); + + // No trusted peers should be disconnected + let trusted_peer_disconnected = peer_conditions.iter().any(|condition| { + condition.trusted + && !peer_manager + .network_globals + .peers + .read() + .is_connected(&condition.peer_id) + }); + + (target_peer_condition || hit_outbound_limit) && !trusted_peer_disconnected }); - TestResult::from_bool( - (target_peer_condition || hit_outbound_limit) && !trusted_peer_disconnected, - ) - }) + prop_assert!(result); + } } } + + #[tokio::test] + async fn test_custody_peer_logic_only_runs_when_peerdas_enabled() { + use crate::types::{GossipEncoding, GossipTopic}; + + let mut peer_manager = build_peer_manager(5).await; + + // Set up sampling subnets so maintain_custody_peers would have work to do + *peer_manager.network_globals.sampling_subnets.write() = std::collections::HashSet::from([ + DataColumnSubnetId::new(0), + DataColumnSubnetId::new(1), + ]); + + // Test 1: No data column subscriptions - custody peer logic should NOT run + peer_manager.heartbeat(); + + // Should be no new DiscoverSubnetPeers events since PeerDAS is not enabled + let discovery_events: Vec<_> = peer_manager + .events + .iter() + .filter(|event| matches!(event, PeerManagerEvent::DiscoverSubnetPeers(_))) + .collect(); + assert!( + discovery_events.is_empty(), + "Should not generate discovery events when PeerDAS is disabled, but found: {:?}", + discovery_events + ); + + // Test 2: Add data column subscription - custody peer logic should run + let data_column_topic = GossipTopic::new( + GossipKind::DataColumnSidecar(DataColumnSubnetId::new(0)), + GossipEncoding::SSZSnappy, + [0, 0, 0, 0], // fork_digest + ); + peer_manager + .network_globals + .gossipsub_subscriptions + .write() + .insert(data_column_topic); + + // Clear any existing events to isolate the test + peer_manager.events.clear(); + + peer_manager.heartbeat(); + + // Should now have DiscoverSubnetPeers events since PeerDAS is enabled + let discovery_events: Vec<_> = peer_manager + .events + .iter() + .filter(|event| matches!(event, PeerManagerEvent::DiscoverSubnetPeers(_))) + .collect(); + assert!( + !discovery_events.is_empty(), + "Should generate discovery events when PeerDAS is enabled, but found no discovery events" + ); + } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index 1ad55ce5c4..729dbd193b 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -4,21 +4,22 @@ use std::net::IpAddr; use std::task::{Context, Poll}; use futures::StreamExt; -use libp2p::core::transport::PortUse; use libp2p::core::ConnectedPoint; +use libp2p::core::transport::PortUse; use libp2p::identity::PeerId; use libp2p::multiaddr::Protocol; use libp2p::swarm::behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm}; use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use libp2p::swarm::dummy::ConnectionHandler; use libp2p::swarm::{ConnectionDenied, ConnectionId, NetworkBehaviour, ToSwarm}; -pub use metrics::{set_gauge_vec, NAT_OPEN}; +use metrics::set_gauge_vec; +use network_utils::discovery_metrics::NAT_OPEN; +use network_utils::enr_ext::EnrExt; use tracing::{debug, error, trace}; use types::EthSpec; -use crate::discovery::enr_ext::EnrExt; use crate::types::SyncState; -use crate::{metrics, ClearDialError}; +use crate::{ClearDialError, metrics}; use super::{ConnectingType, PeerManager, PeerManagerEvent}; @@ -106,14 +107,14 @@ impl NetworkBehaviour for PeerManager { if let Some(enr) = self.peers_to_dial.pop() { self.inject_peer_connection(&enr.peer_id(), ConnectingType::Dialing, Some(enr.clone())); + let multiaddr_quic = if self.quic_enabled { + enr.multiaddr_quic() + } else { + vec![] + }; + // Prioritize Quic connections over Tcp ones. - let multiaddrs = [ - self.quic_enabled - .then_some(enr.multiaddr_quic()) - .unwrap_or_default(), - enr.multiaddr_tcp(), - ] - .concat(); + let multiaddrs = [multiaddr_quic, enr.multiaddr_tcp()].concat(); debug!(peer_id = %enr.peer_id(), ?multiaddrs, "Dialing peer"); return Poll::Ready(ToSwarm::Dial { @@ -172,7 +173,7 @@ impl NetworkBehaviour for PeerManager { _ => { return Err(ConnectionDenied::new(format!( "Connection to peer rejected: invalid multiaddr: {remote_addr}" - ))) + ))); } }; @@ -340,10 +341,10 @@ impl PeerManager { /// connects and the dial attempt later fails. To handle this, we only update the peer_db if /// the peer is not already connected. fn on_dial_failure(&mut self, peer_id: Option) { - if let Some(peer_id) = peer_id { - if !self.network_globals.peers.read().is_connected(&peer_id) { - self.inject_disconnect(&peer_id); - } + if let Some(peer_id) = peer_id + && !self.network_globals.peers.read().is_connected(&peer_id) + { + self.inject_disconnect(&peer_id); } } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 95a4e82fa2..87337cafcf 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,17 +1,16 @@ +use crate::discovery::CombinedKey; use crate::discovery::enr::PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY; -use crate::discovery::{peer_id_to_node_id, CombinedKey}; -use crate::{ - metrics, multiaddr::Multiaddr, types::Subnet, Enr, EnrExt, Gossipsub, PeerId, SyncInfo, -}; +use crate::{Enr, Gossipsub, PeerId, SyncInfo, metrics, multiaddr::Multiaddr, types::Subnet}; use itertools::Itertools; use logging::crit; +use network_utils::enr_ext::{EnrExt, peer_id_to_node_id}; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use score::{PeerAction, ReportSource, Score, ScoreState}; use std::net::IpAddr; use std::time::Instant; use std::{cmp::Ordering, fmt::Display}; use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, + collections::{HashMap, HashSet, hash_map::Entry}, fmt::Formatter, }; use sync_status::SyncStatus; @@ -248,6 +247,31 @@ impl PeerDB { .map(|(peer_id, _)| peer_id) } + /// Returns all the synced peers from the peer db that claim to have the block + /// components for the given epoch based on `status.earliest_available_slot`. + /// + /// If `earliest_available_slot` info is not available, then return peer anyway assuming it has the + /// required data. + pub fn synced_peers_for_epoch(&self, epoch: Epoch) -> impl Iterator { + self.peers + .iter() + .filter(move |(_, info)| { + info.is_connected() + && match info.sync_status() { + SyncStatus::Synced { info } => { + info.has_slot(epoch.end_slot(E::slots_per_epoch())) + } + SyncStatus::Advanced { info } => { + info.has_slot(epoch.end_slot(E::slots_per_epoch())) + } + SyncStatus::IrrelevantPeer + | SyncStatus::Behind { .. } + | SyncStatus::Unknown => false, + } + }) + .map(|(peer_id, _)| peer_id) + } + /// Gives the `peer_id` of all known connected and advanced peers. pub fn advanced_peers(&self) -> impl Iterator { self.peers @@ -268,6 +292,7 @@ impl PeerDB { .filter(move |(_, info)| { // We check both the metadata and gossipsub data as we only want to count long-lived subscribed peers info.is_connected() + && info.is_synced_or_advanced() && info.on_subnet_metadata(&subnet) && info.on_subnet_gossipsub(&subnet) && info.is_good_gossipsub_peer() @@ -286,11 +311,71 @@ impl PeerDB { .filter(move |(_, info)| { // The custody_subnets hashset can be populated via enr or metadata let is_custody_subnet_peer = info.is_assigned_to_custody_subnet(&subnet); - info.is_connected() && info.is_good_gossipsub_peer() && is_custody_subnet_peer + info.is_connected() + && info.is_good_gossipsub_peer() + && is_custody_subnet_peer + && info.is_synced_or_advanced() }) .map(|(peer_id, _)| peer_id) } + /// Checks if there is at least one good peer for each specified custody subnet for the given epoch. + /// A "good" peer is one that is both connected and synced (or advanced) for the specified epoch. + pub fn has_good_custody_range_sync_peer( + &self, + subnets: &HashSet, + epoch: Epoch, + ) -> bool { + let mut remaining_subnets = subnets.clone(); + + let good_sync_peers_for_epoch = self.peers.values().filter(|&info| { + info.is_connected() + && match info.sync_status() { + SyncStatus::Synced { info } | SyncStatus::Advanced { info } => { + info.has_slot(epoch.end_slot(E::slots_per_epoch())) + } + SyncStatus::IrrelevantPeer + | SyncStatus::Behind { .. } + | SyncStatus::Unknown => false, + } + }); + + for info in good_sync_peers_for_epoch { + for subnet in info.custody_subnets_iter() { + if remaining_subnets.remove(subnet) && remaining_subnets.is_empty() { + return true; + } + } + } + + false + } + + /// Checks if there are sufficient good peers for a single custody subnet. + /// A "good" peer is one that is both connected and synced (or advanced). + pub fn has_good_peers_in_custody_subnet( + &self, + subnet: &DataColumnSubnetId, + target_peers: usize, + ) -> bool { + let mut peer_count = 0usize; + for info in self + .peers + .values() + .filter(|info| info.is_connected() && info.is_synced_or_advanced()) + { + if info.is_assigned_to_custody_subnet(subnet) { + peer_count += 1; + } + + if peer_count >= target_peers { + return true; + } + } + + false + } + /// Gives the ids of all known disconnected peers. pub fn disconnected_peers(&self) -> impl Iterator { self.peers @@ -368,12 +453,11 @@ impl PeerDB { .peers .iter() .filter_map(|(peer_id, info)| { - if let PeerConnectionStatus::Dialing { since } = info.connection_status() { - if (*since) + std::time::Duration::from_secs(DIAL_TIMEOUT) + if let PeerConnectionStatus::Dialing { since } = info.connection_status() + && (*since) + std::time::Duration::from_secs(DIAL_TIMEOUT) < std::time::Instant::now() - { - return Some(*peer_id); - } + { + return Some(*peer_id); } None }) @@ -746,6 +830,7 @@ impl PeerDB { head_root: Hash256::ZERO, finalized_epoch: Epoch::new(0), finalized_root: Hash256::ZERO, + earliest_available_slot: Some(Slot::new(0)), }, }, ); @@ -759,8 +844,9 @@ impl PeerDB { } else { let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); - let subnets = compute_subnets_for_node(node_id.raw(), spec.custody_requirement, spec) - .expect("should compute custody subnets"); + let subnets = + compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) + .expect("should compute custody subnets"); peer_info.set_custody_subnets(subnets); } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs index 9450584d6f..5e761f90a9 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/client.rs @@ -127,12 +127,12 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("teku") => { let kind = ClientKind::Teku; - if agent_split.next().is_some() { - if let Some(agent_version) = agent_split.next() { - version = agent_version.into(); - if let Some(agent_os_version) = agent_split.next() { - os_version = agent_os_version.into(); - } + if agent_split.next().is_some() + && let Some(agent_version) = agent_split.next() + { + version = agent_version.into(); + if let Some(agent_os_version) = agent_split.next() { + os_version = agent_os_version.into(); } } (kind, version, os_version) @@ -143,24 +143,24 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String } Some("Prysm") => { let kind = ClientKind::Prysm; - if agent_split.next().is_some() { - if let Some(agent_version) = agent_split.next() { - version = agent_version.into(); - if let Some(agent_os_version) = agent_split.next() { - os_version = agent_os_version.into(); - } + if agent_split.next().is_some() + && let Some(agent_version) = agent_split.next() + { + version = agent_version.into(); + if let Some(agent_os_version) = agent_split.next() { + os_version = agent_os_version.into(); } } (kind, version, os_version) } Some("nimbus") => { let kind = ClientKind::Nimbus; - if agent_split.next().is_some() { - if let Some(agent_version) = agent_split.next() { - version = agent_version.into(); - if let Some(agent_os_version) = agent_split.next() { - os_version = agent_os_version.into(); - } + if agent_split.next().is_some() + && let Some(agent_version) = agent_split.next() + { + version = agent_version.into(); + if let Some(agent_os_version) = agent_split.next() { + os_version = agent_os_version.into(); } } (kind, version, os_version) diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 4c47df6343..c289cb9a69 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -3,19 +3,19 @@ use super::score::{PeerAction, Score, ScoreState}; use super::sync_status::SyncStatus; use crate::discovery::Eth2Enr; use crate::{rpc::MetaData, types::Subnet}; +use PeerConnectionStatus::*; use discv5::Enr; use eth2::types::{PeerDirection, PeerState}; use libp2p::core::multiaddr::{Multiaddr, Protocol}; use serde::{ - ser::{SerializeStruct, Serializer}, Serialize, + ser::{SerializeStruct, Serializer}, }; use std::collections::HashSet; use std::net::IpAddr; use std::time::Instant; use strum::AsRefStr; use types::{DataColumnSubnetId, EthSpec}; -use PeerConnectionStatus::*; /// Information about a given connected peer. #[derive(Clone, Debug, Serialize)] @@ -95,15 +95,15 @@ impl PeerInfo { if let Some(meta_data) = &self.meta_data { match subnet { Subnet::Attestation(id) => { - return meta_data.attnets().get(**id as usize).unwrap_or(false) + return meta_data.attnets().get(**id as usize).unwrap_or(false); } Subnet::SyncCommittee(id) => { return meta_data .syncnets() - .is_ok_and(|s| s.get(**id as usize).unwrap_or(false)) + .is_ok_and(|s| s.get(**id as usize).unwrap_or(false)); } Subnet::DataColumn(subnet_id) => { - return self.is_assigned_to_custody_subnet(subnet_id) + return self.is_assigned_to_custody_subnet(subnet_id); } } } @@ -174,19 +174,6 @@ impl PeerInfo { self.subnets.iter() } - /// Returns the number of long lived subnets a peer is subscribed to. - // NOTE: This currently excludes sync committee subnets - pub fn long_lived_subnet_count(&self) -> usize { - if let Some(meta_data) = self.meta_data.as_ref() { - return meta_data.attnets().num_set_bits(); - } else if let Some(enr) = self.enr.as_ref() { - if let Ok(attnets) = enr.attestation_bitfield::() { - return attnets.num_set_bits(); - } - } - 0 - } - /// Returns an iterator over the long-lived subnets if it has any. pub fn long_lived_subnets(&self) -> Vec { let mut long_lived_subnets = Vec::new(); @@ -222,6 +209,13 @@ impl PeerInfo { } } } + + long_lived_subnets.extend( + self.custody_subnets + .iter() + .map(|&id| Subnet::DataColumn(id)), + ); + long_lived_subnets } @@ -240,6 +234,11 @@ impl PeerInfo { self.custody_subnets.iter() } + /// Returns the number of custody subnets this peer is assigned to. + pub fn custody_subnet_count(&self) -> usize { + self.custody_subnets.len() + } + /// Returns true if the peer is connected to a long-lived subnet. pub fn has_long_lived_subnet(&self) -> bool { // Check the meta_data @@ -247,21 +246,32 @@ impl PeerInfo { if !meta_data.attnets().is_zero() && !self.subnets.is_empty() { return true; } - if let Ok(sync) = meta_data.syncnets() { - if !sync.is_zero() { - return true; - } + if let Ok(sync) = meta_data.syncnets() + && !sync.is_zero() + { + return true; } } // We may not have the metadata but may have an ENR. Lets check that - if let Some(enr) = self.enr.as_ref() { - if let Ok(attnets) = enr.attestation_bitfield::() { - if !attnets.is_zero() && !self.subnets.is_empty() { - return true; - } - } + if let Some(enr) = self.enr.as_ref() + && let Ok(attnets) = enr.attestation_bitfield::() + && !attnets.is_zero() + && !self.subnets.is_empty() + { + return true; } + + // Check if the peer has custody subnets populated and the peer is subscribed to any of + // its custody subnets + let subscribed_to_any_custody_subnets = self + .custody_subnets + .iter() + .any(|subnet_id| self.subnets.contains(&Subnet::DataColumn(*subnet_id))); + if subscribed_to_any_custody_subnets { + return true; + } + false } @@ -318,6 +328,14 @@ impl PeerInfo { ) } + /// Checks if the peer is synced or advanced. + pub fn is_synced_or_advanced(&self) -> bool { + matches!( + self.sync_status, + SyncStatus::Synced { .. } | SyncStatus::Advanced { .. } + ) + } + /// Checks if the status is connected. pub fn is_dialing(&self) -> bool { matches!(self.connection_status, PeerConnectionStatus::Dialing { .. }) @@ -645,3 +663,50 @@ impl From for PeerState { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::Subnet; + use types::{DataColumnSubnetId, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn create_test_peer_info() -> PeerInfo { + PeerInfo::default() + } + + #[test] + fn test_has_long_lived_subnet_empty_custody_subnets() { + let peer_info = create_test_peer_info(); + // peer has no custody subnets or subscribed to any subnets hence return false + assert!(!peer_info.has_long_lived_subnet()); + } + + #[test] + fn test_has_long_lived_subnet_empty_subnets_with_custody_subnets() { + let mut peer_info = create_test_peer_info(); + peer_info.custody_subnets.insert(DataColumnSubnetId::new(1)); + peer_info.custody_subnets.insert(DataColumnSubnetId::new(2)); + // Peer has custody subnets but isn't subscribed to any hence return false + assert!(!peer_info.has_long_lived_subnet()); + } + + #[test] + fn test_has_long_lived_subnet_subscribed_to_custody_subnets() { + let mut peer_info = create_test_peer_info(); + peer_info.custody_subnets.insert(DataColumnSubnetId::new(1)); + peer_info.custody_subnets.insert(DataColumnSubnetId::new(2)); + peer_info.custody_subnets.insert(DataColumnSubnetId::new(3)); + + peer_info + .subnets + .insert(Subnet::DataColumn(DataColumnSubnetId::new(1))); + peer_info + .subnets + .insert(Subnet::DataColumn(DataColumnSubnetId::new(2))); + // Missing DataColumnSubnetId::new(3) - but peer is subscribed to some custody subnets + // Peer is subscribed to any custody subnets - return true + assert!(peer_info.has_long_lived_subnet()); + } +} diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs index 995ebf9064..e57e7907db 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/score.rs @@ -332,11 +332,7 @@ impl Score { Some(v) => { // Only reverse when none of the items is NAN, // so that NAN's are never considered. - if reverse { - v.reverse() - } else { - v - } + if reverse { v.reverse() } else { v } } None if self.score().is_nan() && !other.score().is_nan() => Ordering::Less, None if !self.score().is_nan() && other.score().is_nan() => Ordering::Greater, diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/sync_status.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/sync_status.rs index bab8aa9aeb..91e2156a27 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/sync_status.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/sync_status.rs @@ -25,6 +25,20 @@ pub struct SyncInfo { pub head_root: Hash256, pub finalized_epoch: Epoch, pub finalized_root: Hash256, + pub earliest_available_slot: Option, +} + +impl SyncInfo { + /// Returns true if the provided slot is greater than or equal to the peer's `earliest_available_slot`. + /// + /// If `earliest_available_slot` is None, then we just assume that the peer has the slot. + pub fn has_slot(&self, slot: Slot) -> bool { + if let Some(earliest_available_slot) = self.earliest_available_slot { + slot >= earliest_available_slot + } else { + true + } + } } impl std::cmp::PartialEq for SyncStatus { diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index cdf09bd89d..3550a7e0a1 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -1,14 +1,14 @@ +use crate::rpc::RequestType; use crate::rpc::methods::*; use crate::rpc::protocol::{ - Encoding, ProtocolId, RPCError, SupportedProtocol, ERROR_TYPE_MAX, ERROR_TYPE_MIN, + ERROR_TYPE_MAX, ERROR_TYPE_MIN, Encoding, ProtocolId, RPCError, SupportedProtocol, }; -use crate::rpc::RequestType; use libp2p::bytes::BufMut; use libp2p::bytes::BytesMut; use snap::read::FrameDecoder; use snap::write::FrameEncoder; use ssz::{Decode, Encode}; -use ssz_types::VariableList; +use ssz_types::{RuntimeVariableList, VariableList}; use std::io::Cursor; use std::io::ErrorKind; use std::io::{Read, Write}; @@ -18,10 +18,10 @@ use tokio_util::codec::{Decoder, Encoder}; use types::{ BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EthSpec, ForkContext, ForkName, Hash256, LightClientBootstrap, LightClientFinalityUpdate, - LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, - SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, - SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, - SignedBeaconBlockElectra, SignedBeaconBlockFulu, + LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, SignedBeaconBlockAltair, + SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, + SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, SignedBeaconBlockElectra, + SignedBeaconBlockFulu, SignedBeaconBlockGloas, }; use unsigned_varint::codec::Uvi; @@ -67,7 +67,13 @@ impl SSZSnappyInboundCodec { ) -> Result<(), RPCError> { let bytes = match &item { RpcResponse::Success(resp) => match &resp { - RpcSuccessResponse::Status(res) => res.as_ssz_bytes(), + RpcSuccessResponse::Status(res) => match self.protocol.versioned_protocol { + SupportedProtocol::StatusV1 => res.status_v1().as_ssz_bytes(), + SupportedProtocol::StatusV2 => res.status_v2().as_ssz_bytes(), + _ => { + unreachable!("We only send status responses on negotiating status protocol") + } + }, RpcSuccessResponse::BlocksByRange(res) => res.as_ssz_bytes(), RpcSuccessResponse::BlocksByRoot(res) => res.as_ssz_bytes(), RpcSuccessResponse::BlobsByRange(res) => res.as_ssz_bytes(), @@ -163,7 +169,9 @@ impl Decoder for SSZSnappyInboundCodec { // Should not attempt to decode rpc chunks with `length > max_packet_size` or not within bounds of // packet size for ssz container corresponding to `self.protocol`. - let ssz_limits = self.protocol.rpc_request_limits(&self.fork_context.spec); + let ssz_limits = self + .protocol + .rpc_request_limits::(&self.fork_context.spec); if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { return Err(RPCError::InvalidData(format!( "RPC request length for protocol {:?} is out of bounds, length {}", @@ -187,7 +195,7 @@ impl Decoder for SSZSnappyInboundCodec { handle_rpc_request( self.protocol.versioned_protocol, &decoded_buffer, - self.fork_context.current_fork(), + self.fork_context.current_fork_name(), &self.fork_context.spec, ) } @@ -329,7 +337,16 @@ impl Encoder> for SSZSnappyOutboundCodec { fn encode(&mut self, item: RequestType, dst: &mut BytesMut) -> Result<(), Self::Error> { let bytes = match item { - RequestType::Status(req) => req.as_ssz_bytes(), + RequestType::Status(req) => { + // Send the status message based on the negotiated protocol + match self.protocol.versioned_protocol { + SupportedProtocol::StatusV1 => req.status_v1().as_ssz_bytes(), + SupportedProtocol::StatusV2 => req.status_v2().as_ssz_bytes(), + _ => { + unreachable!("We only send status requests on negotiating status protocol") + } + } + } RequestType::Goodbye(req) => req.as_ssz_bytes(), RequestType::BlocksByRange(r) => match r { OldBlocksByRangeRequest::V1(req) => req.as_ssz_bytes(), @@ -452,71 +469,12 @@ fn context_bytes( resp: &RpcResponse, ) -> Option<[u8; CONTEXT_BYTES_LEN]> { // Add the context bytes if required - if protocol.has_context_bytes() { - if let RpcResponse::Success(rpc_variant) = resp { - match rpc_variant { - RpcSuccessResponse::BlocksByRange(ref_box_block) - | RpcSuccessResponse::BlocksByRoot(ref_box_block) => { - return match **ref_box_block { - // NOTE: If you are adding another fork type here, be sure to modify the - // `fork_context.to_context_bytes()` function to support it as well! - SignedBeaconBlock::Fulu { .. } => { - fork_context.to_context_bytes(ForkName::Fulu) - } - SignedBeaconBlock::Eip7805 { .. } => { - fork_context.to_context_bytes(ForkName::Eip7805) - } - SignedBeaconBlock::Electra { .. } => { - fork_context.to_context_bytes(ForkName::Electra) - } - SignedBeaconBlock::Deneb { .. } => { - fork_context.to_context_bytes(ForkName::Deneb) - } - SignedBeaconBlock::Capella { .. } => { - fork_context.to_context_bytes(ForkName::Capella) - } - SignedBeaconBlock::Bellatrix { .. } => { - fork_context.to_context_bytes(ForkName::Bellatrix) - } - SignedBeaconBlock::Altair { .. } => { - fork_context.to_context_bytes(ForkName::Altair) - } - SignedBeaconBlock::Base { .. } => { - Some(fork_context.genesis_context_bytes()) - } - }; - } - RpcSuccessResponse::BlobsByRange(_) | RpcSuccessResponse::BlobsByRoot(_) => { - return fork_context.to_context_bytes(ForkName::Deneb); - } - RpcSuccessResponse::DataColumnsByRoot(_) - | RpcSuccessResponse::DataColumnsByRange(_) => { - return fork_context.to_context_bytes(ForkName::Fulu); - } - RpcSuccessResponse::LightClientBootstrap(lc_bootstrap) => { - return lc_bootstrap - .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); - } - RpcSuccessResponse::LightClientOptimisticUpdate(lc_optimistic_update) => { - return lc_optimistic_update - .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); - } - RpcSuccessResponse::LightClientFinalityUpdate(lc_finality_update) => { - return lc_finality_update - .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); - } - RpcSuccessResponse::LightClientUpdatesByRange(lc_update) => { - return lc_update - .map_with_fork_name(|fork_name| fork_context.to_context_bytes(fork_name)); - } - // These will not pass the has_context_bytes() check - RpcSuccessResponse::Status(_) - | RpcSuccessResponse::Pong(_) - | RpcSuccessResponse::MetaData(_) => { - return None; - } - } - } + if protocol.has_context_bytes() + && let RpcResponse::Success(rpc_variant) = resp + { + return rpc_variant + .slot() + .map(|slot| fork_context.context_bytes(slot.epoch(E::slots_per_epoch()))); } None } @@ -556,9 +514,12 @@ fn handle_rpc_request( spec: &ChainSpec, ) -> Result>, RPCError> { match versioned_protocol { - SupportedProtocol::StatusV1 => Ok(Some(RequestType::Status( - StatusMessage::from_ssz_bytes(decoded_buffer)?, - ))), + SupportedProtocol::StatusV1 => Ok(Some(RequestType::Status(StatusMessage::V1( + StatusMessageV1::from_ssz_bytes(decoded_buffer)?, + )))), + SupportedProtocol::StatusV2 => Ok(Some(RequestType::Status(StatusMessage::V2( + StatusMessageV2::from_ssz_bytes(decoded_buffer)?, + )))), SupportedProtocol::GoodbyeV1 => Ok(Some(RequestType::Goodbye( GoodbyeReason::from_ssz_bytes(decoded_buffer)?, ))), @@ -601,10 +562,9 @@ fn handle_rpc_request( SupportedProtocol::DataColumnsByRootV1 => Ok(Some(RequestType::DataColumnsByRoot( DataColumnsByRootRequest { data_column_ids: - >::from_ssz_bytes_with_nested( + >>::from_ssz_bytes( decoded_buffer, spec.max_request_blocks(current_fork), - spec.number_of_columns as usize, )?, }, ))), @@ -669,9 +629,12 @@ fn handle_rpc_response( fork_name: Option, ) -> Result>, RPCError> { match versioned_protocol { - SupportedProtocol::StatusV1 => Ok(Some(RpcSuccessResponse::Status( - StatusMessage::from_ssz_bytes(decoded_buffer)?, - ))), + SupportedProtocol::StatusV1 => Ok(Some(RpcSuccessResponse::Status(StatusMessage::V1( + StatusMessageV1::from_ssz_bytes(decoded_buffer)?, + )))), + SupportedProtocol::StatusV2 => Ok(Some(RpcSuccessResponse::Status(StatusMessage::V2( + StatusMessageV2::from_ssz_bytes(decoded_buffer)?, + )))), // This case should be unreachable as `Goodbye` has no response. SupportedProtocol::GoodbyeV1 => Err(RPCError::InvalidData( "Goodbye RPC message has no valid response".to_string(), @@ -872,6 +835,9 @@ fn handle_rpc_response( Some(ForkName::Fulu) => Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( SignedBeaconBlock::Fulu(SignedBeaconBlockFulu::from_ssz_bytes(decoded_buffer)?), )))), + Some(ForkName::Gloas) => Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Gloas(SignedBeaconBlockGloas::from_ssz_bytes(decoded_buffer)?), + )))), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -913,6 +879,9 @@ fn handle_rpc_response( Some(ForkName::Fulu) => Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( SignedBeaconBlock::Fulu(SignedBeaconBlockFulu::from_ssz_bytes(decoded_buffer)?), )))), + Some(ForkName::Gloas) => Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Gloas(SignedBeaconBlockGloas::from_ssz_bytes(decoded_buffer)?), + )))), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -930,7 +899,7 @@ fn context_bytes_to_fork_name( fork_context: Arc, ) -> Result { fork_context - .from_context_bytes(context_bytes) + .get_fork_from_context_bytes(context_bytes) .cloned() .ok_or_else(|| { let encoded = hex::encode(context_bytes); @@ -949,84 +918,106 @@ mod tests { use super::*; use crate::rpc::protocol::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; + use bls::Signature; + use fixed_bytes::FixedBytesExtended; use types::{ - blob_sidecar::BlobIdentifier, data_column_sidecar::Cell, BeaconBlock, BeaconBlockAltair, - BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockHeader, DataColumnsByRootIdentifier, - EmptyBlock, Epoch, FixedBytesExtended, FullPayload, KzgCommitment, KzgProof, Signature, - SignedBeaconBlockHeader, Slot, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockHeader, + DataColumnsByRootIdentifier, EmptyBlock, Epoch, FullPayload, KzgCommitment, KzgProof, + SignedBeaconBlockHeader, Slot, blob_sidecar::BlobIdentifier, data_column_sidecar::Cell, }; type Spec = types::MainnetEthSpec; - fn fork_context(fork_name: ForkName) -> ForkContext { + fn spec_with_all_forks_enabled() -> ChainSpec { let mut chain_spec = Spec::default_spec(); - let altair_fork_epoch = Epoch::new(1); - let bellatrix_fork_epoch = Epoch::new(2); - let capella_fork_epoch = Epoch::new(3); - let deneb_fork_epoch = Epoch::new(4); - let electra_fork_epoch = Epoch::new(5); - let eip7805_fork_epoch = Epoch::new(6); - let fulu_fork_epoch = Epoch::new(7); + chain_spec.altair_fork_epoch = Some(Epoch::new(1)); + chain_spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + chain_spec.capella_fork_epoch = Some(Epoch::new(3)); + chain_spec.deneb_fork_epoch = Some(Epoch::new(4)); + chain_spec.electra_fork_epoch = Some(Epoch::new(5)); + chain_spec.fulu_fork_epoch = Some(Epoch::new(6)); + chain_spec.eip7805_fork_epoch = Some(Epoch::new(7)); + chain_spec.gloas_fork_epoch = Some(Epoch::new(8)); - chain_spec.altair_fork_epoch = Some(altair_fork_epoch); - chain_spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - chain_spec.capella_fork_epoch = Some(capella_fork_epoch); - chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); - chain_spec.electra_fork_epoch = Some(electra_fork_epoch); - chain_spec.eip7805_fork_epoch = Some(eip7805_fork_epoch); - chain_spec.fulu_fork_epoch = Some(fulu_fork_epoch); + // check that we have all forks covered + assert!(chain_spec.fork_epoch(ForkName::latest()).is_some()); + chain_spec + } - let current_slot = match fork_name { - ForkName::Base => Slot::new(0), - ForkName::Altair => altair_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Bellatrix => bellatrix_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Capella => capella_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Deneb => deneb_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Electra => electra_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Eip7805 => eip7805_fork_epoch.start_slot(Spec::slots_per_epoch()), - ForkName::Fulu => fulu_fork_epoch.start_slot(Spec::slots_per_epoch()), + fn fork_context(fork_name: ForkName, spec: &ChainSpec) -> ForkContext { + let current_epoch = match fork_name { + ForkName::Base => Some(Epoch::new(0)), + ForkName::Altair => spec.altair_fork_epoch, + ForkName::Bellatrix => spec.bellatrix_fork_epoch, + ForkName::Capella => spec.capella_fork_epoch, + ForkName::Deneb => spec.deneb_fork_epoch, + ForkName::Electra => spec.electra_fork_epoch, + ForkName::Fulu => spec.fulu_fork_epoch, + ForkName::Eip7805 => spec.eip7805_fork_epoch, + ForkName::Gloas => spec.gloas_fork_epoch, }; - ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) + let current_slot = current_epoch.unwrap().start_slot(Spec::slots_per_epoch()); + ForkContext::new::(current_slot, Hash256::zero(), spec) } /// Smallest sized block across all current forks. Useful for testing /// min length check conditions. - fn empty_base_block() -> SignedBeaconBlock { - let empty_block = BeaconBlock::Base(BeaconBlockBase::::empty(&Spec::default_spec())); + fn empty_base_block(spec: &ChainSpec) -> SignedBeaconBlock { + let empty_block = BeaconBlock::Base(BeaconBlockBase::::empty(spec)); SignedBeaconBlock::from_block(empty_block, Signature::empty()) } - fn altair_block() -> SignedBeaconBlock { - let full_block = - BeaconBlock::Altair(BeaconBlockAltair::::full(&Spec::default_spec())); + fn altair_block(spec: &ChainSpec) -> SignedBeaconBlock { + // The context bytes are now derived from the block epoch, so we need to have the slot set + // here. + let full_block = BeaconBlock::Altair(BeaconBlockAltair::::full(spec)); SignedBeaconBlock::from_block(full_block, Signature::empty()) } - fn empty_blob_sidecar() -> Arc> { - Arc::new(BlobSidecar::empty()) + fn empty_blob_sidecar(spec: &ChainSpec) -> Arc> { + // The context bytes are now derived from the block epoch, so we need to have the slot set + // here. + let mut blob_sidecar = BlobSidecar::::empty(); + blob_sidecar.signed_block_header.message.slot = spec + .deneb_fork_epoch + .expect("deneb fork epoch must be set") + .start_slot(Spec::slots_per_epoch()); + Arc::new(blob_sidecar) } - fn empty_data_column_sidecar() -> Arc> { - Arc::new(DataColumnSidecar { + fn empty_data_column_sidecar(spec: &ChainSpec) -> Arc> { + // The context bytes are now derived from the block epoch, so we need to have the slot set + // here. + let data_column_sidecar = DataColumnSidecar { index: 0, column: VariableList::new(vec![Cell::::default()]).unwrap(), kzg_commitments: VariableList::new(vec![KzgCommitment::empty_for_testing()]).unwrap(), kzg_proofs: VariableList::new(vec![KzgProof::empty()]).unwrap(), signed_block_header: SignedBeaconBlockHeader { - message: BeaconBlockHeader::empty(), + message: BeaconBlockHeader { + slot: spec + .fulu_fork_epoch + .expect("fulu fork epoch must be set") + .start_slot(Spec::slots_per_epoch()), + ..BeaconBlockHeader::empty() + }, signature: Signature::empty(), }, kzg_commitments_inclusion_proof: Default::default(), - }) + }; + Arc::new(data_column_sidecar) } /// Bellatrix block with length < max_rpc_size. fn bellatrix_block_small(spec: &ChainSpec) -> SignedBeaconBlock { + // The context bytes are now derived from the block epoch, so we need to have the slot set + // here. let mut block: BeaconBlockBellatrix<_, FullPayload> = - BeaconBlockBellatrix::empty(&Spec::default_spec()); + BeaconBlockBellatrix::empty(spec); - let tx = VariableList::from(vec![0; 1024]); - let txs = VariableList::from(std::iter::repeat_n(tx, 5000).collect::>()); + let tx = VariableList::try_from(vec![0; 1024]).unwrap(); + let txs = + VariableList::try_from(std::iter::repeat_n(tx, 5000).collect::>()).unwrap(); block.body.execution_payload.execution_payload.transactions = txs; @@ -1039,11 +1030,14 @@ mod tests { /// The max limit for a Bellatrix block is in the order of ~16GiB which wouldn't fit in memory. /// Hence, we generate a Bellatrix block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. fn bellatrix_block_large(spec: &ChainSpec) -> SignedBeaconBlock { + // The context bytes are now derived from the block epoch, so we need to have the slot set + // here. let mut block: BeaconBlockBellatrix<_, FullPayload> = - BeaconBlockBellatrix::empty(&Spec::default_spec()); + BeaconBlockBellatrix::empty(spec); - let tx = VariableList::from(vec![0; 1024]); - let txs = VariableList::from(std::iter::repeat_n(tx, 100000).collect::>()); + let tx = VariableList::try_from(vec![0; 1024]).unwrap(); + let txs = + VariableList::try_from(std::iter::repeat_n(tx, 100000).collect::>()).unwrap(); block.body.execution_payload.execution_payload.transactions = txs; @@ -1052,14 +1046,25 @@ mod tests { SignedBeaconBlock::from_block(block, Signature::empty()) } - fn status_message() -> StatusMessage { - StatusMessage { + fn status_message_v1() -> StatusMessage { + StatusMessage::V1(StatusMessageV1 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), head_root: Hash256::zero(), head_slot: Slot::new(1), - } + }) + } + + fn status_message_v2() -> StatusMessage { + StatusMessage::V2(StatusMessageV2 { + fork_digest: [0; 4], + finalized_root: Hash256::zero(), + finalized_epoch: Epoch::new(1), + head_root: Hash256::zero(), + head_slot: Slot::new(1), + earliest_available_slot: Slot::new(0), + }) } fn bbrange_request_v1() -> OldBlocksByRangeRequest { @@ -1085,13 +1090,12 @@ mod tests { } } - fn dcbroot_request(spec: &ChainSpec, fork_name: ForkName) -> DataColumnsByRootRequest { - let number_of_columns = spec.number_of_columns as usize; + fn dcbroot_request(fork_name: ForkName, spec: &ChainSpec) -> DataColumnsByRootRequest { DataColumnsByRootRequest { data_column_ids: RuntimeVariableList::new( vec![DataColumnsByRootIdentifier { block_root: Hash256::zero(), - columns: RuntimeVariableList::from_vec(vec![0, 1, 2], number_of_columns), + columns: VariableList::try_from(vec![0, 1, 2]).unwrap(), }], spec.max_request_blocks(fork_name), ) @@ -1099,22 +1103,23 @@ mod tests { } } - fn bbroot_request_v1(fork_name: ForkName) -> BlocksByRootRequest { - BlocksByRootRequest::new_v1(vec![Hash256::zero()], &fork_context(fork_name)) + fn bbroot_request_v1(fork_name: ForkName, spec: &ChainSpec) -> BlocksByRootRequest { + BlocksByRootRequest::new_v1(vec![Hash256::zero()], &fork_context(fork_name, spec)).unwrap() } - fn bbroot_request_v2(fork_name: ForkName) -> BlocksByRootRequest { - BlocksByRootRequest::new(vec![Hash256::zero()], &fork_context(fork_name)) + fn bbroot_request_v2(fork_name: ForkName, spec: &ChainSpec) -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![Hash256::zero()], &fork_context(fork_name, spec)).unwrap() } - fn blbroot_request(fork_name: ForkName) -> BlobsByRootRequest { + fn blbroot_request(fork_name: ForkName, spec: &ChainSpec) -> BlobsByRootRequest { BlobsByRootRequest::new( vec![BlobIdentifier { block_root: Hash256::zero(), index: 0, }], - &fork_context(fork_name), + &fork_context(fork_name, spec), ) + .unwrap() } fn ping_message() -> Ping { @@ -1156,7 +1161,7 @@ mod tests { spec: &ChainSpec, ) -> Result { let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); - let fork_context = Arc::new(fork_context(fork_name)); + let fork_context = Arc::new(fork_context(fork_name, spec)); let max_packet_size = spec.max_payload_size as usize; let mut buf = BytesMut::new(); @@ -1170,12 +1175,13 @@ mod tests { fn encode_without_length_checks( bytes: Vec, fork_name: ForkName, + spec: &ChainSpec, ) -> Result { - let fork_context = fork_context(fork_name); + let fork_context = fork_context(fork_name, spec); let mut dst = BytesMut::new(); // Add context bytes if required - dst.extend_from_slice(&fork_context.to_context_bytes(fork_name).unwrap()); + dst.extend_from_slice(&fork_context.context_bytes(fork_context.current_fork_epoch())); let mut uvi_codec: Uvi = Uvi::default(); @@ -1203,7 +1209,7 @@ mod tests { spec: &ChainSpec, ) -> Result>, RPCError> { let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); - let fork_context = Arc::new(fork_context(fork_name)); + let fork_context = Arc::new(fork_context(fork_name, spec)); let max_packet_size = spec.max_payload_size as usize; let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new(snappy_protocol_id, max_packet_size, fork_context); @@ -1224,7 +1230,7 @@ mod tests { /// Verifies that requests we send are encoded in a way that we would correctly decode too. fn encode_then_decode_request(req: RequestType, fork_name: ForkName, spec: &ChainSpec) { - let fork_context = Arc::new(fork_context(fork_name)); + let fork_context = Arc::new(fork_context(fork_name, spec)); let max_packet_size = spec.max_payload_size as usize; let protocol = ProtocolId::new(req.versioned_protocol(), Encoding::SSZSnappy); // Encode a request we send @@ -1295,16 +1301,27 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v1() { - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); assert_eq!( encode_then_decode_response( SupportedProtocol::StatusV1, - RpcResponse::Success(RpcSuccessResponse::Status(status_message())), + RpcResponse::Success(RpcSuccessResponse::Status(status_message_v1())), ForkName::Base, &chain_spec, ), - Ok(Some(RpcSuccessResponse::Status(status_message()))) + Ok(Some(RpcSuccessResponse::Status(status_message_v1()))) + ); + + // A StatusV2 still encodes as a StatusV1 since version is Version::V1 + assert_eq!( + encode_then_decode_response( + SupportedProtocol::StatusV1, + RpcResponse::Success(RpcSuccessResponse::Status(status_message_v2())), + ForkName::Gloas, + &chain_spec, + ), + Ok(Some(RpcSuccessResponse::Status(status_message_v1()))) ); assert_eq!( @@ -1321,13 +1338,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRangeV1, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Base, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))) ); @@ -1336,7 +1353,7 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRangeV1, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - altair_block() + altair_block(&chain_spec) ))), ForkName::Altair, &chain_spec, @@ -1351,13 +1368,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRootV1, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Base, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))) ); @@ -1365,9 +1382,9 @@ mod tests { matches!( encode_then_decode_response( SupportedProtocol::BlocksByRootV1, - RpcResponse::Success(RpcSuccessResponse::BlocksByRoot( - Arc::new(altair_block()) - )), + RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new(altair_block( + &chain_spec + )))), ForkName::Altair, &chain_spec, ) @@ -1412,84 +1429,98 @@ mod tests { assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRangeV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + ))), ForkName::Deneb, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), + Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRangeV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + ))), ForkName::Electra, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), + Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRangeV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + ))), ForkName::Fulu, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), + Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar( + &chain_spec + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRootV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( + &chain_spec + ))), ForkName::Deneb, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), + Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( + &chain_spec + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRootV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( + &chain_spec + ))), ForkName::Electra, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), - ); - - assert_eq!( - encode_then_decode_response( - SupportedProtocol::BlobsByRootV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), - ForkName::Eip7805, + Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( &chain_spec - ), - Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRootV1, - RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), + RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( + &chain_spec + ))), ForkName::Fulu, &chain_spec ), - Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), + Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar( + &chain_spec + )))), ); assert_eq!( encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Deneb, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1497,13 +1528,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Electra, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1511,13 +1542,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Eip7805, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1525,13 +1556,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Fulu, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRange( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1539,13 +1570,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Deneb, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1553,13 +1584,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Electra, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1567,13 +1598,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Eip7805, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); @@ -1581,13 +1612,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) )), ForkName::Fulu, &chain_spec ), Ok(Some(RpcSuccessResponse::DataColumnsByRoot( - empty_data_column_sidecar() + empty_data_column_sidecar(&chain_spec) ))), ); } @@ -1595,19 +1626,19 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v2() { - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); assert_eq!( encode_then_decode_response( SupportedProtocol::BlocksByRangeV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Base, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))) ); @@ -1618,25 +1649,27 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRangeV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Altair, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))) ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlocksByRangeV2, - RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new(altair_block()))), + RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new(altair_block( + &chain_spec + )))), ForkName::Altair, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( - altair_block() + altair_block(&chain_spec) )))) ); @@ -1657,9 +1690,12 @@ mod tests { )))) ); - let mut encoded = - encode_without_length_checks(bellatrix_block_large.as_ssz_bytes(), ForkName::Bellatrix) - .unwrap(); + let mut encoded = encode_without_length_checks( + bellatrix_block_large.as_ssz_bytes(), + ForkName::Bellatrix, + &chain_spec, + ) + .unwrap(); assert!( matches!( @@ -1679,13 +1715,13 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRootV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Base, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))), ); @@ -1696,25 +1732,27 @@ mod tests { encode_then_decode_response( SupportedProtocol::BlocksByRootV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) ))), ForkName::Altair, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block() + empty_base_block(&chain_spec) )))) ); assert_eq!( encode_then_decode_response( SupportedProtocol::BlocksByRootV2, - RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new(altair_block()))), + RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new(altair_block( + &chain_spec + )))), ForkName::Altair, &chain_spec, ), Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( - altair_block() + altair_block(&chain_spec) )))) ); @@ -1732,9 +1770,12 @@ mod tests { )))) ); - let mut encoded = - encode_without_length_checks(bellatrix_block_large.as_ssz_bytes(), ForkName::Bellatrix) - .unwrap(); + let mut encoded = encode_without_length_checks( + bellatrix_block_large.as_ssz_bytes(), + ForkName::Bellatrix, + &chain_spec, + ) + .unwrap(); assert!( matches!( @@ -1770,20 +1811,40 @@ mod tests { ), Ok(Some(RpcSuccessResponse::MetaData(metadata_v2()))) ); + + // A StatusV1 still encodes as a StatusV2 since version is Version::V2 + assert_eq!( + encode_then_decode_response( + SupportedProtocol::StatusV2, + RpcResponse::Success(RpcSuccessResponse::Status(status_message_v1())), + ForkName::Fulu, + &chain_spec, + ), + Ok(Some(RpcSuccessResponse::Status(status_message_v2()))) + ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::StatusV2, + RpcResponse::Success(RpcSuccessResponse::Status(status_message_v2())), + ForkName::Fulu, + &chain_spec, + ), + Ok(Some(RpcSuccessResponse::Status(status_message_v2()))) + ); } // Test RPCResponse encoding/decoding for V2 messages #[test] fn test_context_bytes_v2() { - let fork_context = fork_context(ForkName::Altair); - - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); + let fork_context = fork_context(ForkName::Altair, &chain_spec); // Removing context bytes for v2 messages should error let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRangeV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block(), + empty_base_block(&chain_spec), ))), ForkName::Base, &chain_spec, @@ -1806,7 +1867,7 @@ mod tests { let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRootV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block(), + empty_base_block(&chain_spec), ))), ForkName::Base, &chain_spec, @@ -1830,7 +1891,7 @@ mod tests { let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRangeV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRange(Arc::new( - empty_base_block(), + empty_base_block(&chain_spec), ))), ForkName::Altair, &chain_spec, @@ -1838,8 +1899,8 @@ mod tests { .unwrap(); let mut wrong_fork_bytes = BytesMut::new(); - wrong_fork_bytes - .extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); + let altair_epoch = chain_spec.altair_fork_epoch.unwrap(); + wrong_fork_bytes.extend_from_slice(&fork_context.context_bytes(altair_epoch)); wrong_fork_bytes.extend_from_slice(&encoded_bytes.split_off(4)); assert!(matches!( @@ -1856,14 +1917,18 @@ mod tests { // Trying to decode an altair block with base context bytes should give ssz decoding error let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRootV2, - RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new(altair_block()))), + RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new(altair_block( + &chain_spec, + )))), ForkName::Altair, &chain_spec, ) .unwrap(); let mut wrong_fork_bytes = BytesMut::new(); - wrong_fork_bytes.extend_from_slice(&fork_context.to_context_bytes(ForkName::Base).unwrap()); + wrong_fork_bytes.extend_from_slice( + &fork_context.context_bytes(chain_spec.genesis_slot.epoch(Spec::slots_per_epoch())), + ); wrong_fork_bytes.extend_from_slice(&encoded_bytes.split_off(4)); assert!(matches!( @@ -1879,7 +1944,7 @@ mod tests { // Adding context bytes to Protocols that don't require it should return an error let mut encoded_bytes = BytesMut::new(); - encoded_bytes.extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); + encoded_bytes.extend_from_slice(&fork_context.context_bytes(altair_epoch)); encoded_bytes.extend_from_slice( &encode_response( SupportedProtocol::MetaDataV2, @@ -1890,19 +1955,21 @@ mod tests { .unwrap(), ); - assert!(decode_response( - SupportedProtocol::MetaDataV2, - &mut encoded_bytes, - ForkName::Altair, - &chain_spec, - ) - .is_err()); + assert!( + decode_response( + SupportedProtocol::MetaDataV2, + &mut encoded_bytes, + ForkName::Altair, + &chain_spec, + ) + .is_err() + ); // Sending context bytes which do not correspond to any fork should return an error let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRootV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block(), + empty_base_block(&chain_spec), ))), ForkName::Altair, &chain_spec, @@ -1928,7 +1995,7 @@ mod tests { let mut encoded_bytes = encode_response( SupportedProtocol::BlocksByRootV2, RpcResponse::Success(RpcSuccessResponse::BlocksByRoot(Arc::new( - empty_base_block(), + empty_base_block(&chain_spec), ))), ForkName::Altair, &chain_spec, @@ -1950,12 +2017,12 @@ mod tests { #[test] fn test_encode_then_decode_request() { - let fork_context = fork_context(ForkName::Electra); - let chain_spec = fork_context.spec.clone(); + let chain_spec = spec_with_all_forks_enabled(); let requests: &[RequestType] = &[ RequestType::Ping(ping_message()), - RequestType::Status(status_message()), + RequestType::Status(status_message_v1()), + RequestType::Status(status_message_v2()), RequestType::Goodbye(GoodbyeReason::Fault), RequestType::BlocksByRange(bbrange_request_v1()), RequestType::BlocksByRange(bbrange_request_v2()), @@ -1974,10 +2041,10 @@ mod tests { // Handled separately to have consistent `ForkName` across request and responses let fork_dependent_requests = |fork_name| { [ - RequestType::BlobsByRoot(blbroot_request(fork_name)), - RequestType::BlocksByRoot(bbroot_request_v1(fork_name)), - RequestType::BlocksByRoot(bbroot_request_v2(fork_name)), - RequestType::DataColumnsByRoot(dcbroot_request(&chain_spec, fork_name)), + RequestType::BlobsByRoot(blbroot_request(fork_name, &chain_spec)), + RequestType::BlocksByRoot(bbroot_request_v1(fork_name, &chain_spec)), + RequestType::BlocksByRoot(bbroot_request_v2(fork_name, &chain_spec)), + RequestType::DataColumnsByRoot(dcbroot_request(fork_name, &chain_spec)), ] }; for fork_name in ForkName::list_all() { @@ -2002,7 +2069,7 @@ mod tests { let malicious_padding: &'static [u8] = b"\xFE\x00\x00\x00"; // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. - let status_message_bytes = StatusMessage { + let status_message_bytes = StatusMessageV1 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), @@ -2037,7 +2104,7 @@ mod tests { assert_eq!(writer.get_ref().len(), 42); dst.extend_from_slice(writer.get_ref()); - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( decode_response( @@ -2055,7 +2122,8 @@ mod tests { /// sends a valid message filled with a stream of useless padding before the actual message. #[test] fn test_decode_malicious_v2_message() { - let fork_context = Arc::new(fork_context(ForkName::Altair)); + let chain_spec = spec_with_all_forks_enabled(); + let fork_context = Arc::new(fork_context(ForkName::Altair, &chain_spec)); // 10 byte snappy stream identifier let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY"; @@ -2067,7 +2135,7 @@ mod tests { let malicious_padding: &'static [u8] = b"\xFE\x00\x00\x00"; // Full altair block is 157916 bytes uncompressed. `max_compressed_len` is 32 + 157916 + 157916/6 = 184267. - let block_message_bytes = altair_block().as_ssz_bytes(); + let block_message_bytes = altair_block(&fork_context.spec).as_ssz_bytes(); assert_eq!(block_message_bytes.len(), 157916); assert_eq!( @@ -2079,7 +2147,8 @@ mod tests { let mut dst = BytesMut::with_capacity(1024); // Insert context bytes - dst.extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); + let altair_epoch = fork_context.spec.altair_fork_epoch.unwrap(); + dst.extend_from_slice(&fork_context.context_bytes(altair_epoch)); // Insert length-prefix uvi_codec @@ -2094,14 +2163,14 @@ mod tests { dst.extend_from_slice(malicious_padding); } - // Insert payload (8103 bytes compressed) + // Insert payload (8102 bytes compressed) let mut writer = FrameEncoder::new(Vec::new()); writer.write_all(&block_message_bytes).unwrap(); writer.flush().unwrap(); - assert_eq!(writer.get_ref().len(), 8103); + assert_eq!(writer.get_ref().len(), 8102); dst.extend_from_slice(writer.get_ref()); - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); // 10 (for stream identifier) + 176156 + 8103 = 184269 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( @@ -2125,7 +2194,7 @@ mod tests { assert_eq!(stream_identifier.len(), 10); // Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130. - let status_message_bytes = StatusMessage { + let status_message_bytes = StatusMessageV1 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), @@ -2137,7 +2206,7 @@ mod tests { let mut uvi_codec: Uvi = Uvi::default(); let mut dst = BytesMut::with_capacity(1024); - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); // Insert length-prefix uvi_codec @@ -2173,9 +2242,8 @@ mod tests { let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); - let fork_context = Arc::new(fork_context(ForkName::Base)); - - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); + let fork_context = Arc::new(fork_context(ForkName::Base, &chain_spec)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, @@ -2209,9 +2277,8 @@ mod tests { let snappy_protocol_id = ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy); - let fork_context = Arc::new(fork_context(ForkName::Base)); - - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); + let fork_context = Arc::new(fork_context(ForkName::Base, &chain_spec)); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, @@ -2240,9 +2307,8 @@ mod tests { let protocol_id = ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy); // Response limits - let fork_context = Arc::new(fork_context(ForkName::Base)); - - let chain_spec = Spec::default_spec(); + let chain_spec = spec_with_all_forks_enabled(); + let fork_context = Arc::new(fork_context(ForkName::Base, &chain_spec)); let max_rpc_size = chain_spec.max_payload_size as usize; let limit = protocol_id.rpc_response_limits::(&fork_context); @@ -2269,7 +2335,7 @@ mod tests { )); // Request limits - let limit = protocol_id.rpc_request_limits(&fork_context.spec); + let limit = protocol_id.rpc_request_limits::(&fork_context.spec); let mut max = encode_len(limit.max + 1); let mut codec = SSZSnappyOutboundCodec::::new( protocol_id.clone(), diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 75d49e9cb5..b0ee6fea64 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -1,11 +1,11 @@ +use super::{Protocol, rate_limiter::Quota}; +use std::num::NonZeroU64; use std::{ fmt::{Debug, Display}, str::FromStr, time::Duration, }; -use super::{rate_limiter::Quota, Protocol}; - use serde::{Deserialize, Serialize}; /// Auxiliary struct to aid on configuration parsing. @@ -100,24 +100,28 @@ pub struct RateLimiterConfig { } impl RateLimiterConfig { - pub const DEFAULT_PING_QUOTA: Quota = Quota::n_every(2, 10); - pub const DEFAULT_META_DATA_QUOTA: Quota = Quota::n_every(2, 5); - pub const DEFAULT_STATUS_QUOTA: Quota = Quota::n_every(5, 15); + pub const DEFAULT_PING_QUOTA: Quota = Quota::n_every(NonZeroU64::new(2).unwrap(), 10); + pub const DEFAULT_META_DATA_QUOTA: Quota = Quota::n_every(NonZeroU64::new(2).unwrap(), 5); + pub const DEFAULT_STATUS_QUOTA: Quota = Quota::n_every(NonZeroU64::new(5).unwrap(), 15); pub const DEFAULT_GOODBYE_QUOTA: Quota = Quota::one_every(10); // The number is chosen to balance between upload bandwidth required to serve // blocks and a decent syncing rate for honest nodes. Malicious nodes would need to // spread out their requests over the time window to max out bandwidth on the server. - pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); - pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(128).unwrap(), 10); + pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(128).unwrap(), 10); // `DEFAULT_BLOCKS_BY_RANGE_QUOTA` * (target + 1) to account for high usage - pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(896, 10); - pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(896, 10); - // 320 blocks worth of columns for regular node, or 40 blocks for supernode. - // Range sync load balances when requesting blocks, and each batch is 32 blocks. - pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = Quota::n_every(5120, 10); - // 512 columns per request from spec. This should be plenty as peers are unlikely to send all - // sampling requests to a single peer. - pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(512, 10); + pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(896).unwrap(), 10); + pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(896).unwrap(), 10); + // Allow up to `MAX_REQUEST_DATA_COLUMN_SIDECARS` (16384), the maximum number of data + // column sidecars in a single request from the spec. + pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(16384).unwrap(), 10); + pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = + Quota::n_every(NonZeroU64::new(16384).unwrap(), 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA: Quota = Quota::one_every(10); @@ -275,7 +279,7 @@ mod tests { protocol: Protocol::Goodbye, quota: Quota { replenish_all_every: Duration::from_secs(10), - max_tokens: 8, + max_tokens: NonZeroU64::new(8).unwrap(), }, }; assert_eq!(quota.to_string().parse(), Ok(quota)) diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 33c5521c3b..720895bbe7 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -8,27 +8,27 @@ use super::{RPCReceived, RPCSend, ReqId}; use crate::rpc::outbound::OutboundFramed; use crate::rpc::protocol::InboundFramed; use fnv::FnvHashMap; -use futures::prelude::*; use futures::SinkExt; +use futures::prelude::*; +use libp2p::PeerId; use libp2p::swarm::handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, StreamUpgradeError, SubstreamProtocol, }; use libp2p::swarm::{ConnectionId, Stream}; -use libp2p::PeerId; use logging::crit; use smallvec::SmallVec; use std::{ - collections::{hash_map::Entry, VecDeque}, + collections::{VecDeque, hash_map::Entry}, pin::Pin, sync::Arc, task::{Context, Poll}, time::{Duration, Instant}, }; -use tokio::time::{sleep, Sleep}; -use tokio_util::time::{delay_queue, DelayQueue}; +use tokio::time::{Sleep, sleep}; +use tokio_util::time::{DelayQueue, delay_queue}; use tracing::{debug, trace}; -use types::{EthSpec, ForkContext}; +use types::{EthSpec, ForkContext, Slot}; /// The number of times to retry an outbound upgrade in the case of IO errors. const IO_ERROR_RETRIES: u8 = 3; @@ -39,6 +39,9 @@ const SHUTDOWN_TIMEOUT_SECS: u64 = 15; /// Maximum number of simultaneous inbound substreams we keep for this peer. const MAX_INBOUND_SUBSTREAMS: usize = 32; +/// Timeout that will be used for inbound and outbound responses. +const RESP_TIMEOUT: Duration = Duration::from_secs(10); + /// Identifier of inbound and outbound substreams from the handler's perspective. #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] pub struct SubstreamId(usize); @@ -140,9 +143,6 @@ where /// Waker, to be sure the handler gets polled when needed. waker: Option, - - /// Timeout that will be used for inbound and outbound responses. - resp_timeout: Duration, } enum HandlerState { @@ -224,7 +224,6 @@ where pub fn new( listen_protocol: SubstreamProtocol, ()>, fork_context: Arc, - resp_timeout: Duration, peer_id: PeerId, connection_id: ConnectionId, ) -> Self { @@ -246,7 +245,6 @@ where outbound_io_error_retries: 0, fork_context, waker: None, - resp_timeout, } } @@ -377,7 +375,7 @@ where ConnectionHandlerEvent, > { if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { + if !waker.will_wake(cx.waker()) { self.waker = Some(cx.waker().clone()); } } else { @@ -542,8 +540,7 @@ where // If this substream has not ended, we reset the timer. // Each chunk is allowed RESPONSE_TIMEOUT to be sent. if let Some(ref delay_key) = info.delay_key { - self.inbound_substreams_delay - .reset(delay_key, self.resp_timeout); + self.inbound_substreams_delay.reset(delay_key, RESP_TIMEOUT); } // The stream may be currently idle. Attempt to process more @@ -712,7 +709,7 @@ where }; substream_entry.max_remaining_chunks = Some(max_remaining_chunks); self.outbound_substreams_delay - .reset(delay_key, self.resp_timeout); + .reset(delay_key, RESP_TIMEOUT); } } @@ -848,23 +845,22 @@ where } // Check if we have completed sending a goodbye, disconnect. - if let HandlerState::ShuttingDown(_) = self.state { - if self.dial_queue.is_empty() - && self.outbound_substreams.is_empty() - && self.inbound_substreams.is_empty() - && self.events_out.is_empty() - && self.dial_negotiated == 0 - { - debug!( - peer_id = %self.peer_id, - connection_id = %self.connection_id, - "Goodbye sent, Handler deactivated" - ); - self.state = HandlerState::Deactivated; - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::Close(RPCError::Disconnected), - )); - } + if let HandlerState::ShuttingDown(_) = self.state + && self.dial_queue.is_empty() + && self.outbound_substreams.is_empty() + && self.inbound_substreams.is_empty() + && self.events_out.is_empty() + && self.dial_negotiated == 0 + { + debug!( + peer_id = %self.peer_id, + connection_id = %self.connection_id, + "Goodbye sent, Handler deactivated" + ); + self.state = HandlerState::Deactivated; + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + HandlerEvent::Close(RPCError::Disconnected), + )); } Poll::Pending @@ -912,7 +908,7 @@ where } let (req, substream) = substream; - let current_fork = self.fork_context.current_fork(); + let current_fork = self.fork_context.current_fork_name(); let spec = &self.fork_context.spec; match &req { @@ -932,9 +928,8 @@ where } } RequestType::BlobsByRange(request) => { - let max_requested_blobs = request - .count - .saturating_mul(spec.max_blobs_per_block_by_fork(current_fork)); + let epoch = Slot::new(request.start_slot).epoch(E::slots_per_epoch()); + let max_requested_blobs = request.max_blobs_requested(epoch, spec); let max_allowed = spec.max_request_blob_sidecars(current_fork) as u64; if max_requested_blobs > max_allowed { self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { @@ -951,8 +946,10 @@ where _ => {} }; - let max_responses = - req.max_responses(self.fork_context.current_fork(), &self.fork_context.spec); + let max_responses = req.max_responses( + self.fork_context.current_fork_epoch(), + &self.fork_context.spec, + ); // store requests that expect responses if max_responses > 0 { @@ -960,7 +957,7 @@ where // Store the stream and tag the output. let delay_key = self .inbound_substreams_delay - .insert(self.current_inbound_substream_id, self.resp_timeout); + .insert(self.current_inbound_substream_id, RESP_TIMEOUT); let awaiting_stream = InboundState::Idle(substream); self.inbound_substreams.insert( self.current_inbound_substream_id, @@ -1022,8 +1019,10 @@ where } // add the stream to substreams if we expect a response, otherwise drop the stream. - let max_responses = - request.max_responses(self.fork_context.current_fork(), &self.fork_context.spec); + let max_responses = request.max_responses( + self.fork_context.current_fork_epoch(), + &self.fork_context.spec, + ); if max_responses > 0 { let max_remaining_chunks = if request.expect_exactly_one_response() { // Currently enforced only for multiple responses @@ -1034,7 +1033,7 @@ where // new outbound request. Store the stream and tag the output. let delay_key = self .outbound_substreams_delay - .insert(self.current_outbound_substream_id, self.resp_timeout); + .insert(self.current_outbound_substream_id, RESP_TIMEOUT); let awaiting_stream = OutboundSubstreamState::RequestPendingResponse { substream: Box::new(substream), request, diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 9fe2fef9e8..a9b4aa2fba 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -5,7 +5,7 @@ use regex::bytes::Regex; use serde::Serialize; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use ssz_types::{typenum::U256, VariableList}; +use ssz_types::{RuntimeVariableList, VariableList, typenum::U256}; use std::fmt::Display; use std::marker::PhantomData; use std::ops::Deref; @@ -15,12 +15,11 @@ use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::light_client_update::MAX_REQUEST_LIGHT_CLIENT_UPDATES; use types::{ - blob_sidecar::BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, - DataColumnsByRootIdentifier, Epoch, EthSpec, Hash256, LightClientBootstrap, - LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, - SignedBeaconBlock, Slot, + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnsByRootIdentifier, Epoch, EthSpec, + ForkContext, Hash256, LightClientBootstrap, LightClientFinalityUpdate, + LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, Slot, + blob_sidecar::BlobSidecar, }; -use types::{ForkContext, ForkName}; /// Maximum length of error message. pub type MaxErrorLen = U256; @@ -30,15 +29,21 @@ pub const MAX_ERROR_LEN: u64 = 256; #[derive(Debug, Clone)] pub struct ErrorType(pub VariableList); -impl From for ErrorType { - fn from(s: String) -> Self { - Self(VariableList::from(s.as_bytes().to_vec())) +impl From<&str> for ErrorType { + // This will truncate the error if `string.as_bytes()` exceeds `MaxErrorLen`. + fn from(s: &str) -> Self { + let mut bytes = s.as_bytes().to_vec(); + bytes.truncate(MAX_ERROR_LEN as usize); + Self( + VariableList::try_from(bytes) + .expect("length should not exceed MaxErrorLen after truncation"), + ) } } -impl From<&str> for ErrorType { - fn from(s: &str) -> Self { - Self(VariableList::from(s.as_bytes().to_vec())) +impl From for ErrorType { + fn from(s: String) -> Self { + Self::from(s.as_str()) } } @@ -64,7 +69,11 @@ impl Display for ErrorType { /* Requests */ /// The STATUS request/response handshake message. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq),) +)] +#[derive(Clone, Debug, PartialEq)] pub struct StatusMessage { /// The fork version of the chain we are broadcasting. pub fork_digest: [u8; 4], @@ -80,6 +89,43 @@ pub struct StatusMessage { /// The slot associated with the latest block root. pub head_slot: Slot, + + /// The slot after which we guarantee to have all the blocks + /// and blobs/data columns that we currently advertise. + #[superstruct(only(V2))] + pub earliest_available_slot: Slot, +} + +impl StatusMessage { + pub fn status_v1(&self) -> StatusMessageV1 { + match &self { + Self::V1(status) => status.clone(), + Self::V2(status) => StatusMessageV1 { + fork_digest: status.fork_digest, + finalized_root: status.finalized_root, + finalized_epoch: status.finalized_epoch, + head_root: status.head_root, + head_slot: status.head_slot, + }, + } + } + + pub fn status_v2(&self) -> StatusMessageV2 { + match &self { + Self::V1(status) => StatusMessageV2 { + fork_digest: status.fork_digest, + finalized_root: status.finalized_root, + finalized_epoch: status.finalized_epoch, + head_root: status.head_root, + head_slot: status.head_slot, + // Note: we always produce a V2 message as our local + // status message, so this match arm should ideally never + // be invoked in lighthouse. + earliest_available_slot: Slot::new(0), + }, + Self::V2(status) => status.clone(), + } + } } /// The PING request/response message. @@ -328,8 +374,8 @@ pub struct BlobsByRangeRequest { } impl BlobsByRangeRequest { - pub fn max_blobs_requested(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { - let max_blobs_per_block = spec.max_blobs_per_block_by_fork(current_fork); + pub fn max_blobs_requested(&self, epoch: Epoch, spec: &ChainSpec) -> u64 { + let max_blobs_per_block = spec.max_blobs_per_block(epoch); self.count.saturating_mul(max_blobs_per_block) } } @@ -360,11 +406,11 @@ impl DataColumnsByRangeRequest { .len() } - pub fn ssz_max_len(spec: &ChainSpec) -> usize { + pub fn ssz_max_len() -> usize { DataColumnsByRangeRequest { start_slot: 0, count: 0, - columns: vec![0; spec.number_of_columns as usize], + columns: vec![0; E::number_of_columns()], } .as_ssz_bytes() .len() @@ -441,20 +487,22 @@ pub struct BlocksByRootRequest { } impl BlocksByRootRequest { - pub fn new(block_roots: Vec, fork_context: &ForkContext) -> Self { + pub fn new(block_roots: Vec, fork_context: &ForkContext) -> Result { let max_request_blocks = fork_context .spec - .max_request_blocks(fork_context.current_fork()); - let block_roots = RuntimeVariableList::from_vec(block_roots, max_request_blocks); - Self::V2(BlocksByRootRequestV2 { block_roots }) + .max_request_blocks(fork_context.current_fork_name()); + let block_roots = RuntimeVariableList::new(block_roots, max_request_blocks) + .map_err(|e| format!("BlocksByRootRequestV2 too many roots: {e:?}"))?; + Ok(Self::V2(BlocksByRootRequestV2 { block_roots })) } - pub fn new_v1(block_roots: Vec, fork_context: &ForkContext) -> Self { + pub fn new_v1(block_roots: Vec, fork_context: &ForkContext) -> Result { let max_request_blocks = fork_context .spec - .max_request_blocks(fork_context.current_fork()); - let block_roots = RuntimeVariableList::from_vec(block_roots, max_request_blocks); - Self::V1(BlocksByRootRequestV1 { block_roots }) + .max_request_blocks(fork_context.current_fork_name()); + let block_roots = RuntimeVariableList::new(block_roots, max_request_blocks) + .map_err(|e| format!("BlocksByRootRequestV1 too many roots: {e:?}"))?; + Ok(Self::V1(BlocksByRootRequestV1 { block_roots })) } } @@ -466,29 +514,31 @@ pub struct BlobsByRootRequest { } impl BlobsByRootRequest { - pub fn new(blob_ids: Vec, fork_context: &ForkContext) -> Self { + pub fn new(blob_ids: Vec, fork_context: &ForkContext) -> Result { let max_request_blob_sidecars = fork_context .spec - .max_request_blob_sidecars(fork_context.current_fork()); - let blob_ids = RuntimeVariableList::from_vec(blob_ids, max_request_blob_sidecars); - Self { blob_ids } + .max_request_blob_sidecars(fork_context.current_fork_name()); + let blob_ids = RuntimeVariableList::new(blob_ids, max_request_blob_sidecars) + .map_err(|e| format!("BlobsByRootRequestV1 too many blob IDs: {e:?}"))?; + Ok(Self { blob_ids }) } } /// Request a number of data columns from a peer. #[derive(Clone, Debug, PartialEq)] -pub struct DataColumnsByRootRequest { +pub struct DataColumnsByRootRequest { /// The list of beacon block roots and column indices being requested. - pub data_column_ids: RuntimeVariableList, + pub data_column_ids: RuntimeVariableList>, } -impl DataColumnsByRootRequest { +impl DataColumnsByRootRequest { pub fn new( - data_column_ids: Vec, + data_column_ids: Vec>, max_request_blocks: usize, - ) -> Self { - let data_column_ids = RuntimeVariableList::from_vec(data_column_ids, max_request_blocks); - Self { data_column_ids } + ) -> Result { + let data_column_ids = RuntimeVariableList::new(data_column_ids, max_request_blocks) + .map_err(|_| "DataColumnsByRootRequest too many column IDs")?; + Ok(Self { data_column_ids }) } pub fn max_requested(&self) -> usize { @@ -709,6 +759,23 @@ impl RpcSuccessResponse { RpcSuccessResponse::LightClientUpdatesByRange(_) => Protocol::LightClientUpdatesByRange, } } + + pub fn slot(&self) -> Option { + match self { + Self::BlocksByRange(r) | Self::BlocksByRoot(r) => Some(r.slot()), + Self::BlobsByRange(r) | Self::BlobsByRoot(r) => { + Some(r.signed_block_header.message.slot) + } + Self::DataColumnsByRange(r) | Self::DataColumnsByRoot(r) => { + Some(r.signed_block_header.message.slot) + } + Self::LightClientBootstrap(r) => Some(r.get_slot()), + Self::LightClientFinalityUpdate(r) => Some(r.get_attested_header_slot()), + Self::LightClientOptimisticUpdate(r) => Some(r.get_slot()), + Self::LightClientUpdatesByRange(r) => Some(r.attested_header_slot()), + Self::MetaData(_) | Self::Status(_) | Self::Pong(_) => None, + } + } } impl std::fmt::Display for RpcErrorResponse { @@ -727,7 +794,16 @@ impl std::fmt::Display for RpcErrorResponse { impl std::fmt::Display for StatusMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Status Message: Fork Digest: {:?}, Finalized Root: {}, Finalized Epoch: {}, Head Root: {}, Head Slot: {}", self.fork_digest, self.finalized_root, self.finalized_epoch, self.head_root, self.head_slot) + write!( + f, + "Status Message: Fork Digest: {:?}, Finalized Root: {}, Finalized Epoch: {}, Head Root: {}, Head Slot: {}, Earliest available slot: {:?}", + self.fork_digest(), + self.finalized_root(), + self.finalized_epoch(), + self.head_root(), + self.head_slot(), + self.earliest_available_slot() + ) } } @@ -858,7 +934,7 @@ impl std::fmt::Display for BlobsByRangeRequest { } } -impl std::fmt::Display for DataColumnsByRootRequest { +impl std::fmt::Display for DataColumnsByRootRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 8cb720132a..7c43018af8 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -5,25 +5,22 @@ //! syncing. use handler::RPCHandler; +use libp2p::PeerId; use libp2p::core::transport::PortUse; use libp2p::swarm::{ - handler::ConnectionHandler, CloseConnection, ConnectionId, NetworkBehaviour, NotifyHandler, - ToSwarm, + CloseConnection, ConnectionId, NetworkBehaviour, NotifyHandler, ToSwarm, + handler::ConnectionHandler, }; use libp2p::swarm::{ConnectionClosed, FromSwarm, SubstreamProtocol, THandlerInEvent}; -use libp2p::PeerId; use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; use std::task::{Context, Poll}; -use std::time::Duration; -use tracing::{debug, error, instrument, trace}; +use tracing::{debug, trace}; use types::{EthSpec, ForkContext}; pub(crate) use handler::{HandlerErr, HandlerEvent}; -pub(crate) use methods::{ - MetaData, MetaDataV1, MetaDataV2, MetaDataV3, Ping, RpcResponse, RpcSuccessResponse, -}; +pub(crate) use methods::{MetaData, MetaDataV2, MetaDataV3, Ping, RpcResponse, RpcSuccessResponse}; pub use protocol::RequestType; use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; @@ -100,6 +97,13 @@ pub struct InboundRequestId { substream_id: SubstreamId, } +// An Active inbound request received via Rpc. +struct ActiveInboundRequest { + pub peer_id: PeerId, + pub request_type: RequestType, + pub peer_disconnected: bool, +} + impl InboundRequestId { /// Creates an _unchecked_ [`InboundRequestId`]. /// @@ -138,12 +142,6 @@ pub struct RPCMessage { type BehaviourAction = ToSwarm, RPCSend>; -pub struct NetworkParams { - pub max_payload_size: usize, - pub ttfb_timeout: Duration, - pub resp_timeout: Duration, -} - /// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level /// logic. pub struct RPC { @@ -152,30 +150,21 @@ pub struct RPC { /// Rate limiter for our own requests. outbound_request_limiter: SelfRateLimiter, /// Active inbound requests that are awaiting a response. - active_inbound_requests: HashMap)>, + active_inbound_requests: HashMap>, /// Queue of events to be processed. events: Vec>, fork_context: Arc, enable_light_client_server: bool, - /// Networking constant values - network_params: NetworkParams, /// A sequential counter indicating when data gets modified. seq_number: u64, } impl RPC { - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] pub fn new( fork_context: Arc, enable_light_client_server: bool, inbound_rate_limiter_config: Option, outbound_rate_limiter_config: Option, - network_params: NetworkParams, seq_number: u64, ) -> Self { let response_limiter = inbound_rate_limiter_config.map(|config| { @@ -195,30 +184,24 @@ impl RPC { events: Vec::new(), fork_context, enable_light_client_server, - network_params, seq_number, } } /// Sends an RPC response. - /// - /// The peer must be connected for this to succeed. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] + /// Returns an `Err` if the request does exist in the active inbound requests list. pub fn send_response( &mut self, - peer_id: PeerId, request_id: InboundRequestId, response: RpcResponse, - ) { - let Some((_peer_id, request_type)) = self.active_inbound_requests.remove(&request_id) + ) -> Result<(), RpcResponse> { + let Some(ActiveInboundRequest { + peer_id, + request_type, + peer_disconnected, + }) = self.active_inbound_requests.remove(&request_id) else { - error!(%peer_id, ?request_id, %response, "Request not found in active_inbound_requests. Response not sent"); - return; + return Err(response); }; // Add the request back to active requests if the response is `Success` and requires stream @@ -226,11 +209,24 @@ impl RPC { if request_type.protocol().terminator().is_some() && matches!(response, RpcResponse::Success(_)) { - self.active_inbound_requests - .insert(request_id, (peer_id, request_type.clone())); + self.active_inbound_requests.insert( + request_id, + ActiveInboundRequest { + peer_id, + request_type: request_type.clone(), + peer_disconnected, + }, + ); + } + + if peer_disconnected { + trace!(%peer_id, ?request_id, %response, + "Discarding response, peer is no longer connected"); + return Ok(()); } self.send_response_inner(peer_id, request_type.protocol(), request_id, response); + Ok(()) } fn send_response_inner( @@ -240,17 +236,17 @@ impl RPC { request_id: InboundRequestId, response: RpcResponse, ) { - if let Some(response_limiter) = self.response_limiter.as_mut() { - if !response_limiter.allows( + if let Some(response_limiter) = self.response_limiter.as_mut() + && !response_limiter.allows( peer_id, protocol, request_id.connection_id, request_id.substream_id, response.clone(), - ) { - // Response is logged and queued internally in the response limiter. - return; - } + ) + { + // Response is logged and queued internally in the response limiter. + return; } self.events.push(ToSwarm::NotifyHandler { @@ -263,12 +259,6 @@ impl RPC { /// Submits an RPC request. /// /// The peer must be connected for this to succeed. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] pub fn send_request(&mut self, peer_id: PeerId, request_id: Id, req: RequestType) { match self .outbound_request_limiter @@ -287,12 +277,6 @@ impl RPC { /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This /// gracefully terminates the RPC behaviour with a goodbye message. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] pub fn shutdown(&mut self, peer_id: PeerId, id: Id, reason: GoodbyeReason) { self.events.push(ToSwarm::NotifyHandler { peer_id, @@ -301,23 +285,11 @@ impl RPC { }); } - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] pub fn update_seq_number(&mut self, seq_number: u64) { self.seq_number = seq_number } /// Send a Ping request to the destination `PeerId` via `ConnectionId`. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p_rpc"), - name = "libp2p_rpc", - skip_all - )] pub fn ping(&mut self, peer_id: PeerId, id: Id) { let ping = Ping { data: self.seq_number, @@ -348,18 +320,11 @@ where max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, phantom: PhantomData, - ttfb_timeout: self.network_params.ttfb_timeout, }, (), ); - let handler = RPCHandler::new( - protocol, - self.fork_context.clone(), - self.network_params.resp_timeout, - peer_id, - connection_id, - ); + let handler = RPCHandler::new(protocol, self.fork_context.clone(), peer_id, connection_id); Ok(handler) } @@ -378,18 +343,11 @@ where max_rpc_size: self.fork_context.spec.max_payload_size as usize, enable_light_client_server: self.enable_light_client_server, phantom: PhantomData, - ttfb_timeout: self.network_params.ttfb_timeout, }, (), ); - let handler = RPCHandler::new( - protocol, - self.fork_context.clone(), - self.network_params.resp_timeout, - peer_id, - connection_id, - ); + let handler = RPCHandler::new(protocol, self.fork_context.clone(), peer_id, connection_id); Ok(handler) } @@ -427,9 +385,10 @@ where self.events.push(error_msg); } - self.active_inbound_requests.retain( - |_inbound_request_id, (request_peer_id, _request_type)| *request_peer_id != peer_id, - ); + self.active_inbound_requests + .values_mut() + .filter(|request| request.peer_id == peer_id) + .for_each(|request| request.peer_disconnected = true); if let Some(limiter) = self.response_limiter.as_mut() { limiter.peer_disconnected(peer_id); @@ -470,9 +429,17 @@ where .active_inbound_requests .iter() .filter( - |(_inbound_request_id, (request_peer_id, active_request_type))| { + |( + _inbound_request_id, + ActiveInboundRequest { + peer_id: request_peer_id, + request_type: active_request_type, + peer_disconnected, + }, + )| { *request_peer_id == peer_id && active_request_type.protocol() == request_type.protocol() + && !peer_disconnected }, ) .count() @@ -496,19 +463,25 @@ where } // Requests that are below the limit on the number of simultaneous requests are added to the active inbound requests. - self.active_inbound_requests - .insert(request_id, (peer_id, request_type.clone())); + self.active_inbound_requests.insert( + request_id, + ActiveInboundRequest { + peer_id, + request_type: request_type.clone(), + peer_disconnected: false, + }, + ); // If we received a Ping, we queue a Pong response. if let RequestType::Ping(_) = request_type { trace!(connection_id = %connection_id, %peer_id, "Received Ping, queueing Pong"); self.send_response( - peer_id, request_id, RpcResponse::Success(RpcSuccessResponse::Pong(Ping { data: self.seq_number, })), - ); + ) + .expect("Request to exist"); } self.events.push(ToSwarm::GenerateEvent(RPCMessage { @@ -566,15 +539,15 @@ where } fn poll(&mut self, cx: &mut Context) -> Poll>> { - if let Some(response_limiter) = self.response_limiter.as_mut() { - if let Poll::Ready(responses) = response_limiter.poll_ready(cx) { - for response in responses { - self.events.push(ToSwarm::NotifyHandler { - peer_id: response.peer_id, - handler: NotifyHandler::One(response.connection_id), - event: RPCSend::Response(response.substream_id, response.response), - }); - } + if let Some(response_limiter) = self.response_limiter.as_mut() + && let Poll::Ready(responses) = response_limiter.poll_ready(cx) + { + for response in responses { + self.events.push(ToSwarm::NotifyHandler { + peer_id: response.peer_id, + handler: NotifyHandler::One(response.connection_id), + event: RPCSend::Response(response.substream_id, response.response), + }); } } diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index b614313a84..3fbc279d00 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -1,6 +1,6 @@ -use super::protocol::ProtocolId; use super::RPCError; use super::RequestType; +use super::protocol::ProtocolId; use crate::rpc::codec::SSZSnappyOutboundCodec; use crate::rpc::protocol::Encoding; use futures::future::BoxFuture; diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 44d0a5b04d..c0b4da097b 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -1,5 +1,6 @@ use super::methods::*; use crate::rpc::codec::SSZSnappyInboundCodec; +use bls::Signature; use futures::future::BoxFuture; use futures::prelude::{AsyncRead, AsyncWrite}; use futures::{FutureExt, StreamExt}; @@ -11,17 +12,16 @@ use std::marker::PhantomData; use std::sync::{Arc, LazyLock}; use std::time::Duration; use strum::{AsRefStr, Display, EnumString, IntoStaticStr}; -use tokio_io_timeout::TimeoutStream; use tokio_util::{ codec::Framed, compat::{Compat, FuturesAsyncReadCompatExt}, }; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BlobSidecar, ChainSpec, DataColumnSidecar, - EmptyBlock, EthSpec, EthSpecId, ForkContext, ForkName, LightClientBootstrap, + EmptyBlock, Epoch, EthSpec, EthSpecId, ForkContext, ForkName, LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientUpdate, - MainnetEthSpec, MinimalEthSpec, Signature, SignedBeaconBlock, + MainnetEthSpec, MinimalEthSpec, SignedBeaconBlock, }; // Note: Hardcoding the `EthSpec` type for `SignedBeaconBlock` as min/max values is @@ -71,13 +71,15 @@ pub static BLOB_SIDECAR_SIZE_MINIMAL: LazyLock = LazyLock::new(BlobSidecar::::max_size); pub static ERROR_TYPE_MIN: LazyLock = LazyLock::new(|| { - VariableList::::from(Vec::::new()) + VariableList::::try_from(Vec::::new()) + .expect("MaxErrorLen should not exceed MAX_ERROR_LEN") .as_ssz_bytes() .len() }); pub static ERROR_TYPE_MAX: LazyLock = LazyLock::new(|| { - VariableList::::from(vec![0u8; MAX_ERROR_LEN as usize]) + VariableList::::try_from(vec![0u8; MAX_ERROR_LEN as usize]) + .expect("MaxErrorLen should not exceed MAX_ERROR_LEN") .as_ssz_bytes() .len() }); @@ -158,7 +160,7 @@ fn rpc_light_client_updates_by_range_limits_by_fork(current_fork: ForkName) -> R ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_DENEB_MAX) } - ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + ForkName::Electra | ForkName::Fulu | ForkName::Eip7805 | ForkName::Gloas => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_ELECTRA_MAX) } } @@ -178,7 +180,7 @@ fn rpc_light_client_finality_update_limits_by_fork(current_fork: ForkName) -> Rp ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX) } - ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + ForkName::Electra | ForkName::Fulu | ForkName::Eip7805 | ForkName::Gloas => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX) } } @@ -199,7 +201,7 @@ fn rpc_light_client_optimistic_update_limits_by_fork(current_fork: ForkName) -> ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX) } - ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => RpcLimits::new( + ForkName::Electra | ForkName::Fulu | ForkName::Eip7805 | ForkName::Gloas => RpcLimits::new( altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX, ), @@ -216,7 +218,7 @@ fn rpc_light_client_bootstrap_limits_by_fork(current_fork: ForkName) -> RpcLimit } ForkName::Capella => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX), ForkName::Deneb => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX), - ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + ForkName::Electra | ForkName::Fulu | ForkName::Eip7805 | ForkName::Gloas => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX) } } @@ -298,6 +300,7 @@ pub enum Encoding { #[derive(Debug, Clone, Copy, PartialEq)] pub enum SupportedProtocol { StatusV1, + StatusV2, GoodbyeV1, BlocksByRangeV1, BlocksByRangeV2, @@ -321,6 +324,7 @@ impl SupportedProtocol { pub fn version_string(&self) -> &'static str { match self { SupportedProtocol::StatusV1 => "1", + SupportedProtocol::StatusV2 => "2", SupportedProtocol::GoodbyeV1 => "1", SupportedProtocol::BlocksByRangeV1 => "1", SupportedProtocol::BlocksByRangeV2 => "2", @@ -344,6 +348,7 @@ impl SupportedProtocol { pub fn protocol(&self) -> Protocol { match self { SupportedProtocol::StatusV1 => Protocol::Status, + SupportedProtocol::StatusV2 => Protocol::Status, SupportedProtocol::GoodbyeV1 => Protocol::Goodbye, SupportedProtocol::BlocksByRangeV1 => Protocol::BlocksByRange, SupportedProtocol::BlocksByRangeV2 => Protocol::BlocksByRange, @@ -368,6 +373,7 @@ impl SupportedProtocol { fn currently_supported(fork_context: &ForkContext) -> Vec { let mut supported = vec![ + ProtocolId::new(Self::StatusV2, Encoding::SSZSnappy), ProtocolId::new(Self::StatusV1, Encoding::SSZSnappy), ProtocolId::new(Self::GoodbyeV1, Encoding::SSZSnappy), // V2 variants have higher preference then V1 @@ -421,7 +427,6 @@ pub struct RPCProtocol { pub max_rpc_size: usize, pub enable_light_client_server: bool, pub phantom: PhantomData, - pub ttfb_timeout: Duration, } impl UpgradeInfo for RPCProtocol { @@ -489,11 +494,11 @@ impl AsRef for ProtocolId { impl ProtocolId { /// Returns min and max size for messages of given protocol id requests. - pub fn rpc_request_limits(&self, spec: &ChainSpec) -> RpcLimits { + pub fn rpc_request_limits(&self, spec: &ChainSpec) -> RpcLimits { match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + ::ssz_fixed_len(), + ::ssz_fixed_len(), ), Protocol::Goodbye => RpcLimits::new( ::ssz_fixed_len(), @@ -513,7 +518,7 @@ impl ProtocolId { Protocol::DataColumnsByRoot => RpcLimits::new(0, spec.max_data_columns_by_root_request), Protocol::DataColumnsByRange => RpcLimits::new( DataColumnsByRangeRequest::ssz_min_len(), - DataColumnsByRangeRequest::ssz_max_len(spec), + DataColumnsByRangeRequest::ssz_max_len::(), ), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), @@ -537,19 +542,19 @@ impl ProtocolId { pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + ::ssz_fixed_len(), + ::ssz_fixed_len(), ), Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response - Protocol::BlocksByRange => rpc_block_limits_by_fork(fork_context.current_fork()), - Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork()), + Protocol::BlocksByRange => rpc_block_limits_by_fork(fork_context.current_fork_name()), + Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork_name()), Protocol::BlobsByRange => rpc_blob_limits::(), Protocol::BlobsByRoot => rpc_blob_limits::(), Protocol::DataColumnsByRoot => { - rpc_data_column_limits::(fork_context.current_fork(), &fork_context.spec) + rpc_data_column_limits::(fork_context.current_fork_epoch(), &fork_context.spec) } Protocol::DataColumnsByRange => { - rpc_data_column_limits::(fork_context.current_fork(), &fork_context.spec) + rpc_data_column_limits::(fork_context.current_fork_epoch(), &fork_context.spec) } Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), @@ -560,16 +565,16 @@ impl ProtocolId { as Encode>::ssz_fixed_len(), ), Protocol::LightClientBootstrap => { - rpc_light_client_bootstrap_limits_by_fork(fork_context.current_fork()) + rpc_light_client_bootstrap_limits_by_fork(fork_context.current_fork_name()) } Protocol::LightClientOptimisticUpdate => { - rpc_light_client_optimistic_update_limits_by_fork(fork_context.current_fork()) + rpc_light_client_optimistic_update_limits_by_fork(fork_context.current_fork_name()) } Protocol::LightClientFinalityUpdate => { - rpc_light_client_finality_update_limits_by_fork(fork_context.current_fork()) + rpc_light_client_finality_update_limits_by_fork(fork_context.current_fork_name()) } Protocol::LightClientUpdatesByRange => { - rpc_light_client_updates_by_range_limits_by_fork(fork_context.current_fork()) + rpc_light_client_updates_by_range_limits_by_fork(fork_context.current_fork_name()) } } } @@ -589,6 +594,7 @@ impl ProtocolId { | SupportedProtocol::LightClientFinalityUpdateV1 | SupportedProtocol::LightClientUpdatesByRangeV1 => true, SupportedProtocol::StatusV1 + | SupportedProtocol::StatusV2 | SupportedProtocol::BlocksByRootV1 | SupportedProtocol::BlocksByRangeV1 | SupportedProtocol::PingV1 @@ -630,10 +636,13 @@ pub fn rpc_blob_limits() -> RpcLimits { } } -pub fn rpc_data_column_limits(fork_name: ForkName, spec: &ChainSpec) -> RpcLimits { +pub fn rpc_data_column_limits( + current_digest_epoch: Epoch, + spec: &ChainSpec, +) -> RpcLimits { RpcLimits::new( DataColumnSidecar::::min_size(), - DataColumnSidecar::::max_size(spec.max_blobs_per_block_by_fork(fork_name) as usize), + DataColumnSidecar::::max_size(spec.max_blobs_per_block(current_digest_epoch) as usize), ) } @@ -644,7 +653,7 @@ pub fn rpc_data_column_limits(fork_name: ForkName, spec: &ChainSpec) pub type InboundOutput = (RequestType, InboundFramed); pub type InboundFramed = - Framed>>>, SSZSnappyInboundCodec>; + Framed>>, SSZSnappyInboundCodec>; impl InboundUpgrade for RPCProtocol where @@ -668,10 +677,7 @@ where ), }; - let mut timed_socket = TimeoutStream::new(socket); - timed_socket.set_read_timeout(Some(self.ttfb_timeout)); - - let socket = Framed::new(Box::pin(timed_socket), codec); + let socket = Framed::new(Box::pin(socket), codec); // MetaData requests should be empty, return the stream match versioned_protocol { @@ -717,7 +723,7 @@ pub enum RequestType { BlocksByRoot(BlocksByRootRequest), BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), - DataColumnsByRoot(DataColumnsByRootRequest), + DataColumnsByRoot(DataColumnsByRootRequest), DataColumnsByRange(DataColumnsByRangeRequest), LightClientBootstrap(LightClientBootstrapRequest), LightClientOptimisticUpdate, @@ -732,13 +738,13 @@ impl RequestType { /* These functions are used in the handler for stream management */ /// Maximum number of responses expected for this request. - pub fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { + pub fn max_responses(&self, digest_epoch: Epoch, spec: &ChainSpec) -> u64 { match self { RequestType::Status(_) => 1, RequestType::Goodbye(_) => 0, RequestType::BlocksByRange(req) => *req.count(), RequestType::BlocksByRoot(req) => req.block_roots().len() as u64, - RequestType::BlobsByRange(req) => req.max_blobs_requested(current_fork, spec), + RequestType::BlobsByRange(req) => req.max_blobs_requested(digest_epoch, spec), RequestType::BlobsByRoot(req) => req.blob_ids.len() as u64, RequestType::DataColumnsByRoot(req) => req.max_requested() as u64, RequestType::DataColumnsByRange(req) => req.max_requested::(), @@ -754,7 +760,10 @@ impl RequestType { /// Gives the corresponding `SupportedProtocol` to this request. pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - RequestType::Status(_) => SupportedProtocol::StatusV1, + RequestType::Status(req) => match req { + StatusMessage::V1(_) => SupportedProtocol::StatusV1, + StatusMessage::V2(_) => SupportedProtocol::StatusV2, + }, RequestType::Goodbye(_) => SupportedProtocol::GoodbyeV1, RequestType::BlocksByRange(req) => match req { OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, @@ -813,10 +822,10 @@ impl RequestType { pub fn supported_protocols(&self) -> Vec { match self { // add more protocols when versions/encodings are supported - RequestType::Status(_) => vec![ProtocolId::new( - SupportedProtocol::StatusV1, - Encoding::SSZSnappy, - )], + RequestType::Status(_) => vec![ + ProtocolId::new(SupportedProtocol::StatusV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy), + ], RequestType::Goodbye(_) => vec![ProtocolId::new( SupportedProtocol::GoodbyeV1, Encoding::SSZSnappy, @@ -1019,7 +1028,7 @@ impl RPCError { /// Used for metrics. pub fn as_static_str(&self) -> &'static str { match self { - RPCError::ErrorResponse(ref code, ..) => code.into(), + RPCError::ErrorResponse(code, ..) => code.into(), e => e.into(), } } diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index f666c30d52..8b364f506c 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -1,3 +1,5 @@ +#![deny(clippy::arithmetic_side_effects)] + use super::config::RateLimiterConfig; use crate::rpc::Protocol; use fnv::FnvHashMap; @@ -5,12 +7,13 @@ use libp2p::PeerId; use serde::{Deserialize, Serialize}; use std::future::Future; use std::hash::Hash; +use std::num::NonZeroU64; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; use tokio::time::Interval; -use types::{ChainSpec, EthSpec, ForkContext, ForkName}; +use types::{ChainSpec, Epoch, EthSpec, ForkContext}; /// Nanoseconds since a given time. // Maintained as u64 to reduce footprint @@ -55,7 +58,7 @@ pub struct Quota { pub(super) replenish_all_every: Duration, /// Token limit. This translates on how large can an instantaneous batch of /// tokens be. - pub(super) max_tokens: u64, + pub(super) max_tokens: NonZeroU64, } impl Quota { @@ -63,12 +66,12 @@ impl Quota { pub const fn one_every(seconds: u64) -> Self { Quota { replenish_all_every: Duration::from_secs(seconds), - max_tokens: 1, + max_tokens: NonZeroU64::new(1).unwrap(), } } /// Allow `n` tokens to be use used every `seconds`. - pub const fn n_every(n: u64, seconds: u64) -> Self { + pub const fn n_every(n: NonZeroU64, seconds: u64) -> Self { Quota { replenish_all_every: Duration::from_secs(seconds), max_tokens: n, @@ -236,7 +239,9 @@ impl RPCRateLimiterBuilder { // check for peers to prune every 30 seconds, starting in 30 seconds let prune_every = tokio::time::Duration::from_secs(30); - let prune_start = tokio::time::Instant::now() + prune_every; + let prune_start = tokio::time::Instant::now() + .checked_add(prune_every) + .ok_or("prune time overflow")?; let prune_interval = tokio::time::interval_at(prune_start, prune_every); Ok(RPCRateLimiter { prune_interval, @@ -262,7 +267,7 @@ impl RPCRateLimiterBuilder { pub trait RateLimiterItem { fn protocol(&self) -> Protocol; - fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64; + fn max_responses(&self, digest_epoch: Epoch, spec: &ChainSpec) -> u64; } impl RateLimiterItem for super::RequestType { @@ -270,8 +275,8 @@ impl RateLimiterItem for super::RequestType { self.versioned_protocol().protocol() } - fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { - self.max_responses(current_fork, spec) + fn max_responses(&self, digest_epoch: Epoch, spec: &ChainSpec) -> u64 { + self.max_responses(digest_epoch, spec) } } @@ -280,7 +285,7 @@ impl RateLimiterItem for (super::RpcResponse, Protocol) { self.1 } - fn max_responses(&self, _current_fork: ForkName, _spec: &ChainSpec) -> u64 { + fn max_responses(&self, _digest_epoch: Epoch, _spec: &ChainSpec) -> u64 { // A response chunk consumes one token of the rate limiter. 1 } @@ -348,7 +353,10 @@ impl RPCRateLimiter { ) -> Result<(), RateLimitedErr> { let time_since_start = self.init_time.elapsed(); let tokens = request - .max_responses(self.fork_context.current_fork(), &self.fork_context.spec) + .max_responses( + self.fork_context.current_fork_epoch(), + &self.fork_context.spec, + ) .max(1); let check = @@ -374,16 +382,41 @@ impl RPCRateLimiter { pub fn prune(&mut self) { let time_since_start = self.init_time.elapsed(); - self.ping_rl.prune(time_since_start); - self.status_rl.prune(time_since_start); - self.metadata_rl.prune(time_since_start); - self.goodbye_rl.prune(time_since_start); - self.bbrange_rl.prune(time_since_start); - self.bbroots_rl.prune(time_since_start); - self.blbrange_rl.prune(time_since_start); - self.blbroot_rl.prune(time_since_start); - self.dcbrange_rl.prune(time_since_start); - self.dcbroot_rl.prune(time_since_start); + + let Self { + prune_interval: _, + init_time: _, + goodbye_rl, + ping_rl, + metadata_rl, + status_rl, + bbrange_rl, + bbroots_rl, + blbrange_rl, + blbroot_rl, + dcbroot_rl, + dcbrange_rl, + lc_bootstrap_rl, + lc_optimistic_update_rl, + lc_finality_update_rl, + lc_updates_by_range_rl, + fork_context: _, + } = self; + + goodbye_rl.prune(time_since_start); + ping_rl.prune(time_since_start); + metadata_rl.prune(time_since_start); + status_rl.prune(time_since_start); + bbrange_rl.prune(time_since_start); + bbroots_rl.prune(time_since_start); + blbrange_rl.prune(time_since_start); + blbroot_rl.prune(time_since_start); + dcbrange_rl.prune(time_since_start); + dcbroot_rl.prune(time_since_start); + lc_bootstrap_rl.prune(time_since_start); + lc_optimistic_update_rl.prune(time_since_start); + lc_finality_update_rl.prune(time_since_start); + lc_updates_by_range_rl.prune(time_since_start); } } @@ -412,14 +445,13 @@ pub struct Limiter { impl Limiter { pub fn from_quota(quota: Quota) -> Result { - if quota.max_tokens == 0 { - return Err("Max number of tokens should be positive"); - } let tau = quota.replenish_all_every.as_nanos(); if tau == 0 { return Err("Replenish time must be positive"); } - let t = (tau / quota.max_tokens as u128) + let t = tau + .checked_div(quota.max_tokens.get() as u128) + .expect("Division by zero never occurs, since Quota::max_token is of type NonZeroU64.") .try_into() .map_err(|_| "total replenish time is too long")?; let tau = tau @@ -442,7 +474,7 @@ impl Limiter { let tau = self.tau; let t = self.t; // how long does it take to replenish these tokens - let additional_time = t * tokens; + let additional_time = t.saturating_mul(tokens); if additional_time > tau { // the time required to process this amount of tokens is longer than the time that // makes the bucket full. So, this batch can _never_ be processed @@ -455,16 +487,16 @@ impl Limiter { .entry(key.clone()) .or_insert(time_since_start); // check how soon could the request be made - let earliest_time = (*tat + additional_time).saturating_sub(tau); + let earliest_time = (*tat).saturating_add(additional_time).saturating_sub(tau); // earliest_time is in the future if time_since_start < earliest_time { Err(RateLimitedErr::TooSoon(Duration::from_nanos( /* time they need to wait, i.e. how soon were they */ - earliest_time - time_since_start, + earliest_time.saturating_sub(time_since_start), ))) } else { // calculate the new TAT - *tat = time_since_start.max(*tat) + additional_time; + *tat = time_since_start.max(*tat).saturating_add(additional_time); Ok(()) } } @@ -479,14 +511,15 @@ impl Limiter { #[cfg(test)] mod tests { - use crate::rpc::rate_limiter::{Limiter, Quota}; + use crate::rpc::rate_limiter::{Limiter, Quota, RateLimitedErr}; + use std::num::NonZeroU64; use std::time::Duration; #[test] fn it_works_a() { let mut limiter = Limiter::from_quota(Quota { replenish_all_every: Duration::from_secs(2), - max_tokens: 4, + max_tokens: NonZeroU64::new(4).unwrap(), }) .unwrap(); let key = 10; @@ -498,32 +531,44 @@ mod tests { // | | | | | // 0 1 2 - assert!(limiter - .allows(Duration::from_secs_f32(0.0), &key, 4) - .is_ok()); + assert!( + limiter + .allows(Duration::from_secs_f32(0.0), &key, 4) + .is_ok() + ); limiter.prune(Duration::from_secs_f32(0.1)); - assert!(limiter - .allows(Duration::from_secs_f32(0.1), &key, 1) - .is_err()); - assert!(limiter - .allows(Duration::from_secs_f32(0.5), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(1.0), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(1.4), &key, 1) - .is_err()); - assert!(limiter - .allows(Duration::from_secs_f32(2.0), &key, 2) - .is_ok()); + assert!( + limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_err() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(0.5), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(1.0), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(1.4), &key, 1) + .is_err() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(2.0), &key, 2) + .is_ok() + ); } #[test] fn it_works_b() { let mut limiter = Limiter::from_quota(Quota { replenish_all_every: Duration::from_secs(2), - max_tokens: 4, + max_tokens: NonZeroU64::new(4).unwrap(), }) .unwrap(); let key = 10; @@ -531,20 +576,48 @@ mod tests { // first half second, when one token will be available again. Check also that before // regaining a token, another request is rejected - assert!(limiter - .allows(Duration::from_secs_f32(0.0), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(0.1), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(0.2), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(0.3), &key, 1) - .is_ok()); - assert!(limiter - .allows(Duration::from_secs_f32(0.4), &key, 1) - .is_err()); + assert!( + limiter + .allows(Duration::from_secs_f32(0.0), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(0.1), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(0.2), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(0.3), &key, 1) + .is_ok() + ); + assert!( + limiter + .allows(Duration::from_secs_f32(0.4), &key, 1) + .is_err() + ); + } + + #[test] + fn large_tokens() { + // These have been adjusted so that an overflow occurs when calculating `additional_time` in + // `Limiter::allows`. If we don't handle overflow properly, `Limiter::allows` returns `Ok` + // in this case. + let replenish_all_every = 2; + let tokens = u64::MAX / 2 + 1; + + let mut limiter = Limiter::from_quota(Quota { + replenish_all_every: Duration::from_nanos(replenish_all_every), + max_tokens: NonZeroU64::new(1).unwrap(), + }) + .unwrap(); + + let result = limiter.allows(Duration::from_secs_f32(0.0), &10, tokens); + assert!(matches!(result, Err(RateLimitedErr::TooLarge))); } } diff --git a/beacon_node/lighthouse_network/src/rpc/response_limiter.rs b/beacon_node/lighthouse_network/src/rpc/response_limiter.rs index c583baaadd..bd3035f89c 100644 --- a/beacon_node/lighthouse_network/src/rpc/response_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/response_limiter.rs @@ -1,8 +1,8 @@ +use crate::PeerId; use crate::rpc::config::InboundRateLimiterConfig; use crate::rpc::rate_limiter::{RPCRateLimiter, RateLimitedErr}; use crate::rpc::self_limiter::timestamp_now; use crate::rpc::{Protocol, RpcResponse, SubstreamId}; -use crate::PeerId; use futures::FutureExt; use libp2p::swarm::ConnectionId; use logging::crit; diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index e5b685676f..90e2db9135 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -1,19 +1,19 @@ use super::{ + BehaviourAction, MAX_CONCURRENT_REQUESTS, Protocol, RPCSend, ReqId, RequestType, config::OutboundRateLimiterConfig, rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}, - BehaviourAction, Protocol, RPCSend, ReqId, RequestType, MAX_CONCURRENT_REQUESTS, }; use crate::rpc::rate_limiter::RateLimiterItem; use std::time::{SystemTime, UNIX_EPOCH}; use std::{ - collections::{hash_map::Entry, HashMap, VecDeque}, + collections::{HashMap, VecDeque, hash_map::Entry}, sync::Arc, task::{Context, Poll}, time::Duration, }; use futures::FutureExt; -use libp2p::{swarm::NotifyHandler, PeerId}; +use libp2p::{PeerId, swarm::NotifyHandler}; use logging::crit; use smallvec::SmallVec; use tokio_util::time::DelayQueue; @@ -130,24 +130,23 @@ impl SelfRateLimiter { request_id: Id, req: RequestType, ) -> Result, (QueuedRequest, Duration)> { - if let Some(active_request) = active_requests.get(&peer_id) { - if let Some(count) = active_request.get(&req.protocol()) { - if *count >= MAX_CONCURRENT_REQUESTS { - debug!( - %peer_id, - protocol = %req.protocol(), - "Self rate limiting due to the number of concurrent requests" - ); - return Err(( - QueuedRequest { - req, - request_id, - queued_at: timestamp_now(), - }, - Duration::from_millis(WAIT_TIME_DUE_TO_CONCURRENT_REQUESTS), - )); - } - } + if let Some(active_request) = active_requests.get(&peer_id) + && let Some(count) = active_request.get(&req.protocol()) + && *count >= MAX_CONCURRENT_REQUESTS + { + debug!( + %peer_id, + protocol = %req.protocol(), + "Self rate limiting due to the number of concurrent requests" + ); + return Err(( + QueuedRequest { + req, + request_id, + queued_at: timestamp_now(), + }, + Duration::from_millis(WAIT_TIME_DUE_TO_CONCURRENT_REQUESTS), + )); } if let Some(limiter) = rate_limiter.as_mut() { @@ -258,13 +257,13 @@ impl SelfRateLimiter { /// Informs the limiter that a response has been received. pub fn request_completed(&mut self, peer_id: &PeerId, protocol: Protocol) { - if let Some(active_requests) = self.active_requests.get_mut(peer_id) { - if let Entry::Occupied(mut entry) = active_requests.entry(protocol) { - if *entry.get() > 1 { - *entry.get_mut() -= 1; - } else { - entry.remove(); - } + if let Some(active_requests) = self.active_requests.get_mut(peer_id) + && let Entry::Occupied(mut entry) = active_requests.entry(protocol) + { + if *entry.get() > 1 { + *entry.get_mut() -= 1; + } else { + entry.remove(); } } } @@ -316,6 +315,7 @@ mod tests { use crate::service::api_types::{AppRequestId, SingleLookupReqId, SyncRequestId}; use libp2p::PeerId; use logging::create_test_tracing_subscriber; + use std::num::NonZeroU64; use std::time::Duration; use types::{EthSpec, ForkContext, Hash256, MainnetEthSpec, Slot}; @@ -324,7 +324,7 @@ mod tests { async fn test_next_peer_request_ready() { create_test_tracing_subscriber(); let config = OutboundRateLimiterConfig(RateLimiterConfig { - ping_quota: Quota::n_every(1, 2), + ping_quota: Quota::n_every(NonZeroU64::new(1).unwrap(), 2), ..Default::default() }); let fork_context = std::sync::Arc::new(ForkContext::new::( @@ -510,13 +510,17 @@ mod tests { } assert!(limiter.active_requests.contains_key(&peer1)); - assert!(limiter - .delayed_requests - .contains_key(&(peer1, Protocol::Ping))); + assert!( + limiter + .delayed_requests + .contains_key(&(peer1, Protocol::Ping)) + ); assert!(limiter.active_requests.contains_key(&peer2)); - assert!(limiter - .delayed_requests - .contains_key(&(peer2, Protocol::Ping))); + assert!( + limiter + .delayed_requests + .contains_key(&(peer2, Protocol::Ping)) + ); // Check that the limiter returns the IDs of pending requests and that the IDs are ordered correctly. let mut failed_requests = limiter.peer_disconnected(peer1); @@ -532,13 +536,17 @@ mod tests { // Check that peer1’s active and delayed requests have been removed. assert!(!limiter.active_requests.contains_key(&peer1)); - assert!(!limiter - .delayed_requests - .contains_key(&(peer1, Protocol::Ping))); + assert!( + !limiter + .delayed_requests + .contains_key(&(peer1, Protocol::Ping)) + ); assert!(limiter.active_requests.contains_key(&peer2)); - assert!(limiter - .delayed_requests - .contains_key(&(peer2, Protocol::Ping))); + assert!( + limiter + .delayed_requests + .contains_key(&(peer2, Protocol::Ping)) + ); } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index b36f8cc215..f1a4d87de7 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,8 +1,9 @@ use crate::rpc::methods::{ResponseTermination, RpcResponse, RpcSuccessResponse, StatusMessage}; +use libp2p::PeerId; use std::fmt::{Display, Formatter}; use std::sync::Arc; use types::{ - BlobSidecar, DataColumnSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, + BlobSidecar, DataColumnSidecar, Epoch, EthSpec, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, SignedBeaconBlock, }; @@ -59,8 +60,19 @@ pub struct BlobsByRangeRequestId { pub struct DataColumnsByRangeRequestId { /// Id to identify this attempt at a data_columns_by_range request for `parent_request_id` pub id: Id, - /// The Id of the overall By Range request for block components. - pub parent_request_id: ComponentsByRangeRequestId, + /// The Id of the overall By Range request for either a components by range request or a custody backfill request. + pub parent_request_id: DataColumnsByRangeRequester, + /// The peer id associated with the request. + /// + /// This is useful to penalize the peer at a later point if it returned data columns that + /// did not match with the verified block. + pub peer: PeerId, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum DataColumnsByRangeRequester { + ComponentsByRange(ComponentsByRangeRequestId), + CustodyBackfillSync(CustodyBackFillBatchRequestId), } /// Block components by range request for range sync. Includes an ID for downstream consumers to @@ -74,6 +86,24 @@ pub struct ComponentsByRangeRequestId { pub requester: RangeRequestId, } +/// A batch of data columns by range request for custody sync. Includes an ID for downstream consumers to +/// handle retries and tie all the range requests for the given epoch together. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct CustodyBackFillBatchRequestId { + /// For each `epoch` we may request the same data in a later retry. This Id identifies the + /// current attempt. + pub id: Id, + pub batch_id: CustodyBackfillBatchId, +} + +/// Custody backfill may be restarted and sync each epoch multiple times in different runs. Identify +/// each batch by epoch and run_id for uniqueness. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct CustodyBackfillBatchId { + pub epoch: Epoch, + pub run_id: u64, +} + /// Range sync chain or backfill batch #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum RangeRequestId { @@ -81,9 +111,10 @@ pub enum RangeRequestId { BackfillSync { batch_id: Epoch }, } +// TODO(das) refactor in a separate PR. We might be able to remove this and replace +// [`DataColumnsByRootRequestId`] with a [`SingleLookupReqId`]. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRootRequester { - Sampling(SamplingId), Custody(CustodyId), } @@ -93,21 +124,6 @@ pub enum RangeRequester { BackfillSync { batch_id: Epoch }, } -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct SamplingId { - pub id: SamplingRequester, - pub sampling_request_id: SamplingRequestId, -} - -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum SamplingRequester { - ImportedBlock(Hash256), -} - -/// Identifier of sampling requests. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct SamplingRequestId(pub usize); - #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct CustodyId { pub requester: CustodyRequester, @@ -225,13 +241,13 @@ impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); impl_display!(CustodyId, "{}", requester); -impl_display!(SamplingId, "{}/{}", sampling_request_id, id); +impl_display!(CustodyBackFillBatchRequestId, "{}/{}", id, batch_id); +impl_display!(CustodyBackfillBatchId, "{}/{}", epoch, run_id); impl Display for DataColumnsByRootRequester { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Custody(id) => write!(f, "Custody/{id}"), - Self::Sampling(id) => write!(f, "Sampling/{id}"), } } } @@ -251,16 +267,11 @@ impl Display for RangeRequestId { } } -impl Display for SamplingRequestId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Display for SamplingRequester { +impl Display for DataColumnsByRangeRequester { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::ImportedBlock(block) => write!(f, "ImportedBlock/{block}"), + Self::ComponentsByRange(id) => write!(f, "ByRange/{id}"), + Self::CustodyBackfillSync(id) => write!(f, "CustodyBackfill/{id}"), } } } @@ -283,30 +294,21 @@ mod tests { assert_eq!(format!("{id}"), "123/Custody/121/Lookup/101"); } - #[test] - fn display_id_data_columns_by_root_sampling() { - let id = DataColumnsByRootRequestId { - id: 123, - requester: DataColumnsByRootRequester::Sampling(SamplingId { - id: SamplingRequester::ImportedBlock(Hash256::ZERO), - sampling_request_id: SamplingRequestId(101), - }), - }; - assert_eq!(format!("{id}"), "123/Sampling/101/ImportedBlock/0x0000000000000000000000000000000000000000000000000000000000000000"); - } - #[test] fn display_id_data_columns_by_range() { let id = DataColumnsByRangeRequestId { id: 123, - parent_request_id: ComponentsByRangeRequestId { - id: 122, - requester: RangeRequestId::RangeSync { - chain_id: 54, - batch_id: Epoch::new(0), + parent_request_id: DataColumnsByRangeRequester::ComponentsByRange( + ComponentsByRangeRequestId { + id: 122, + requester: RangeRequestId::RangeSync { + chain_id: 54, + batch_id: Epoch::new(0), + }, }, - }, + ), + peer: PeerId::random(), }; - assert_eq!(format!("{id}"), "123/122/RangeSync/0/54"); + assert_eq!(format!("{id}"), "123/ByRange/122/RangeSync/0/54"); } } diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index f8986b0518..b8286fa3c8 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -1,11 +1,11 @@ -use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::pin::Pin; use std::task::{Context, Poll}; use std::time::Duration; -use crate::types::GossipKind; use crate::GossipTopic; +use crate::types::GossipKind; use tokio_util::time::delay_queue::{DelayQueue, Key}; diff --git a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs index 6fffd649f5..873d3f9252 100644 --- a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs +++ b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs @@ -1,5 +1,5 @@ -use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::TopicHash; +use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use gossipsub::{IdentTopic as Topic, PeerScoreParams, PeerScoreThresholds, TopicScoreParams}; use std::cmp::max; use std::collections::HashMap; diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 23060df9e6..4eebda1dec 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1,50 +1,50 @@ use self::gossip_cache::GossipCache; -use crate::config::{gossipsub_config, GossipsubConfigParams, NetworkLoad}; +use crate::Eth2Enr; +use crate::config::{GossipsubConfigParams, NetworkLoad, gossipsub_config}; use crate::discovery::{ - subnet_predicate, DiscoveredPeers, Discovery, FIND_NODE_QUERY_CLOSEST_PEERS, + DiscoveredPeers, Discovery, FIND_NODE_QUERY_CLOSEST_PEERS, subnet_predicate, }; use crate::peer_manager::{ - config::Config as PeerManagerCfg, peerdb::score::PeerAction, peerdb::score::ReportSource, - ConnectionDirection, PeerManager, PeerManagerEvent, + ConnectionDirection, PeerManager, PeerManagerEvent, config::Config as PeerManagerCfg, + peerdb::score::PeerAction, peerdb::score::ReportSource, }; use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; use crate::rpc::methods::MetadataRequest; use crate::rpc::{ - GoodbyeReason, HandlerErr, InboundRequestId, NetworkParams, Protocol, RPCError, RPCMessage, - RPCReceived, RequestType, ResponseTermination, RpcErrorResponse, RpcResponse, - RpcSuccessResponse, RPC, + GoodbyeReason, HandlerErr, InboundRequestId, Protocol, RPC, RPCError, RPCMessage, RPCReceived, + RequestType, ResponseTermination, RpcResponse, RpcSuccessResponse, }; use crate::types::{ - all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, + all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, }; -use crate::EnrExt; -use crate::Eth2Enr; -use crate::{metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; +use crate::{Enr, NetworkGlobals, PubsubMessage, TopicHash, metrics}; use api_types::{AppRequestId, Response}; use futures::stream::StreamExt; use gossipsub::{ IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, PublishError, TopicScoreParams, }; -use gossipsub_scoring_parameters::{lighthouse_gossip_thresholds, PeerScoreSettings}; +use gossipsub_scoring_parameters::{PeerScoreSettings, lighthouse_gossip_thresholds}; +use libp2p::identity::Keypair; use libp2p::multiaddr::{self, Multiaddr, Protocol as MProtocol}; use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::swarm::{NetworkBehaviour, Swarm, SwarmEvent}; use libp2p::upnp::tokio::Behaviour as Upnp; -use libp2p::{identify, PeerId, SwarmBuilder}; +use libp2p::{PeerId, SwarmBuilder, identify}; use logging::crit; +use network_utils::enr_ext::EnrExt; use std::num::{NonZeroU8, NonZeroUsize}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, info, instrument, trace, warn}; -use types::{ - consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, EnrForkId, EthSpec, ForkContext, Slot, SubnetId, -}; +use tracing::{debug, error, info, trace, warn}; use types::{ChainSpec, ForkName}; -use utils::{build_transport, strip_peer_id, Context as ServiceContext}; +use types::{ + EnrForkId, EthSpec, ForkContext, Slot, SubnetId, consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, +}; +use utils::{Context as ServiceContext, build_transport, strip_peer_id}; pub mod api_types; mod gossip_cache; @@ -168,20 +168,14 @@ pub struct Network { /// Implements the combined behaviour for the libp2p service. impl Network { - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub async fn new( executor: task_executor::TaskExecutor, mut ctx: ServiceContext<'_>, + custody_group_count: u64, + local_keypair: Keypair, ) -> Result<(Self, Arc>), String> { let config = ctx.config.clone(); trace!("Libp2p Service starting"); - // initialise the node's ID - let local_keypair = utils::load_private_key(&config); // Trusted peers will also be marked as explicit in GossipSub. // Cfr. https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#explicit-peering-agreements @@ -193,19 +187,26 @@ impl Network { // set up a collection of variables accessible outside of the network crate // Create an ENR or load from disk if appropriate + let next_fork_digest = ctx + .fork_context + .next_fork_digest() + .unwrap_or_else(|| ctx.fork_context.current_fork_digest()); + + let advertised_cgc = config + .advertise_false_custody_group_count + .unwrap_or(custody_group_count); let enr = crate::discovery::enr::build_or_load_enr::( local_keypair.clone(), &config, &ctx.enr_fork_id, + advertised_cgc, + next_fork_digest, &ctx.chain_spec, )?; // Construct the metadata - let custody_group_count = ctx.chain_spec.is_peer_das_scheduled().then(|| { - ctx.chain_spec - .custody_group_count(config.subscribe_all_data_column_subnets) - }); - let meta_data = utils::load_or_build_metadata(&config.network_dir, custody_group_count); + + let meta_data = utils::load_or_build_metadata(&config.network_dir, advertised_cgc); let seq_number = *meta_data.seq_number(); let globals = NetworkGlobals::new( enr, @@ -281,27 +282,26 @@ impl Network { // Set up a scoring update interval let update_gossipsub_scores = tokio::time::interval(params.decay_interval); - let current_and_future_forks = ForkName::list_all().into_iter().filter_map(|fork| { - if fork >= ctx.fork_context.current_fork() { - ctx.fork_context - .to_context_bytes(fork) - .map(|fork_digest| (fork, fork_digest)) - } else { - None - } - }); + let current_digest_epoch = ctx.fork_context.current_fork_epoch(); + let current_and_future_digests = + ctx.chain_spec + .all_digest_epochs() + .filter_map(|digest_epoch| { + if digest_epoch >= current_digest_epoch { + Some((digest_epoch, ctx.fork_context.context_bytes(digest_epoch))) + } else { + None + } + }); - let all_topics_for_forks = current_and_future_forks - .map(|(fork, fork_digest)| { + let all_topics_for_digests = current_and_future_digests + .map(|(epoch, digest)| { + let fork = ctx.chain_spec.fork_name_at_epoch(epoch); all_topics_at_fork::(fork, &ctx.chain_spec) .into_iter() .map(|topic| { - Topic::new(GossipTopic::new( - topic, - GossipEncoding::default(), - fork_digest, - )) - .into() + Topic::new(GossipTopic::new(topic, GossipEncoding::default(), digest)) + .into() }) .collect::>() }) @@ -309,7 +309,7 @@ impl Network { // For simplicity find the fork with the most individual topics and assume all forks // have the same topic count - let max_topics_at_any_fork = all_topics_for_forks + let max_topics_at_any_fork = all_topics_for_digests .iter() .map(|topics| topics.len()) .max() @@ -328,26 +328,25 @@ impl Network { max_subscriptions_per_request: max_topics_at_any_fork * 2, }; - // If metrics are enabled for libp2p build the configuration - let gossipsub_metrics = ctx.libp2p_registry.as_mut().map(|registry| { - ( - registry.sub_registry_with_prefix("gossipsub"), - Default::default(), - ) - }); - let spec = &ctx.chain_spec; let snappy_transform = SnappyTransform::new(spec.max_payload_size as usize, spec.max_compressed_len()); let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( MessageAuthenticity::Anonymous, gs_config.clone(), - gossipsub_metrics, filter, snappy_transform, ) .map_err(|e| format!("Could not construct gossipsub: {:?}", e))?; + // If metrics are enabled for libp2p build the configuration + if let Some(ref mut registry) = ctx.libp2p_registry { + gossipsub = gossipsub.with_metrics( + registry.sub_registry_with_prefix("gossipsub"), + Default::default(), + ); + } + gossipsub .with_peer_score(params, thresholds) .expect("Valid score params and thresholds"); @@ -360,7 +359,7 @@ impl Network { // If we are using metrics, then register which topics we want to make sure to keep // track of if ctx.libp2p_registry.is_some() { - for topics in all_topics_for_forks { + for topics in all_topics_for_digests { gossipsub.register_topics_for_metrics(topics); } } @@ -368,17 +367,11 @@ impl Network { (gossipsub, update_gossipsub_scores) }; - let network_params = NetworkParams { - max_payload_size: ctx.chain_spec.max_payload_size as usize, - ttfb_timeout: ctx.chain_spec.ttfb_timeout(), - resp_timeout: ctx.chain_spec.resp_timeout(), - }; let eth2_rpc = RPC::new( ctx.fork_context.clone(), config.enable_light_client_server, config.inbound_rate_limiter_config.clone(), config.outbound_rate_limiter_config.clone(), - network_params, seq_number, ); @@ -529,12 +522,6 @@ impl Network { /// - Starts listening in the given ports. /// - Dials boot-nodes and libp2p peers. /// - Subscribes to starting gossipsub topics. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] async fn start(&mut self, config: &crate::NetworkConfig) -> Result<(), String> { let enr = self.network_globals.local_enr(); info!( @@ -658,114 +645,48 @@ impl Network { /* Public Accessible Functions to interact with the behaviour */ /// The routing pub-sub mechanism for eth2. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn gossipsub_mut(&mut self) -> &mut Gossipsub { &mut self.swarm.behaviour_mut().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn eth2_rpc_mut(&mut self) -> &mut RPC { &mut self.swarm.behaviour_mut().eth2_rpc } /// Discv5 Discovery protocol. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn discovery_mut(&mut self) -> &mut Discovery { &mut self.swarm.behaviour_mut().discovery } /// Provides IP addresses and peer information. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn identify_mut(&mut self) -> &mut identify::Behaviour { &mut self.swarm.behaviour_mut().identify } /// The peer manager that keeps track of peer's reputation and status. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn peer_manager_mut(&mut self) -> &mut PeerManager { &mut self.swarm.behaviour_mut().peer_manager } /// The routing pub-sub mechanism for eth2. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn gossipsub(&self) -> &Gossipsub { &self.swarm.behaviour().gossipsub } /// The Eth2 RPC specified in the wire-0 protocol. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn eth2_rpc(&self) -> &RPC { &self.swarm.behaviour().eth2_rpc } /// Discv5 Discovery protocol. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn discovery(&self) -> &Discovery { &self.swarm.behaviour().discovery } /// Provides IP addresses and peer information. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn identify(&self) -> &identify::Behaviour { &self.swarm.behaviour().identify } /// The peer manager that keeps track of peer's reputation and status. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn peer_manager(&self) -> &PeerManager { &self.swarm.behaviour().peer_manager } /// Returns the local ENR of the node. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn local_enr(&self) -> Enr { self.network_globals.local_enr() } @@ -774,12 +695,6 @@ impl Network { /// Subscribes to a gossipsub topic kind, letting the network service determine the /// encoding and fork version. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn subscribe_kind(&mut self, kind: GossipKind) -> bool { let gossip_topic = GossipTopic::new( kind, @@ -792,12 +707,6 @@ impl Network { /// Unsubscribes from a gossipsub topic kind, letting the network service determine the /// encoding and fork version. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn unsubscribe_kind(&mut self, kind: GossipKind) -> bool { let gossip_topic = GossipTopic::new( kind, @@ -808,12 +717,6 @@ impl Network { } /// Subscribe to all required topics for the `new_fork` with the given `new_fork_digest`. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn subscribe_new_fork_topics(&mut self, new_fork: ForkName, new_fork_digest: [u8; 4]) { // Re-subscribe to non-core topics with the new fork digest let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone(); @@ -838,12 +741,6 @@ impl Network { } /// Unsubscribe from all topics that doesn't have the given fork_digest - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn unsubscribe_from_fork_topics_except(&mut self, except: [u8; 4]) { let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone(); for topic in subscriptions @@ -856,12 +753,6 @@ impl Network { } /// Remove topic weight from all topics that don't have the given fork digest. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn remove_topic_weight_except(&mut self, except: [u8; 4]) { let new_param = TopicScoreParams { topic_weight: 0.0, @@ -885,13 +776,18 @@ impl Network { } } + /// Subscribe to all data columns determined by the cgc. + pub fn subscribe_new_data_column_subnets(&mut self, sampling_column_count: u64) { + self.network_globals + .update_data_column_subnets(sampling_column_count); + + for column in self.network_globals.sampling_subnets() { + let kind = GossipKind::DataColumnSidecar(column); + self.subscribe_kind(kind); + } + } + /// Returns the scoring parameters for a topic if set. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn get_topic_params(&self, topic: GossipTopic) -> Option<&TopicScoreParams> { self.swarm .behaviour() @@ -902,12 +798,6 @@ impl Network { /// Subscribes to a gossipsub topic. /// /// Returns `true` if the subscription was successful and `false` otherwise. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn subscribe(&mut self, topic: GossipTopic) -> bool { // update the network globals self.network_globals @@ -930,12 +820,6 @@ impl Network { } /// Unsubscribe from a gossipsub topic. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn unsubscribe(&mut self, topic: GossipTopic) -> bool { // update the network globals self.network_globals @@ -951,12 +835,6 @@ impl Network { } /// Publishes a list of messages on the pubsub (gossipsub) behaviour, choosing the encoding. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn publish(&mut self, messages: Vec>) { for message in messages { for topic in message.topics(GossipEncoding::default(), self.enr_fork_id.fork_digest) { @@ -1011,12 +889,6 @@ impl Network { /// Informs the gossipsub about the result of a message validation. /// If the message is valid it will get propagated by gossipsub. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn report_message_validation_result( &mut self, propagation_source: &PeerId, @@ -1027,19 +899,17 @@ impl Network { MessageAcceptance::Accept => None, MessageAcceptance::Ignore => Some("ignore"), MessageAcceptance::Reject => Some("reject"), - } { - if let Some(client) = self - .network_globals - .peers - .read() - .peer_info(propagation_source) - .map(|info| info.client().kind.as_ref()) - { - metrics::inc_counter_vec( - &metrics::GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT, - &[client, result], - ) - } + } && let Some(client) = self + .network_globals + .peers + .read() + .peer_info(propagation_source) + .map(|info| info.client().kind.as_ref()) + { + metrics::inc_counter_vec( + &metrics::GOSSIP_UNACCEPTED_MESSAGES_PER_CLIENT, + &[client, result], + ) } self.gossipsub_mut().report_message_validation_result( @@ -1051,12 +921,6 @@ impl Network { /// Updates the current gossipsub scoring parameters based on the validator count and current /// slot. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn update_gossipsub_parameters( &mut self, active_validators: usize, @@ -1100,12 +964,7 @@ impl Network { /* Eth2 RPC behaviour functions */ /// Send a request to a peer over RPC. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] + #[allow(clippy::result_large_err)] pub fn send_request( &mut self, peer_id: PeerId, @@ -1123,60 +982,28 @@ impl Network { } /// Send a successful response to a peer over RPC. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] - pub fn send_response( + pub fn send_response>>( &mut self, peer_id: PeerId, inbound_request_id: InboundRequestId, - response: Response, + response: T, ) { - self.eth2_rpc_mut() - .send_response(peer_id, inbound_request_id, response.into()) - } - - /// Inform the peer that their request produced an error. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] - pub fn send_error_response( - &mut self, - peer_id: PeerId, - inbound_request_id: InboundRequestId, - error: RpcErrorResponse, - reason: String, - ) { - self.eth2_rpc_mut().send_response( - peer_id, - inbound_request_id, - RpcResponse::Error(error, reason.into()), - ) + if let Err(response) = self + .eth2_rpc_mut() + .send_response(inbound_request_id, response.into()) + && self.network_globals.peers.read().is_connected(&peer_id) + { + error!(%peer_id, ?inbound_request_id, %response, + "Request not found in RPC active requests" + ); + } } /* Peer management functions */ - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn testing_dial(&mut self, addr: Multiaddr) -> Result<(), libp2p::swarm::DialError> { self.swarm.dial(addr) } - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn report_peer( &mut self, peer_id: &PeerId, @@ -1192,12 +1019,6 @@ impl Network { /// /// This will send a goodbye, disconnect and then ban the peer. /// This is fatal for a peer, and should be used in unrecoverable circumstances. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason, source: ReportSource) { self.peer_manager_mut() .goodbye_peer(peer_id, reason, source); @@ -1205,34 +1026,16 @@ impl Network { /// Hard (ungraceful) disconnect for testing purposes only /// Use goodbye_peer for disconnections, do not use this function. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn __hard_disconnect_testing_only(&mut self, peer_id: PeerId) { let _ = self.swarm.disconnect_peer_id(peer_id); } /// Returns an iterator over all enr entries in the DHT. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn enr_entries(&self) -> Vec { self.discovery().table_entries_enr() } /// Add an ENR to the routing table of the discovery mechanism. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn add_enr(&mut self, enr: Enr) { self.discovery_mut().add_enr(enr); } @@ -1240,12 +1043,6 @@ impl Network { /// Updates a subnet value to the ENR attnets/syncnets bitfield. /// /// The `value` is `true` if a subnet is being added and false otherwise. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn update_enr_subnet(&mut self, subnet_id: Subnet, value: bool) { if let Err(e) = self.discovery_mut().update_enr_bitfield(subnet_id, value) { crit!(error = e, "Could not update ENR bitfield"); @@ -1254,14 +1051,17 @@ impl Network { self.update_metadata_bitfields(); } + /// Updates the cgc value in the ENR. + pub fn update_enr_cgc(&mut self, new_custody_group_count: u64) { + if let Err(e) = self.discovery_mut().update_enr_cgc(new_custody_group_count) { + crit!(error = e, "Could not update cgc in ENR"); + } + // update the local meta data which informs our peers of the update during PINGS + self.update_metadata_cgc(new_custody_group_count); + } + /// Attempts to discover new peers for a given subnet. The `min_ttl` gives the time at which we /// would like to retain the peers for. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn discover_subnet_peers(&mut self, subnets_to_discover: Vec) { // If discovery is not started or disabled, ignore the request if !self.discovery().started { @@ -1316,12 +1116,6 @@ impl Network { } /// Updates the local ENR's "eth2" field with the latest EnrForkId. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub fn update_fork_version(&mut self, enr_fork_id: EnrForkId) { self.discovery_mut().update_eth2_enr(enr_fork_id.clone()); @@ -1329,15 +1123,15 @@ impl Network { self.enr_fork_id = enr_fork_id; } + pub fn update_nfd(&mut self, nfd: [u8; 4]) { + if let Err(e) = self.discovery_mut().update_enr_nfd(nfd) { + crit!(error = e, "Could not update nfd in ENR"); + } + } + /* Private internal functions */ /// Updates the current meta data of the node to match the local ENR. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn update_metadata_bitfields(&mut self) { let local_attnets = self .discovery_mut() @@ -1368,24 +1162,28 @@ impl Network { utils::save_metadata_to_disk(&self.network_dir, meta_data); } + fn update_metadata_cgc(&mut self, custody_group_count: u64) { + let mut meta_data_w = self.network_globals.local_metadata.write(); + + *meta_data_w.seq_number_mut() += 1; + if let Ok(cgc) = meta_data_w.custody_group_count_mut() { + *cgc = custody_group_count; + } + let seq_number = *meta_data_w.seq_number(); + let meta_data = meta_data_w.clone(); + + drop(meta_data_w); + self.eth2_rpc_mut().update_seq_number(seq_number); + // Save the updated metadata to disk + utils::save_metadata_to_disk(&self.network_dir, meta_data); + } + /// Sends a Ping request to the peer. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn ping(&mut self, peer_id: PeerId) { self.eth2_rpc_mut().ping(peer_id, AppRequestId::Internal); } /// Sends a METADATA request to a peer. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn send_meta_data_request(&mut self, peer_id: PeerId) { let event = if self.fork_context.spec.is_peer_das_scheduled() { // Nodes with higher custody will probably start advertising it @@ -1400,34 +1198,9 @@ impl Network { } /// Sends a METADATA response to a peer. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] - fn send_meta_data_response( - &mut self, - _req: MetadataRequest, - inbound_request_id: InboundRequestId, - peer_id: PeerId, - ) { - let metadata = self.network_globals.local_metadata.read().clone(); - // The encoder is responsible for sending the negotiated version of the metadata - let event = RpcResponse::Success(RpcSuccessResponse::MetaData(Arc::new(metadata))); - self.eth2_rpc_mut() - .send_response(peer_id, inbound_request_id, event); - } - // RPC Propagation methods /// Queues the response to be sent upwards as long at it was requested outside the Behaviour. #[must_use = "return the response"] - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn build_response( &mut self, app_request_id: AppRequestId, @@ -1446,12 +1219,6 @@ impl Network { /// Dial cached Enrs in discovery service that are in the given `subnet_id` and aren't /// in Connected, Dialing or Banned state. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn dial_cached_enrs_in_subnet(&mut self, subnet: Subnet, spec: Arc) { let predicate = subnet_predicate::(vec![subnet], spec); let peers_to_dial: Vec = self @@ -1494,12 +1261,6 @@ impl Network { /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn inject_gs_event(&mut self, event: gossipsub::Event) -> Option> { match event { gossipsub::Event::Message { @@ -1606,14 +1367,12 @@ impl Network { } => { debug!( peer_id = %peer_id, - publish = failed_messages.publish, - forward = failed_messages.forward, priority = failed_messages.priority, non_priority = failed_messages.non_priority, "Slow gossipsub peer" ); // Punish the peer if it cannot handle priority messages - if failed_messages.timeout > 10 { + if failed_messages.priority > 10 { debug!(%peer_id, "Slow gossipsub peer penalized for priority failure"); self.peer_manager_mut().report_peer( &peer_id, @@ -1622,7 +1381,7 @@ impl Network { None, "publish_timeout_penalty", ); - } else if failed_messages.total_queue_full() > 10 { + } else if failed_messages.non_priority > 10 { debug!(%peer_id, "Slow gossipsub peer penalized for send queue full"); self.peer_manager_mut().report_peer( &peer_id, @@ -1638,12 +1397,6 @@ impl Network { } /// Handle an RPC event. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn inject_rpc_event(&mut self, event: RPCMessage) -> Option> { let peer_id = event.peer_id; @@ -1706,9 +1459,13 @@ impl Network { self.peer_manager_mut().ping_request(&peer_id, ping.data); None } - RequestType::MetaData(req) => { + RequestType::MetaData(_req) => { // send the requested meta-data - self.send_meta_data_response(req, inbound_request_id, peer_id); + let metadata = self.network_globals.local_metadata.read().clone(); + // The encoder is responsible for sending the negotiated version of the metadata + let response = + RpcResponse::Success(RpcSuccessResponse::MetaData(Arc::new(metadata))); + self.send_response(peer_id, inbound_request_id, response); None } RequestType::Goodbye(reason) => { @@ -1930,12 +1687,6 @@ impl Network { } /// Handle an identify event. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn inject_identify_event(&mut self, event: identify::Event) -> Option> { match event { identify::Event::Received { @@ -1958,12 +1709,6 @@ impl Network { } /// Handle a peer manager event. - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn inject_pm_event(&mut self, event: PeerManagerEvent) -> Option> { match event { PeerManagerEvent::PeerConnectedIncoming(peer_id) => { @@ -2017,12 +1762,6 @@ impl Network { } } - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn inject_upnp_event(&mut self, event: libp2p::upnp::Event) { match event { libp2p::upnp::Event::NewExternalAddr(addr) => { @@ -2066,12 +1805,6 @@ impl Network { } /* Networking polling */ - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] pub async fn next_event(&mut self) -> NetworkEvent { loop { tokio::select! { @@ -2105,12 +1838,6 @@ impl Network { } } - #[instrument(parent = None, - level = "trace", - fields(service = "libp2p"), - name = "libp2p", - skip_all - )] fn parse_swarm_event( &mut self, event: SwarmEvent>, @@ -2152,6 +1879,7 @@ impl Network { send_back_addr, error, connection_id: _, + peer_id: _, } => { let error_repr = match error { libp2p::swarm::ListenError::Aborted => { @@ -2160,8 +1888,8 @@ impl Network { libp2p::swarm::ListenError::WrongPeerId { obtained, endpoint } => { format!("Wrong peer id, obtained {obtained}, endpoint {endpoint:?}") } - libp2p::swarm::ListenError::LocalPeerId { endpoint } => { - format!("Dialing local peer id {endpoint:?}") + libp2p::swarm::ListenError::LocalPeerId { address } => { + format!("Dialing local peer id {address:?}") } libp2p::swarm::ListenError::Denied { cause } => { format!("Connection was denied with cause: {cause:?}") diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 239cf935c8..83011c0449 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -1,14 +1,13 @@ use crate::multiaddr::Protocol; -use crate::rpc::methods::MetaDataV3; -use crate::rpc::{MetaData, MetaDataV1, MetaDataV2}; +use crate::rpc::{MetaData, MetaDataV2, MetaDataV3}; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipEncoding, GossipKind}; use crate::{GossipTopic, NetworkConfig}; use futures::future::Either; use gossipsub; use libp2p::core::{multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed}; -use libp2p::identity::{secp256k1, Keypair}; -use libp2p::{core, noise, yamux, PeerId, Transport}; -use prometheus_client::registry::Registry; +use libp2p::identity::{Keypair, secp256k1}; +use libp2p::metrics::Registry; +use libp2p::{PeerId, Transport, core, noise, yamux}; use ssz::Decode; use std::collections::HashSet; use std::fs::File; @@ -42,7 +41,7 @@ pub fn build_transport( quic_support: bool, ) -> std::io::Result { // mplex config - let mut mplex_config = libp2p_mplex::MplexConfig::new(); + let mut mplex_config = libp2p_mplex::Config::new(); mplex_config.set_max_buffer_size(256); mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::Block); @@ -79,8 +78,6 @@ pub fn build_transport( Ok(transport) } -// Useful helper functions for debugging. Currently not used in the client. -#[allow(dead_code)] fn keypair_from_hex(hex_bytes: &str) -> Result { let hex_bytes = if let Some(stripped) = hex_bytes.strip_prefix("0x") { stripped.to_string() @@ -93,7 +90,6 @@ fn keypair_from_hex(hex_bytes: &str) -> Result { .and_then(keypair_from_bytes) } -#[allow(dead_code)] fn keypair_from_bytes(mut bytes: Vec) -> Result { secp256k1::SecretKey::try_from_bytes(&mut bytes) .map(|secret| { @@ -107,21 +103,54 @@ fn keypair_from_bytes(mut bytes: Vec) -> Result { /// generated and is then saved to disk. /// /// Currently only secp256k1 keys are allowed, as these are the only keys supported by discv5. +/// Supports both hex format (with or without 0x prefix) and raw bytes format. pub fn load_private_key(config: &NetworkConfig) -> Keypair { // check for key from disk let network_key_f = config.network_dir.join(NETWORK_KEY_FILENAME); if let Ok(mut network_key_file) = File::open(network_key_f.clone()) { - let mut key_bytes: Vec = Vec::with_capacity(36); - match network_key_file.read_to_end(&mut key_bytes) { - Err(_) => debug!("Could not read network key file"), - Ok(_) => { - // only accept secp256k1 keys for now - if let Ok(secret_key) = secp256k1::SecretKey::try_from_bytes(&mut key_bytes) { - let kp: secp256k1::Keypair = secret_key.into(); - debug!("Loaded network key from disk."); - return kp.into(); - } else { - debug!("Network key file is not a valid secp256k1 key"); + // Limit read to reasonable hex key size: 32 bytes = 64 hex chars + "0x" prefix + whitespace + let mut buffer = vec![0u8; 70]; + match network_key_file.read(&mut buffer) { + Ok(bytes_read) => { + if let Ok(hex_string) = String::from_utf8(buffer[..bytes_read].to_vec()) { + // First try to parse as hex string + let hex_content = hex_string.trim(); + if let Ok(keypair) = keypair_from_hex(hex_content) { + debug!("Loaded network key from disk (hex format)."); + return keypair; + } + } + } + Err(_) => debug!("Could not read network key file as string, trying binary format"), + } + + // If hex parsing failed or file couldn't be read as string, try binary format + if let Ok(mut network_key_file) = File::open(network_key_f.clone()) { + let mut key_bytes: Vec = Vec::with_capacity(36); + match network_key_file.read_to_end(&mut key_bytes) { + Err(_) => debug!("Could not read network key file"), + Ok(_) => { + // only accept secp256k1 keys for now + if let Ok(secret_key) = secp256k1::SecretKey::try_from_bytes(&mut key_bytes) { + let kp: secp256k1::Keypair = secret_key.clone().into(); + debug!( + "Loaded network key from disk (binary format), migrating to hex format." + ); + + // Migrate binary key to hex format + let hex_key = hex::encode(secret_key.to_bytes()); + if let Err(e) = File::create(network_key_f.clone()) + .and_then(|mut f| f.write_all(hex_key.as_bytes())) + { + debug!("Failed to migrate key to hex format: {}", e); + } else { + debug!("Successfully migrated key to hex format."); + } + + return kp.into(); + } else { + debug!("Network key file is not a valid secp256k1 key"); + } } } } @@ -130,9 +159,8 @@ pub fn load_private_key(config: &NetworkConfig) -> Keypair { // if a key could not be loaded from disk, generate a new one and save it let local_private_key = secp256k1::Keypair::generate(); let _ = std::fs::create_dir_all(&config.network_dir); - match File::create(network_key_f.clone()) - .and_then(|mut f| f.write_all(&local_private_key.secret().to_bytes())) - { + let hex_key = hex::encode(local_private_key.secret().to_bytes()); + match File::create(network_key_f.clone()).and_then(|mut f| f.write_all(hex_key.as_bytes())) { Ok(_) => { debug!("New network key generated and written to disk"); } @@ -165,38 +193,41 @@ pub fn strip_peer_id(addr: &mut Multiaddr) { /// Load metadata from persisted file. Return default metadata if loading fails. pub fn load_or_build_metadata( network_dir: &Path, - custody_group_count_opt: Option, + custody_group_count: u64, ) -> MetaData { - // We load a V2 metadata version by default (regardless of current fork) - // since a V2 metadata can be converted to V1. The RPC encoder is responsible + // We load a V3 metadata version by default (regardless of current fork) + // since a V3 metadata can be converted to V1 or V2. The RPC encoder is responsible // for sending the correct metadata version based on the negotiated protocol version. - let mut meta_data = MetaDataV2 { + let mut meta_data = MetaDataV3 { seq_number: 0, attnets: EnrAttestationBitfield::::default(), syncnets: EnrSyncCommitteeBitfield::::default(), + custody_group_count, }; + // Read metadata from persisted file if available let metadata_path = network_dir.join(METADATA_FILENAME); if let Ok(mut metadata_file) = File::open(metadata_path) { let mut metadata_ssz = Vec::new(); if metadata_file.read_to_end(&mut metadata_ssz).is_ok() { - // Attempt to read a MetaDataV2 version from the persisted file, - // if that fails, read MetaDataV1 - match MetaDataV2::::from_ssz_bytes(&metadata_ssz) { + // Attempt to read a MetaDataV3 version from the persisted file, + // if that fails, read MetaDataV2 + match MetaDataV3::::from_ssz_bytes(&metadata_ssz) { Ok(persisted_metadata) => { meta_data.seq_number = persisted_metadata.seq_number; // Increment seq number if persisted attnet is not default if persisted_metadata.attnets != meta_data.attnets || persisted_metadata.syncnets != meta_data.syncnets + || persisted_metadata.custody_group_count != meta_data.custody_group_count { meta_data.seq_number += 1; } debug!("Loaded metadata from disk"); } Err(_) => { - match MetaDataV1::::from_ssz_bytes(&metadata_ssz) { + match MetaDataV2::::from_ssz_bytes(&metadata_ssz) { Ok(persisted_metadata) => { - let persisted_metadata = MetaData::V1(persisted_metadata); + let persisted_metadata = MetaData::V2(persisted_metadata); // Increment seq number as the persisted metadata version is updated meta_data.seq_number = *persisted_metadata.seq_number() + 1; debug!("Loaded metadata from disk"); @@ -213,19 +244,8 @@ pub fn load_or_build_metadata( } }; - // Wrap the MetaData - let meta_data = if let Some(custody_group_count) = custody_group_count_opt { - MetaData::V3(MetaDataV3 { - attnets: meta_data.attnets, - seq_number: meta_data.seq_number, - syncnets: meta_data.syncnets, - custody_group_count, - }) - } else { - MetaData::V2(meta_data) - }; - - debug!(seq_num = meta_data.seq_number(), "Metadata sequence number"); + debug!(seq_num = meta_data.seq_number, "Metadata sequence number"); + let meta_data = MetaData::V3(meta_data); save_metadata_to_disk(network_dir, meta_data.clone()); meta_data } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index fd99d93589..f46eb05ceb 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -3,14 +3,14 @@ use super::TopicConfig; use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV3}; use crate::types::{BackFillState, SyncState}; -use crate::{Client, Enr, EnrExt, GossipTopic, Multiaddr, NetworkConfig, PeerId}; +use crate::{Client, Enr, GossipTopic, Multiaddr, NetworkConfig, PeerId}; +use eth2::lighthouse::sync_state::CustodyBackFillState; +use network_utils::enr_ext::EnrExt; use parking_lot::RwLock; use std::collections::HashSet; use std::sync::Arc; use tracing::error; -use types::data_column_custody_group::{ - compute_columns_for_custody_group, compute_subnets_from_custody_group, get_custody_groups, -}; +use types::data_column_custody_group::{compute_subnets_from_custody_group, get_custody_groups}; use types::{ChainSpec, ColumnIndex, DataColumnSubnetId, EthSpec}; pub struct NetworkGlobals { @@ -30,11 +30,10 @@ pub struct NetworkGlobals { pub sync_state: RwLock, /// The current state of the backfill sync. pub backfill_state: RwLock, + /// The current state of custody sync. + pub custody_sync_state: RwLock, /// The computed sampling subnets and columns is stored to avoid re-computing. - pub sampling_subnets: HashSet, - pub sampling_columns: HashSet, - /// Constant custody group count (CGC) set at startup - custody_group_count: u64, + pub sampling_subnets: RwLock>, /// Network-related configuration. Immutable after initialization. pub config: Arc, /// Ethereum chain configuration. Immutable after initialization. @@ -68,24 +67,23 @@ impl NetworkGlobals { // The below `expect` calls will panic on start up if the chain spec config values used // are invalid let sampling_size = spec - .sampling_size(custody_group_count) + .sampling_size_custody_groups(custody_group_count) .expect("should compute node sampling size from valid chain spec"); let custody_groups = get_custody_groups(node_id, sampling_size, &spec) .expect("should compute node custody groups"); let mut sampling_subnets = HashSet::new(); for custody_index in &custody_groups { - let subnets = compute_subnets_from_custody_group(*custody_index, &spec) + let subnets = compute_subnets_from_custody_group::(*custody_index, &spec) .expect("should compute custody subnets for node"); sampling_subnets.extend(subnets); } - let mut sampling_columns = HashSet::new(); - for custody_index in &custody_groups { - let columns = compute_columns_for_custody_group(*custody_index, &spec) - .expect("should compute custody columns for node"); - sampling_columns.extend(columns); - } + tracing::debug!( + cgc = custody_group_count, + ?sampling_subnets, + "Starting node with custody params" + ); NetworkGlobals { local_enr: RwLock::new(enr.clone()), @@ -96,14 +94,31 @@ impl NetworkGlobals { gossipsub_subscriptions: RwLock::new(HashSet::new()), sync_state: RwLock::new(SyncState::Stalled), backfill_state: RwLock::new(BackFillState::Paused), - sampling_subnets, - sampling_columns, - custody_group_count, + custody_sync_state: RwLock::new(CustodyBackFillState::Pending( + "Custody backfill sync initialized".to_string(), + )), + sampling_subnets: RwLock::new(sampling_subnets), config, spec, } } + /// Update the sampling subnets based on an updated cgc. + pub fn update_data_column_subnets(&self, sampling_size: u64) { + // The below `expect` calls will panic on start up if the chain spec config values used + // are invalid + let custody_groups = + get_custody_groups(self.local_enr().node_id().raw(), sampling_size, &self.spec) + .expect("should compute node custody groups"); + + let mut sampling_subnets = self.sampling_subnets.write(); + for custody_index in &custody_groups { + let subnets = compute_subnets_from_custody_group::(*custody_index, &self.spec) + .expect("should compute custody subnets for node"); + sampling_subnets.extend(subnets); + } + } + /// Returns the local ENR from the underlying Discv5 behaviour that external peers may connect /// to. pub fn local_enr(&self) -> Enr { @@ -120,19 +135,6 @@ impl NetworkGlobals { self.listen_multiaddrs.read().clone() } - /// Returns true if this node is configured as a PeerDAS supernode - pub fn is_supernode(&self) -> bool { - self.custody_group_count == self.spec.number_of_custody_groups - } - - /// Returns the count of custody columns this node must sample for block import - pub fn custody_columns_count(&self) -> u64 { - // This only panics if the chain spec contains invalid values - self.spec - .sampling_size(self.custody_group_count) - .expect("should compute node sampling size from valid chain spec") - } - /// Returns the number of libp2p connected peers. pub fn connected_peers(&self) -> usize { self.peers.read().connected_peer_ids().count() @@ -225,11 +227,14 @@ impl NetworkGlobals { TopicConfig { enable_light_client_server: self.config.enable_light_client_server, subscribe_all_subnets: self.config.subscribe_all_subnets, - subscribe_all_data_column_subnets: self.config.subscribe_all_data_column_subnets, - sampling_subnets: &self.sampling_subnets, + sampling_subnets: self.sampling_subnets.read().clone(), } } + pub fn sampling_subnets(&self) -> HashSet { + self.sampling_subnets.read().clone() + } + /// TESTING ONLY. Build a dummy NetworkGlobals instance. pub fn new_test_globals( trusted_peers: Vec, @@ -251,7 +256,7 @@ impl NetworkGlobals { config: Arc, spec: Arc, ) -> NetworkGlobals { - use crate::CombinedKeyExt; + use network_utils::enr_ext::CombinedKeyExt; let keypair = libp2p::identity::secp256k1::Keypair::generate(); let enr_key: discv5::enr::CombinedKey = discv5::enr::CombinedKey::from_secp256k1(&keypair); let enr = discv5::enr::Enr::builder().build(&enr_key).unwrap(); @@ -272,7 +277,13 @@ mod test { spec.fulu_fork_epoch = Some(Epoch::new(0)); let custody_group_count = spec.number_of_custody_groups / 2; - let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); + let sampling_size_custody_groups = spec + .sampling_size_custody_groups(custody_group_count) + .unwrap(); + let expected_sampling_subnet_count = sampling_size_custody_groups + * spec.data_column_sidecar_subnet_count + / spec.number_of_custody_groups; + let metadata = get_metadata(custody_group_count); let config = Arc::new(NetworkConfig::default()); @@ -283,31 +294,8 @@ mod test { Arc::new(spec), ); assert_eq!( - globals.sampling_subnets.len(), - subnet_sampling_size as usize - ); - } - - #[test] - fn test_sampling_columns() { - create_test_tracing_subscriber(); - let mut spec = E::default_spec(); - spec.fulu_fork_epoch = Some(Epoch::new(0)); - - let custody_group_count = spec.number_of_custody_groups / 2; - let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); - let metadata = get_metadata(custody_group_count); - let config = Arc::new(NetworkConfig::default()); - - let globals = NetworkGlobals::::new_test_globals_with_metadata( - vec![], - metadata, - config, - Arc::new(spec), - ); - assert_eq!( - globals.sampling_columns.len(), - subnet_sampling_size as usize + globals.sampling_subnets.read().len(), + expected_sampling_subnet_count as usize ); } diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index 868cdb6eb9..eea8782b2d 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -3,18 +3,19 @@ mod pubsub; mod subnet; mod topics; -use types::{BitVector, EthSpec}; +use ssz_types::BitVector; +use types::EthSpec; pub type EnrAttestationBitfield = BitVector<::SubnetBitfieldLength>; pub type EnrSyncCommitteeBitfield = BitVector<::SyncCommitteeSubnetCount>; pub type Enr = discv5::enr::Enr; -pub use eth2::lighthouse::sync_state::{BackFillState, SyncState}; +pub use eth2::lighthouse::sync_state::{BackFillState, CustodyBackFillState, SyncState}; pub use globals::NetworkGlobals; pub use pubsub::{PubsubMessage, SnappyTransform}; pub use subnet::{Subnet, SubnetDiscovery}; pub use topics::{ - all_topics_at_fork, core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, - GossipEncoding, GossipKind, GossipTopic, TopicConfig, + GossipEncoding, GossipKind, GossipTopic, TopicConfig, all_topics_at_fork, + core_topics_to_subscribe, is_fork_non_core_topic, subnet_from_topic_hash, }; diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 34ebe53113..177d8ec352 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -1,21 +1,21 @@ //! Handles the encoding and decoding of pubsub messages. -use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::TopicHash; -use snap::raw::{decompress_len, Decoder, Encoder}; +use crate::types::{GossipEncoding, GossipKind, GossipTopic}; +use snap::raw::{Decoder, Encoder, decompress_len}; use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - Attestation, AttestationBase, AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, - BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, BlobSidecar, + DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, - SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedInclusionList, SignedVoluntaryExit, SingleAttestation, - SubnetId, SyncCommitteeMessage, SyncSubnetId, + SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockGloas, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedInclusionList, + SignedVoluntaryExit, SingleAttestation, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] @@ -28,10 +28,8 @@ pub enum PubsubMessage { DataColumnSidecar(Box<(DataColumnSubnetId, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. AggregateAndProofAttestation(Box>), - /// Gossipsub message providing notification of a raw un-aggregated attestation with its subnet id. - Attestation(Box<(SubnetId, Attestation)>), - /// Gossipsub message providing notification of a `SingleAttestation`` with its subnet id. - SingleAttestation(Box<(SubnetId, SingleAttestation)>), + /// Gossipsub message providing notification of a `SingleAttestation` with its subnet id. + Attestation(Box<(SubnetId, SingleAttestation)>), /// Gossipsub message providing notification of a voluntary exit. VoluntaryExit(Box), /// Gossipsub message providing notification of a new proposer slashing. @@ -143,9 +141,6 @@ impl PubsubMessage { PubsubMessage::Attestation(attestation_data) => { GossipKind::Attestation(attestation_data.0) } - PubsubMessage::SingleAttestation(attestation_data) => { - GossipKind::Attestation(attestation_data.0) - } PubsubMessage::VoluntaryExit(_) => GossipKind::VoluntaryExit, PubsubMessage::ProposerSlashing(_) => GossipKind::ProposerSlashing, PubsubMessage::AttesterSlashing(_) => GossipKind::AttesterSlashing, @@ -180,118 +175,103 @@ impl PubsubMessage { // the ssz decoders match gossip_topic.kind() { GossipKind::BeaconAggregateAndProof => { - let signed_aggregate_and_proof = - match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(&fork_name) => { - if fork_name.electra_enabled() { - SignedAggregateAndProof::Electra( - SignedAggregateAndProofElectra::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } else { - SignedAggregateAndProof::Base( - SignedAggregateAndProofBase::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } + let signed_aggregate_and_proof = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { + Some(&fork_name) => { + if fork_name.electra_enabled() { + SignedAggregateAndProof::Electra( + SignedAggregateAndProofElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } else { + SignedAggregateAndProof::Base( + SignedAggregateAndProofBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) } - None => { - return Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )) - } - }; + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )); + } + }; Ok(PubsubMessage::AggregateAndProofAttestation(Box::new( signed_aggregate_and_proof, ))) } GossipKind::Attestation(subnet_id) => { - match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(&fork_name) => { - if fork_name.electra_enabled() { - let single_attestation = - SingleAttestation::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; - Ok(PubsubMessage::SingleAttestation(Box::new(( - *subnet_id, - single_attestation, - )))) - } else { - let attestation = Attestation::Base( - AttestationBase::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ); - Ok(PubsubMessage::Attestation(Box::new(( - *subnet_id, - attestation, - )))) - } - } - None => Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )), - } + let attestation = SingleAttestation::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?; + Ok(PubsubMessage::Attestation(Box::new(( + *subnet_id, + attestation, + )))) } GossipKind::BeaconBlock => { - let beacon_block = - match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(ForkName::Base) => SignedBeaconBlock::::Base( - SignedBeaconBlockBase::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Altair) => SignedBeaconBlock::::Altair( - SignedBeaconBlockAltair::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Bellatrix) => SignedBeaconBlock::::Bellatrix( - SignedBeaconBlockBellatrix::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Capella) => SignedBeaconBlock::::Capella( - SignedBeaconBlockCapella::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Deneb) => SignedBeaconBlock::::Deneb( - SignedBeaconBlockDeneb::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Electra) => SignedBeaconBlock::::Electra( - SignedBeaconBlockElectra::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Eip7805) => SignedBeaconBlock::::Eip7805( - SignedBeaconBlockEip7805::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - Some(ForkName::Fulu) => SignedBeaconBlock::::Fulu( - SignedBeaconBlockFulu::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ), - None => { - return Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )) - } - }; + let beacon_block = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { + Some(ForkName::Base) => SignedBeaconBlock::::Base( + SignedBeaconBlockBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Altair) => SignedBeaconBlock::::Altair( + SignedBeaconBlockAltair::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Bellatrix) => SignedBeaconBlock::::Bellatrix( + SignedBeaconBlockBellatrix::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Capella) => SignedBeaconBlock::::Capella( + SignedBeaconBlockCapella::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Deneb) => SignedBeaconBlock::::Deneb( + SignedBeaconBlockDeneb::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Electra) => SignedBeaconBlock::::Electra( + SignedBeaconBlockElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Fulu) => SignedBeaconBlock::::Fulu( + SignedBeaconBlockFulu::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Eip7805) => SignedBeaconBlock::::Eip7805( + SignedBeaconBlockEip7805::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + Some(ForkName::Gloas) => SignedBeaconBlock::::Gloas( + SignedBeaconBlockGloas::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )); + } + }; Ok(PubsubMessage::BeaconBlock(Arc::new(beacon_block))) } GossipKind::BlobSidecar(blob_index) => { if let Some(fork_name) = - fork_context.from_context_bytes(gossip_topic.fork_digest) + fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) + && fork_name.deneb_enabled() { - if fork_name.deneb_enabled() { - let blob_sidecar = Arc::new( - BlobSidecar::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ); - return Ok(PubsubMessage::BlobSidecar(Box::new(( - *blob_index, - blob_sidecar, - )))); - } + let blob_sidecar = Arc::new( + BlobSidecar::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ); + return Ok(PubsubMessage::BlobSidecar(Box::new(( + *blob_index, + blob_sidecar, + )))); } Err(format!( @@ -300,7 +280,7 @@ impl PubsubMessage { )) } GossipKind::DataColumnSidecar(subnet_id) => { - match fork_context.from_context_bytes(gossip_topic.fork_digest) { + match fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) { Some(fork) if fork.fulu_enabled() => { let col_sidecar = Arc::new( DataColumnSidecar::from_ssz_bytes(data) @@ -328,28 +308,29 @@ impl PubsubMessage { Ok(PubsubMessage::ProposerSlashing(Box::new(proposer_slashing))) } GossipKind::AttesterSlashing => { - let attester_slashing = - match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(&fork_name) => { - if fork_name.electra_enabled() { - AttesterSlashing::Electra( - AttesterSlashingElectra::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } else { - AttesterSlashing::Base( - AttesterSlashingBase::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } + let attester_slashing = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { + Some(&fork_name) => { + if fork_name.electra_enabled() { + AttesterSlashing::Electra( + AttesterSlashingElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } else { + AttesterSlashing::Base( + AttesterSlashingBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) } - None => { - return Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )) - } - }; + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )); + } + }; Ok(PubsubMessage::AttesterSlashing(Box::new(attester_slashing))) } GossipKind::SignedContributionAndProof => { @@ -376,37 +357,45 @@ impl PubsubMessage { ))) } GossipKind::LightClientFinalityUpdate => { - let light_client_finality_update = match fork_context.from_context_bytes(gossip_topic.fork_digest) { + let light_client_finality_update = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { Some(&fork_name) => { - LightClientFinalityUpdate::from_ssz_bytes(data, fork_name) + LightClientFinalityUpdate::from_ssz_bytes(data, fork_name) .map_err(|e| format!("{:?}", e))? - }, - None => return Err(format!( - "light_client_finality_update topic invalid for given fork digest {:?}", - gossip_topic.fork_digest - )), + } + None => { + return Err(format!( + "light_client_finality_update topic invalid for given fork digest {:?}", + gossip_topic.fork_digest + )); + } }; Ok(PubsubMessage::LightClientFinalityUpdate(Box::new( light_client_finality_update, ))) } GossipKind::LightClientOptimisticUpdate => { - let light_client_optimistic_update = match fork_context.from_context_bytes(gossip_topic.fork_digest) { + let light_client_optimistic_update = match fork_context + .get_fork_from_context_bytes(gossip_topic.fork_digest) + { Some(&fork_name) => { LightClientOptimisticUpdate::from_ssz_bytes(data, fork_name) - .map_err(|e| format!("{:?}", e))? - }, - None => return Err(format!( - "light_client_optimistic_update topic invalid for given fork digest {:?}", - gossip_topic.fork_digest - )), + .map_err(|e| format!("{:?}", e))? + } + None => { + return Err(format!( + "light_client_optimistic_update topic invalid for given fork digest {:?}", + gossip_topic.fork_digest + )); + } }; Ok(PubsubMessage::LightClientOptimisticUpdate(Box::new( light_client_optimistic_update, ))) } GossipKind::InclusionList => { - match fork_context.from_context_bytes(gossip_topic.fork_digest) { + match fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) { Some(fork) if fork.electra_enabled() => { let il = SignedInclusionList::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?; @@ -449,7 +438,6 @@ impl PubsubMessage { PubsubMessage::ProposerSlashing(data) => data.as_ssz_bytes(), PubsubMessage::AttesterSlashing(data) => data.as_ssz_bytes(), PubsubMessage::Attestation(data) => data.1.as_ssz_bytes(), - PubsubMessage::SingleAttestation(data) => data.1.as_ssz_bytes(), PubsubMessage::SignedContributionAndProof(data) => data.as_ssz_bytes(), PubsubMessage::SyncCommitteeMessage(data) => data.1.as_ssz_bytes(), PubsubMessage::BlsToExecutionChange(data) => data.as_ssz_bytes(), @@ -489,19 +477,9 @@ impl std::fmt::Display for PubsubMessage { att.message().aggregator_index(), ), PubsubMessage::Attestation(data) => write!( - f, - "Attestation: subnet_id: {}, attestation_slot: {}, attestation_index: {:?}", - *data.0, - data.1.data().slot, - data.1.committee_index(), - ), - PubsubMessage::SingleAttestation(data) => write!( f, "SingleAttestation: subnet_id: {}, attestation_slot: {}, committee_index: {:?}, attester_index: {:?}", - *data.0, - data.1.data.slot, - data.1.committee_index, - data.1.attester_index, + *data.0, data.1.data.slot, data.1.committee_index, data.1.attester_index, ), PubsubMessage::VoluntaryExit(_data) => write!(f, "Voluntary Exit"), PubsubMessage::ProposerSlashing(_data) => write!(f, "Proposer Slashing"), diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index 394c7e79aa..692fa2ba58 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -2,7 +2,8 @@ use gossipsub::{IdentTopic as Topic, TopicHash}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use strum::AsRefStr; -use types::{ChainSpec, DataColumnSubnetId, EthSpec, ForkName, SubnetId, SyncSubnetId, Unsigned}; +use typenum::Unsigned; +use types::{ChainSpec, DataColumnSubnetId, EthSpec, ForkName, SubnetId, SyncSubnetId}; use crate::Subnet; @@ -27,11 +28,10 @@ pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update pub const INCLUSION_LIST_TOPIC: &str = "inclusion_list"; #[derive(Debug)] -pub struct TopicConfig<'a> { +pub struct TopicConfig { pub enable_light_client_server: bool, pub subscribe_all_subnets: bool, - pub subscribe_all_data_column_subnets: bool, - pub sampling_subnets: &'a HashSet, + pub sampling_subnets: HashSet, } /// Returns all the topics the node should subscribe at `fork_name` @@ -85,14 +85,8 @@ pub fn core_topics_to_subscribe( } if fork_name.fulu_enabled() { - if opts.subscribe_all_data_column_subnets { - for i in 0..spec.data_column_sidecar_subnet_count { - topics.push(GossipKind::DataColumnSidecar(i.into())); - } - } else { - for subnet in opts.sampling_subnets { - topics.push(GossipKind::DataColumnSidecar(*subnet)); - } + for subnet in &opts.sampling_subnets { + topics.push(GossipKind::DataColumnSidecar(*subnet)); } } @@ -131,8 +125,7 @@ pub fn all_topics_at_fork(fork: ForkName, spec: &ChainSpec) -> Vec(fork, &opts, spec) } @@ -196,8 +189,8 @@ impl std::fmt::Display for GossipKind { GossipKind::BlobSidecar(blob_index) => { write!(f, "{}{}", BLOB_SIDECAR_PREFIX, blob_index) } - GossipKind::DataColumnSidecar(column_index) => { - write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_index) + GossipKind::DataColumnSidecar(column_subnet_id) => { + write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_subnet_id) } x => f.write_str(x.as_ref()), } @@ -326,8 +319,8 @@ impl std::fmt::Display for GossipTopic { GossipKind::BlobSidecar(blob_index) => { format!("{}{}", BLOB_SIDECAR_PREFIX, blob_index) } - GossipKind::DataColumnSidecar(index) => { - format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *index) + GossipKind::DataColumnSidecar(column_subnet_id) => { + format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *column_subnet_id) } GossipKind::BlsToExecutionChange => BLS_TO_EXECUTION_CHANGE_TOPIC.into(), GossipKind::LightClientFinalityUpdate => LIGHT_CLIENT_FINALITY_UPDATE.into(), @@ -530,8 +523,7 @@ mod tests { TopicConfig { enable_light_client_server: false, subscribe_all_subnets: false, - subscribe_all_data_column_subnets: false, - sampling_subnets, + sampling_subnets: sampling_subnets.clone(), } } @@ -541,8 +533,10 @@ mod tests { let s = get_sampling_subnets(); let topic_config = get_topic_config(&s); for fork in ForkName::list_all() { - assert!(core_topics_to_subscribe::(fork, &topic_config, &spec,) - .contains(&GossipKind::BeaconBlock)); + assert!( + core_topics_to_subscribe::(fork, &topic_config, &spec,) + .contains(&GossipKind::BeaconBlock) + ); } } @@ -560,9 +554,8 @@ mod tests { #[test] fn columns_are_subscribed_in_peerdas() { let spec = get_spec(); - let s = get_sampling_subnets(); - let mut topic_config = get_topic_config(&s); - topic_config.subscribe_all_data_column_subnets = true; + let s = HashSet::from_iter([0.into()]); + let topic_config = get_topic_config(&s); assert!( core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec) .contains(&GossipKind::DataColumnSidecar(0.into())) diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 332e8a73cd..63bad3530f 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -1,54 +1,57 @@ #![cfg(test)] -use lighthouse_network::service::Network as LibP2PService; +use fixed_bytes::FixedBytesExtended; use lighthouse_network::Enr; -use lighthouse_network::EnrExt; use lighthouse_network::Multiaddr; +use lighthouse_network::service::Network as LibP2PService; use lighthouse_network::{NetworkConfig, NetworkEvent}; +use network_utils::enr_ext::EnrExt; use std::sync::Arc; use std::sync::Weak; use tokio::runtime::Runtime; -use tracing::{debug, error, info_span, Instrument}; +use tracing::{Instrument, debug, error, info_span}; use tracing_subscriber::EnvFilter; -use types::{ - ChainSpec, EnrForkId, Epoch, EthSpec, FixedBytesExtended, ForkContext, ForkName, Hash256, - MinimalEthSpec, Slot, -}; +use types::{ChainSpec, EnrForkId, Epoch, EthSpec, ForkContext, ForkName, Hash256, MinimalEthSpec}; type E = MinimalEthSpec; +use lighthouse_network::identity::secp256k1; use lighthouse_network::rpc::config::InboundRateLimiterConfig; use tempfile::Builder as TempBuilder; -/// Returns a dummy fork context -pub fn fork_context(fork_name: ForkName) -> ForkContext { +/// Returns a chain spec with all forks enabled. +pub fn spec_with_all_forks_enabled() -> ChainSpec { let mut chain_spec = E::default_spec(); - let altair_fork_epoch = Epoch::new(1); - let bellatrix_fork_epoch = Epoch::new(2); - let capella_fork_epoch = Epoch::new(3); - let deneb_fork_epoch = Epoch::new(4); - let electra_fork_epoch = Epoch::new(5); - let eip7805_fork_epoch = Epoch::new(6); - let fulu_fork_epoch = Epoch::new(7); + chain_spec.altair_fork_epoch = Some(Epoch::new(1)); + chain_spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + chain_spec.capella_fork_epoch = Some(Epoch::new(3)); + chain_spec.deneb_fork_epoch = Some(Epoch::new(4)); + chain_spec.electra_fork_epoch = Some(Epoch::new(5)); + chain_spec.fulu_fork_epoch = Some(Epoch::new(6)); + chain_spec.eip7805_fork_epoch = Some(Epoch::new(7)); + chain_spec.gloas_fork_epoch = Some(Epoch::new(8)); - chain_spec.altair_fork_epoch = Some(altair_fork_epoch); - chain_spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - chain_spec.capella_fork_epoch = Some(capella_fork_epoch); - chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); - chain_spec.electra_fork_epoch = Some(electra_fork_epoch); - chain_spec.eip7805_fork_epoch = Some(eip7805_fork_epoch); - chain_spec.fulu_fork_epoch = Some(fulu_fork_epoch); + // check that we have all forks covered + assert!(chain_spec.fork_epoch(ForkName::latest()).is_some()); + chain_spec +} - let current_slot = match fork_name { - ForkName::Base => Slot::new(0), - ForkName::Altair => altair_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Bellatrix => bellatrix_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Capella => capella_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Deneb => deneb_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Electra => electra_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Eip7805 => eip7805_fork_epoch.start_slot(E::slots_per_epoch()), - ForkName::Fulu => fulu_fork_epoch.start_slot(E::slots_per_epoch()), +/// Returns a dummy fork context +pub fn fork_context(fork_name: ForkName, spec: &ChainSpec) -> ForkContext { + let current_epoch = match fork_name { + ForkName::Base => Some(Epoch::new(0)), + ForkName::Altair => spec.altair_fork_epoch, + ForkName::Bellatrix => spec.bellatrix_fork_epoch, + ForkName::Capella => spec.capella_fork_epoch, + ForkName::Deneb => spec.deneb_fork_epoch, + ForkName::Electra => spec.electra_fork_epoch, + ForkName::Fulu => spec.fulu_fork_epoch, + ForkName::Eip7805 => spec.eip7805_fork_epoch, + ForkName::Gloas => spec.gloas_fork_epoch, }; - ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) + let current_slot = current_epoch + .unwrap_or_else(|| panic!("expect fork {fork_name} to be scheduled")) + .start_slot(E::slots_per_epoch()); + ForkContext::new::(current_slot, Hash256::zero(), spec) } pub struct Libp2pInstance( @@ -72,12 +75,18 @@ impl std::ops::DerefMut for Libp2pInstance { } #[allow(unused)] -pub fn build_tracing_subscriber(level: &str, enabled: bool) { +pub fn build_tracing_subscriber( + level: &str, + enabled: bool, +) -> Option { if enabled { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::try_new(level).unwrap()) - .try_init() - .unwrap(); + Some(tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_new(level).unwrap()) + .finish(), + )) + } else { + None } } @@ -100,7 +109,7 @@ pub fn build_config( config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, port, port, port); config.enr_address = (Some(std::net::Ipv4Addr::LOCALHOST), None); config.boot_nodes_enr.append(&mut boot_nodes); - config.network_dir = path.into_path(); + config.network_dir = path.keep(); config.disable_peer_scoring = disable_peer_scoring; config.inbound_rate_limiter_config = inbound_rate_limiter; Arc::new(config) @@ -121,18 +130,24 @@ pub async fn build_libp2p_instance( let (signal, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new(rt, exit, shutdown_tx, service_name); + let custody_group_count = chain_spec.custody_requirement; let libp2p_context = lighthouse_network::Context { config, enr_fork_id: EnrForkId::default(), - fork_context: Arc::new(fork_context(fork_name)), + fork_context: Arc::new(fork_context(fork_name, &chain_spec)), chain_spec, libp2p_registry: None, }; Libp2pInstance( - LibP2PService::new(executor, libp2p_context) - .await - .expect("should build libp2p instance") - .0, + LibP2PService::new( + executor, + libp2p_context, + custody_group_count, + secp256k1::Keypair::generate().into(), + ) + .await + .expect("should build libp2p instance") + .0, signal, ) } diff --git a/beacon_node/lighthouse_network/tests/main.rs b/beacon_node/lighthouse_network/tests/main.rs new file mode 100644 index 0000000000..2ed0eabaff --- /dev/null +++ b/beacon_node/lighthouse_network/tests/main.rs @@ -0,0 +1,2 @@ +mod common; +mod rpc_tests; diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 9b43e8b581..599fcd242b 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -1,22 +1,25 @@ #![cfg(test)] -mod common; - -use common::{build_tracing_subscriber, Protocol}; -use lighthouse_network::rpc::{methods::*, RequestType}; +use crate::common; +use crate::common::spec_with_all_forks_enabled; +use crate::common::{Protocol, build_tracing_subscriber}; +use bls::Signature; +use fixed_bytes::FixedBytesExtended; +use lighthouse_network::rpc::{RequestType, methods::*}; use lighthouse_network::service::api_types::AppRequestId; use lighthouse_network::{NetworkEvent, ReportSource, Response}; use ssz::Encode; -use ssz_types::VariableList; +use ssz_types::{RuntimeVariableList, VariableList}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::runtime::Runtime; use tokio::time::sleep; -use tracing::{debug, error, warn}; +use tracing::{Instrument, debug, error, info_span, warn}; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BlobSidecar, ChainSpec, - EmptyBlock, Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, MinimalEthSpec, - RuntimeVariableList, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockHeader, + BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EmptyBlock, Epoch, + EthSpec, ForkName, Hash256, KzgCommitment, KzgProof, MinimalEthSpec, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; type E = MinimalEthSpec; @@ -24,8 +27,8 @@ type E = MinimalEthSpec; /// Bellatrix block with length < max_rpc_size. fn bellatrix_block_small(spec: &ChainSpec) -> BeaconBlock { let mut block = BeaconBlockBellatrix::::empty(spec); - let tx = VariableList::from(vec![0; 1024]); - let txs = VariableList::from(std::iter::repeat_n(tx, 5000).collect::>()); + let tx = VariableList::try_from(vec![0; 1024]).unwrap(); + let txs = VariableList::try_from(std::iter::repeat_n(tx, 5000).collect::>()).unwrap(); block.body.execution_payload.execution_payload.transactions = txs; @@ -39,8 +42,8 @@ fn bellatrix_block_small(spec: &ChainSpec) -> BeaconBlock { /// Hence, we generate a bellatrix block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. fn bellatrix_block_large(spec: &ChainSpec) -> BeaconBlock { let mut block = BeaconBlockBellatrix::::empty(spec); - let tx = VariableList::from(vec![0; 1024]); - let txs = VariableList::from(std::iter::repeat_n(tx, 100000).collect::>()); + let tx = VariableList::try_from(vec![0; 1024]).unwrap(); + let txs = VariableList::try_from(std::iter::repeat_n(tx, 100000).collect::>()).unwrap(); block.body.execution_payload.execution_payload.transactions = txs; @@ -55,12 +58,12 @@ fn bellatrix_block_large(spec: &ChainSpec) -> BeaconBlock { fn test_tcp_status_rpc() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // get sender/receiver @@ -75,22 +78,24 @@ fn test_tcp_status_rpc() { .await; // Dummy STATUS RPC message - let rpc_request = RequestType::Status(StatusMessage { + let rpc_request = RequestType::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), head_root: Hash256::zero(), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // Dummy STATUS RPC message - let rpc_response = Response::Status(StatusMessage { + let rpc_response = Response::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), head_root: Hash256::zero(), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // build the sender future let sender_future = async { @@ -117,7 +122,8 @@ fn test_tcp_status_rpc() { _ => {} } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -141,7 +147,8 @@ fn test_tcp_status_rpc() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -159,14 +166,14 @@ fn test_tcp_status_rpc() { fn test_tcp_blocks_by_range_chunked_rpc() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 6; let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // get sender/receiver @@ -245,7 +252,8 @@ fn test_tcp_blocks_by_range_chunked_rpc() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -286,7 +294,8 @@ fn test_tcp_blocks_by_range_chunked_rpc() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -304,8 +313,8 @@ fn test_tcp_blocks_by_range_chunked_rpc() { fn test_blobs_by_range_chunked_rpc() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let slot_count = 32; let messages_to_send = 34; @@ -314,7 +323,7 @@ fn test_blobs_by_range_chunked_rpc() { rt.block_on(async { // get sender/receiver - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), ForkName::Deneb, @@ -326,13 +335,18 @@ fn test_blobs_by_range_chunked_rpc() { .await; // BlobsByRange Request + let deneb_slot = spec + .deneb_fork_epoch + .expect("deneb must be scheduled") + .start_slot(E::slots_per_epoch()); let rpc_request = RequestType::BlobsByRange(BlobsByRangeRequest { - start_slot: 0, + start_slot: deneb_slot.as_u64(), count: slot_count, }); - // BlocksByRange Response - let blob = BlobSidecar::::empty(); + // BlobsByRange Response + let mut blob = BlobSidecar::::empty(); + blob.signed_block_header.message.slot = deneb_slot; let rpc_response = Response::BlobsByRange(Some(Arc::new(blob))); @@ -373,7 +387,8 @@ fn test_blobs_by_range_chunked_rpc() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -407,7 +422,8 @@ fn test_blobs_by_range_chunked_rpc() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -425,14 +441,14 @@ fn test_blobs_by_range_chunked_rpc() { fn test_tcp_blocks_by_range_over_limit() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 5; let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // get sender/receiver @@ -479,7 +495,8 @@ fn test_tcp_blocks_by_range_over_limit() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -512,7 +529,8 @@ fn test_tcp_blocks_by_range_over_limit() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -529,15 +547,15 @@ fn test_tcp_blocks_by_range_over_limit() { fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 10; let extra_messages_to_send = 10; let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // get sender/receiver @@ -601,7 +619,8 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // determine messages to send (PeerId, RequestId). If some, indicates we still need to send // messages @@ -637,9 +656,8 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { } // if we need to send messages send them here. This will happen after a delay - if message_info.is_some() { + if let Some((peer_id, inbound_request_id)) = &message_info { messages_sent += 1; - let (peer_id, inbound_request_id) = message_info.as_ref().unwrap(); receiver.send_response(*peer_id, *inbound_request_id, rpc_response.clone()); debug!("Sending message {}", messages_sent); if messages_sent == messages_to_send + extra_messages_to_send { @@ -648,7 +666,8 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { } } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -666,12 +685,12 @@ fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { fn test_tcp_blocks_by_range_single_empty_rpc() { // Set up the logging. let log_level = "trace"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // get sender/receiver @@ -734,7 +753,8 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -767,7 +787,8 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} _ = receiver_future => {} @@ -787,19 +808,20 @@ fn test_tcp_blocks_by_range_single_empty_rpc() { fn test_tcp_blocks_by_root_chunked_rpc() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let messages_to_send = 6; - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); + let current_fork_name = ForkName::Bellatrix; let rt = Arc::new(Runtime::new().unwrap()); // get sender/receiver rt.block_on(async { let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - ForkName::Bellatrix, + current_fork_name, spec.clone(), Protocol::Tcp, false, @@ -810,7 +832,7 @@ fn test_tcp_blocks_by_root_chunked_rpc() { // BlocksByRoot Request let rpc_request = RequestType::BlocksByRoot(BlocksByRootRequest::V2(BlocksByRootRequestV2 { - block_roots: RuntimeVariableList::from_vec( + block_roots: RuntimeVariableList::new( vec![ Hash256::zero(), Hash256::zero(), @@ -819,8 +841,9 @@ fn test_tcp_blocks_by_root_chunked_rpc() { Hash256::zero(), Hash256::zero(), ], - spec.max_request_blocks_upper_bound(), - ), + spec.max_request_blocks(current_fork_name), + ) + .unwrap(), })); // BlocksByRoot Response @@ -877,7 +900,8 @@ fn test_tcp_blocks_by_root_chunked_rpc() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -916,11 +940,320 @@ fn test_tcp_blocks_by_root_chunked_rpc() { _ => {} // Ignore other events } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} _ = receiver_future => {} - _ = sleep(Duration::from_secs(30)) => { + _ = sleep(Duration::from_secs(300)) => { + panic!("Future timed out"); + } + } + }) +} + +#[test] +#[allow(clippy::single_match)] +fn test_tcp_columns_by_root_chunked_rpc() { + // Set up the logging. + let log_level = "debug"; + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); + let num_of_columns = E::number_of_columns(); + let messages_to_send = 32 * num_of_columns; + + let spec = Arc::new(spec_with_all_forks_enabled()); + let current_fork_name = ForkName::Fulu; + + let rt = Arc::new(Runtime::new().unwrap()); + // get sender/receiver + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + current_fork_name, + spec.clone(), + Protocol::Tcp, + false, + None, + ) + .await; + + // DataColumnsByRootRequest Request + + let max_request_blocks = spec.max_request_blocks(current_fork_name); + let req = DataColumnsByRootRequest::new( + vec![ + DataColumnsByRootIdentifier { + block_root: Hash256::zero(), + columns: VariableList::new( + (0..E::number_of_columns() as u64).collect::>() + ) + .unwrap(), + }; + max_request_blocks + ], + max_request_blocks, + ) + .unwrap(); + let req_bytes = req.data_column_ids.as_ssz_bytes(); + let req_decoded = DataColumnsByRootRequest { + data_column_ids: >>::from_ssz_bytes( + &req_bytes, + spec.max_request_blocks(current_fork_name), + ) + .unwrap(), + }; + assert_eq!(req, req_decoded); + let rpc_request = RequestType::DataColumnsByRoot(req); + + // DataColumnsByRoot Response + let data_column = Arc::new(DataColumnSidecar { + index: 1, + signed_block_header: SignedBeaconBlockHeader { + message: BeaconBlockHeader { + slot: 320u64.into(), + proposer_index: 1, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body_root: Hash256::zero(), + }, + signature: Signature::empty(), + }, + column: vec![vec![0; E::bytes_per_cell()].try_into().unwrap()] + .try_into() + .unwrap(), + kzg_commitments: vec![KzgCommitment::empty_for_testing()].try_into().unwrap(), + kzg_proofs: vec![KzgProof::empty()].try_into().unwrap(), + kzg_commitments_inclusion_proof: vec![ + Hash256::zero(); + E::kzg_commitments_inclusion_proof_depth() + ] + .try_into() + .unwrap(), + }); + + let rpc_response = Response::DataColumnsByRoot(Some(data_column.clone())); + + // keep count of the number of messages received + let mut messages_received = 0; + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + tracing::info!("Sending RPC"); + tokio::time::sleep(Duration::from_secs(1)).await; + sender + .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) + .unwrap(); + } + NetworkEvent::ResponseReceived { + peer_id: _, + app_request_id: AppRequestId::Router, + response, + } => match response { + Response::DataColumnsByRoot(Some(sidecar)) => { + assert_eq!(sidecar, data_column.clone()); + messages_received += 1; + tracing::info!("Chunk received"); + } + Response::DataColumnsByRoot(None) => { + // should be exactly messages_to_send + assert_eq!(messages_received, messages_to_send); + // end the test + return; + } + _ => {} // Ignore other RPC messages + }, + _ => {} // Ignore other behaviour events + } + } + } + .instrument(info_span!("Sender")); + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + } => { + if request_type == rpc_request { + // send the response + tracing::info!("Receiver got request"); + + for _ in 0..messages_to_send { + receiver.send_response( + peer_id, + inbound_request_id, + rpc_response.clone(), + ); + tracing::info!("Sending message"); + } + // send the stream termination + receiver.send_response( + peer_id, + inbound_request_id, + Response::DataColumnsByRoot(None), + ); + tracing::info!("Send stream term"); + } + } + e => { + tracing::info!(?e, "Got event"); + } // Ignore other events + } + } + } + .instrument(info_span!("Receiver")); + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(300)) => { + panic!("Future timed out"); + } + } + }) +} + +#[test] +#[allow(clippy::single_match)] +fn test_tcp_columns_by_range_chunked_rpc() { + // Set up the logging. + let log_level = "debug"; + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); + + let messages_to_send = 32; + + let spec = Arc::new(spec_with_all_forks_enabled()); + let current_fork_name = ForkName::Fulu; + + let rt = Arc::new(Runtime::new().unwrap()); + // get sender/receiver + rt.block_on(async { + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + current_fork_name, + spec.clone(), + Protocol::Tcp, + false, + None, + ) + .await; + + // DataColumnsByRange Request + let rpc_request = RequestType::DataColumnsByRange(DataColumnsByRangeRequest { + start_slot: 320, + count: 32, + columns: (0..E::number_of_columns() as u64).collect(), + }); + + // DataColumnsByRange Response + let data_column = Arc::new(DataColumnSidecar { + index: 1, + signed_block_header: SignedBeaconBlockHeader { + message: BeaconBlockHeader { + slot: 320u64.into(), + proposer_index: 1, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body_root: Hash256::zero(), + }, + signature: Signature::empty(), + }, + column: vec![vec![0; E::bytes_per_cell()].try_into().unwrap()] + .try_into() + .unwrap(), + kzg_commitments: vec![KzgCommitment::empty_for_testing()].try_into().unwrap(), + kzg_proofs: vec![KzgProof::empty()].try_into().unwrap(), + kzg_commitments_inclusion_proof: vec![ + Hash256::zero(); + E::kzg_commitments_inclusion_proof_depth() + ] + .try_into() + .unwrap(), + }); + + let rpc_response = Response::DataColumnsByRange(Some(data_column.clone())); + + // keep count of the number of messages received + let mut messages_received = 0; + // build the sender future + let sender_future = async { + loop { + match sender.next_event().await { + NetworkEvent::PeerConnectedOutgoing(peer_id) => { + tracing::info!("Sending RPC"); + sender + .send_request(peer_id, AppRequestId::Router, rpc_request.clone()) + .unwrap(); + } + NetworkEvent::ResponseReceived { + peer_id: _, + app_request_id: AppRequestId::Router, + response, + } => match response { + Response::DataColumnsByRange(Some(sidecar)) => { + assert_eq!(sidecar, data_column.clone()); + messages_received += 1; + tracing::info!("Chunk received"); + } + Response::DataColumnsByRange(None) => { + // should be exactly messages_to_send + assert_eq!(messages_received, messages_to_send); + // end the test + return; + } + _ => {} // Ignore other RPC messages + }, + _ => {} // Ignore other behaviour events + } + } + } + .instrument(info_span!("Sender")); + + // build the receiver future + let receiver_future = async { + loop { + match receiver.next_event().await { + NetworkEvent::RequestReceived { + peer_id, + inbound_request_id, + request_type, + } => { + if request_type == rpc_request { + // send the response + tracing::info!("Receiver got request"); + + for _ in 0..messages_to_send { + receiver.send_response( + peer_id, + inbound_request_id, + rpc_response.clone(), + ); + tracing::info!("Sending message"); + } + // send the stream termination + receiver.send_response( + peer_id, + inbound_request_id, + Response::DataColumnsByRange(None), + ); + tracing::info!("Send stream term"); + } + } + _ => {} // Ignore other events + } + } + } + .instrument(info_span!("Receiver")); + tokio::select! { + _ = sender_future => {} + _ = receiver_future => {} + _ = sleep(Duration::from_secs(300)) => { panic!("Future timed out"); } } @@ -932,20 +1265,21 @@ fn test_tcp_blocks_by_root_chunked_rpc() { fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { // Set up the logging. let log_level = "debug"; - let enable_logging = false; - build_tracing_subscriber(log_level, enable_logging); + let enable_logging = true; + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let messages_to_send: u64 = 10; let extra_messages_to_send: u64 = 10; - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); + let current_fork = ForkName::Base; let rt = Arc::new(Runtime::new().unwrap()); // get sender/receiver rt.block_on(async { let (mut sender, mut receiver) = common::build_node_pair( Arc::downgrade(&rt), - ForkName::Base, + current_fork, spec.clone(), Protocol::Tcp, false, @@ -956,7 +1290,7 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { // BlocksByRoot Request let rpc_request = RequestType::BlocksByRoot(BlocksByRootRequest::V2(BlocksByRootRequestV2 { - block_roots: RuntimeVariableList::from_vec( + block_roots: RuntimeVariableList::new( vec![ Hash256::zero(), Hash256::zero(), @@ -969,8 +1303,9 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { Hash256::zero(), Hash256::zero(), ], - spec.max_request_blocks_upper_bound(), - ), + spec.max_request_blocks(current_fork), + ) + .unwrap(), })); // BlocksByRoot Response @@ -1015,7 +1350,8 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { _ => {} // Ignore other behaviour events } } - }; + } + .instrument(info_span!("Sender")); // determine messages to send (PeerId, RequestId). If some, indicates we still need to send // messages @@ -1051,9 +1387,8 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { } // if we need to send messages send them here. This will happen after a delay - if message_info.is_some() { + if let Some((peer_id, inbound_request_id)) = &message_info { messages_sent += 1; - let (peer_id, inbound_request_id) = message_info.as_ref().unwrap(); receiver.send_response(*peer_id, *inbound_request_id, rpc_response.clone()); debug!("Sending message {}", messages_sent); if messages_sent == messages_to_send + extra_messages_to_send { @@ -1062,7 +1397,8 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { } } } - }; + } + .instrument(info_span!("Receiver")); tokio::select! { _ = sender_future => {} @@ -1078,11 +1414,11 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { /// Goodbye message. fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { // Set up the logging. - build_tracing_subscriber(log_level, enable_logging); + let _subscriber = build_tracing_subscriber(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); // get sender/receiver rt.block_on(async { @@ -1115,7 +1451,8 @@ fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { _ => {} // Ignore other RPC messages } } - }; + } + .instrument(info_span!("Sender")); // build the receiver future let receiver_future = async { @@ -1125,7 +1462,8 @@ fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { return; } } - }; + } + .instrument(info_span!("Receiver")); let total_future = futures::future::join(sender_future, receiver_future); @@ -1143,7 +1481,7 @@ fn goodbye_test(log_level: &str, enable_logging: bool, protocol: Protocol) { #[allow(clippy::single_match)] fn tcp_test_goodbye_rpc() { let log_level = "debug"; - let enabled_logging = false; + let enabled_logging = true; goodbye_test(log_level, enabled_logging, Protocol::Tcp); } @@ -1152,15 +1490,17 @@ fn tcp_test_goodbye_rpc() { #[allow(clippy::single_match)] fn quic_test_goodbye_rpc() { let log_level = "debug"; - let enabled_logging = false; + let enabled_logging = true; goodbye_test(log_level, enabled_logging, Protocol::Quic); } // Test that the receiver delays the responses during response rate-limiting. #[test] fn test_delayed_rpc_response() { + // Set up the logging. + let _subscriber = build_tracing_subscriber("debug", true); let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); // Allow 1 token to be use used every 3 seconds. const QUOTA_SEC: u64 = 3; @@ -1179,22 +1519,24 @@ fn test_delayed_rpc_response() { .await; // Dummy STATUS RPC message - let rpc_request = RequestType::Status(StatusMessage { + let rpc_request = RequestType::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::from_low_u64_be(0), finalized_epoch: Epoch::new(1), head_root: Hash256::from_low_u64_be(0), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // Dummy STATUS RPC message - let rpc_response = Response::Status(StatusMessage { + let rpc_response = Response::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::from_low_u64_be(0), finalized_epoch: Epoch::new(1), head_root: Hash256::from_low_u64_be(0), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // build the sender future let sender_future = async { @@ -1214,7 +1556,7 @@ fn test_delayed_rpc_response() { app_request_id: _, response, } => { - debug!(%request_id, "Sender received"); + debug!(%request_id, elapsed = ?request_sent_at.elapsed(), "Sender received response"); assert_eq!(response, rpc_response); match request_id { @@ -1226,10 +1568,12 @@ fn test_delayed_rpc_response() { // The second and subsequent responses are delayed due to the response rate-limiter on the receiver side. // Adding a slight margin to the elapsed time check to account for potential timing issues caused by system // scheduling or execution delays during testing. + // https://github.com/sigp/lighthouse/issues/7466 + let margin = 500; assert!( request_sent_at.elapsed() > (Duration::from_secs(QUOTA_SEC) - - Duration::from_millis(100)) + - Duration::from_millis(margin)) ); if request_id == 5 { // End the test @@ -1289,8 +1633,10 @@ fn test_delayed_rpc_response() { // once, thanks to the self-limiter on the sender side. #[test] fn test_active_requests() { + // Set up the logging. + let _subscriber = build_tracing_subscriber("debug", true); let rt = Arc::new(Runtime::new().unwrap()); - let spec = Arc::new(E::default_spec()); + let spec = Arc::new(spec_with_all_forks_enabled()); rt.block_on(async { // Get sender/receiver. @@ -1305,22 +1651,24 @@ fn test_active_requests() { .await; // Dummy STATUS RPC request. - let rpc_request = RequestType::Status(StatusMessage { + let rpc_request = RequestType::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::from_low_u64_be(0), finalized_epoch: Epoch::new(1), head_root: Hash256::from_low_u64_be(0), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // Dummy STATUS RPC response. - let rpc_response = Response::Status(StatusMessage { + let rpc_response = Response::Status(StatusMessage::V2(StatusMessageV2 { fork_digest: [0; 4], finalized_root: Hash256::zero(), finalized_epoch: Epoch::new(1), head_root: Hash256::zero(), head_slot: Slot::new(1), - }); + earliest_available_slot: Slot::new(0), + })); // Number of requests. const REQUESTS: u8 = 10; diff --git a/beacon_node/lighthouse_tracing/Cargo.toml b/beacon_node/lighthouse_tracing/Cargo.toml new file mode 100644 index 0000000000..cd71c20253 --- /dev/null +++ b/beacon_node/lighthouse_tracing/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "lighthouse_tracing" +version = "0.1.0" +edition = { workspace = true } diff --git a/beacon_node/lighthouse_tracing/src/lib.rs b/beacon_node/lighthouse_tracing/src/lib.rs new file mode 100644 index 0000000000..56dccadaa9 --- /dev/null +++ b/beacon_node/lighthouse_tracing/src/lib.rs @@ -0,0 +1,80 @@ +//! This module contains root span identifiers for key code paths in the beacon node. +//! +//! TODO: These span identifiers will be used to implement selective tracing export (to be implemented), +//! where only the listed root spans and their descendants will be exported to the tracing backend. + +/// Root span names for block production and publishing +pub const SPAN_PRODUCE_BLOCK_V2: &str = "produce_block_v2"; +pub const SPAN_PRODUCE_BLOCK_V3: &str = "produce_block_v3"; +pub const SPAN_PUBLISH_BLOCK: &str = "publish_block"; + +/// Data Availability checker span identifiers +pub const SPAN_PENDING_COMPONENTS: &str = "pending_components"; + +/// Gossip methods root spans +pub const SPAN_PROCESS_GOSSIP_DATA_COLUMN: &str = "process_gossip_data_column"; +pub const SPAN_PROCESS_GOSSIP_BLOB: &str = "process_gossip_blob"; +pub const SPAN_PROCESS_GOSSIP_BLOCK: &str = "process_gossip_block"; + +/// Sync methods root spans +pub const SPAN_SYNCING_CHAIN: &str = "syncing_chain"; +pub const SPAN_OUTGOING_RANGE_REQUEST: &str = "outgoing_range_request"; +pub const SPAN_SINGLE_BLOCK_LOOKUP: &str = "single_block_lookup"; +pub const SPAN_OUTGOING_BLOCK_BY_ROOT_REQUEST: &str = "outgoing_block_by_root_request"; +pub const SPAN_OUTGOING_CUSTODY_REQUEST: &str = "outgoing_custody_request"; +pub const SPAN_PROCESS_RPC_BLOCK: &str = "process_rpc_block"; +pub const SPAN_PROCESS_RPC_BLOBS: &str = "process_rpc_blobs"; +pub const SPAN_PROCESS_RPC_CUSTODY_COLUMNS: &str = "process_rpc_custody_columns"; +pub const SPAN_PROCESS_CHAIN_SEGMENT: &str = "process_chain_segment"; +pub const SPAN_CUSTODY_BACKFILL_SYNC_BATCH_REQUEST: &str = "custody_backfill_sync_batch_request"; +pub const SPAN_PROCESS_CHAIN_SEGMENT_BACKFILL: &str = "process_chain_segment_backfill"; +pub const SPAN_CUSTODY_BACKFILL_SYNC_IMPORT_COLUMNS: &str = "custody_backfill_sync_import_columns"; + +/// Fork choice root spans +pub const SPAN_RECOMPUTE_HEAD: &str = "recompute_head_at_slot"; + +/// RPC methods root spans +pub const SPAN_HANDLE_BLOCKS_BY_RANGE_REQUEST: &str = "handle_blocks_by_range_request"; +pub const SPAN_HANDLE_BLOBS_BY_RANGE_REQUEST: &str = "handle_blobs_by_range_request"; +pub const SPAN_HANDLE_DATA_COLUMNS_BY_RANGE_REQUEST: &str = "handle_data_columns_by_range_request"; +pub const SPAN_HANDLE_BLOCKS_BY_ROOT_REQUEST: &str = "handle_blocks_by_root_request"; +pub const SPAN_HANDLE_BLOBS_BY_ROOT_REQUEST: &str = "handle_blobs_by_root_request"; +pub const SPAN_HANDLE_DATA_COLUMNS_BY_ROOT_REQUEST: &str = "handle_data_columns_by_root_request"; +pub const SPAN_HANDLE_LIGHT_CLIENT_UPDATES_BY_RANGE: &str = "handle_light_client_updates_by_range"; +pub const SPAN_HANDLE_LIGHT_CLIENT_BOOTSTRAP: &str = "handle_light_client_bootstrap"; +pub const SPAN_HANDLE_LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = + "handle_light_client_optimistic_update"; +pub const SPAN_HANDLE_LIGHT_CLIENT_FINALITY_UPDATE: &str = "handle_light_client_finality_update"; + +/// List of all root span names that are allowed to be exported to the tracing backend. +/// Only these spans and their descendants will be processed to reduce noise from +/// uninstrumented code paths. New root spans must be added to this list to be traced. +pub const LH_BN_ROOT_SPAN_NAMES: &[&str] = &[ + SPAN_PRODUCE_BLOCK_V2, + SPAN_PRODUCE_BLOCK_V3, + SPAN_PUBLISH_BLOCK, + SPAN_PENDING_COMPONENTS, + SPAN_PROCESS_GOSSIP_DATA_COLUMN, + SPAN_PROCESS_GOSSIP_BLOB, + SPAN_PROCESS_GOSSIP_BLOCK, + SPAN_SYNCING_CHAIN, + SPAN_OUTGOING_RANGE_REQUEST, + SPAN_SINGLE_BLOCK_LOOKUP, + SPAN_PROCESS_RPC_BLOCK, + SPAN_PROCESS_RPC_BLOBS, + SPAN_PROCESS_RPC_CUSTODY_COLUMNS, + SPAN_PROCESS_CHAIN_SEGMENT, + SPAN_PROCESS_CHAIN_SEGMENT_BACKFILL, + SPAN_HANDLE_BLOCKS_BY_RANGE_REQUEST, + SPAN_HANDLE_BLOBS_BY_RANGE_REQUEST, + SPAN_HANDLE_DATA_COLUMNS_BY_RANGE_REQUEST, + SPAN_HANDLE_BLOCKS_BY_ROOT_REQUEST, + SPAN_HANDLE_BLOBS_BY_ROOT_REQUEST, + SPAN_HANDLE_DATA_COLUMNS_BY_ROOT_REQUEST, + SPAN_HANDLE_LIGHT_CLIENT_UPDATES_BY_RANGE, + SPAN_HANDLE_LIGHT_CLIENT_BOOTSTRAP, + SPAN_HANDLE_LIGHT_CLIENT_OPTIMISTIC_UPDATE, + SPAN_HANDLE_LIGHT_CLIENT_FINALITY_UPDATE, + SPAN_CUSTODY_BACKFILL_SYNC_BATCH_REQUEST, + SPAN_CUSTODY_BACKFILL_SYNC_IMPORT_COLUMNS, +]; diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 4e36953880..bf26196576 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -4,17 +4,12 @@ version = "0.2.0" authors = ["Sigma Prime "] edition = { workspace = true } -[dev-dependencies] -bls = { workspace = true } -eth2 = { workspace = true } -eth2_network_config = { workspace = true } -genesis = { workspace = true } -gossipsub = { workspace = true } -k256 = "0.13.4" -kzg = { workspace = true } -matches = "0.1.8" -rand_chacha = "0.3.1" -serde_json = { workspace = true } +[features] +# NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill +disable-backfill = [] +fork_from_env = ["beacon_chain/fork_from_env"] +portable = ["beacon_chain/portable"] +test_logger = [] [dependencies] alloy-primitives = { workspace = true } @@ -24,15 +19,17 @@ async-channel = { workspace = true } beacon_chain = { workspace = true } beacon_processor = { workspace = true } delay_map = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } +fixed_bytes = { workspace = true } fnv = { workspace = true } futures = { workspace = true } hex = { workspace = true } igd-next = { version = "0.16", features = ["aio_tokio"] } itertools = { workspace = true } lighthouse_network = { workspace = true } +lighthouse_tracing = { workspace = true } logging = { workspace = true } lru_cache = { workspace = true } metrics = { workspace = true } @@ -49,12 +46,19 @@ tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +typenum = { workspace = true } types = { workspace = true } -[features] -# NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill -disable-backfill = [] -fork_from_env = ["beacon_chain/fork_from_env"] -portable = ["beacon_chain/portable"] -test_logger = [] -ci_logger = [] +[dev-dependencies] +bls = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } +genesis = { workspace = true } +gossipsub = { workspace = true } +k256 = "0.13.4" +kzg = { workspace = true } +matches = "0.1.8" +rand_08 = { package = "rand", version = "0.8.5" } +rand_chacha = "0.9.0" +rand_chacha_03 = { package = "rand_chacha", version = "0.3.1" } +serde_json = { workspace = true } diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index b129b54841..cea06a28c8 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -1,14 +1,13 @@ use beacon_chain::{ - attestation_verification::Error as AttnError, + AvailabilityProcessingStatus, BlockError, attestation_verification::Error as AttnError, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, - sync_committee_verification::Error as SyncCommitteeError, AvailabilityProcessingStatus, - BlockError, + sync_committee_verification::Error as SyncCommitteeError, }; use fnv::FnvHashMap; use lighthouse_network::{ - peer_manager::peerdb::client::ClientKind, types::GossipKind, GossipTopic, Gossipsub, - NetworkGlobals, + GossipTopic, Gossipsub, NetworkGlobals, peer_manager::peerdb::client::ClientKind, + types::GossipKind, }; pub use metrics::*; use std::sync::{Arc, LazyLock}; @@ -17,9 +16,6 @@ use strum::IntoEnumIterator; use types::DataColumnSubnetId; use types::EthSpec; -pub const SUCCESS: &str = "SUCCESS"; -pub const FAILURE: &str = "FAILURE"; - #[derive(Debug, AsRefStr)] pub(crate) enum BlockSource { Gossip, @@ -120,17 +116,18 @@ pub static BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_processor_gossip_block_requeued_total", - "Total number of gossip blocks that arrived early and were re-queued for later processing." - ) + "beacon_processor_gossip_block_requeued_total", + "Total number of gossip blocks that arrived early and were re-queued for later processing.", + ) }); -pub static BEACON_PROCESSOR_GOSSIP_BLOCK_EARLY_SECONDS: LazyLock> = - LazyLock::new(|| { +pub static BEACON_PROCESSOR_GOSSIP_BLOCK_EARLY_SECONDS: LazyLock> = LazyLock::new( + || { try_create_histogram( - "beacon_processor_gossip_block_early_seconds", - "Whenever a gossip block is received early this metrics is set to how early that block was." - ) - }); + "beacon_processor_gossip_block_early_seconds", + "Whenever a gossip block is received early this metrics is set to how early that block was.", + ) + }, +); pub static BEACON_PROCESSOR_GOSSIP_BLOB_VERIFIED_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -215,6 +212,22 @@ pub static BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL: LazyLock, +> = LazyLock::new(|| { + try_create_int_counter( + "beacon_processor_custody_backfill_column_import_success_total", + "Total number of custody backfill sync columns successfully processed.", + ) +}); +pub static BEACON_PROCESSOR_CUSTODY_BACKFILL_BATCH_FAILED_TOTAL: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_processor_custody_backfill_batch_failed_total", + "Total number of custody backfill batches that failed to be processed.", + ) + }); // Chain segments. pub static BEACON_PROCESSOR_CHAIN_SEGMENT_SUCCESS_TOTAL: LazyLock> = LazyLock::new(|| { @@ -262,9 +275,9 @@ pub static BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_IMPORTED_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_processor_unaggregated_attestation_requeued_total", - "Total number of unaggregated attestations that referenced an unknown block and were re-queued." - ) + "beacon_processor_unaggregated_attestation_requeued_total", + "Total number of unaggregated attestations that referenced an unknown block and were re-queued.", + ) }); // Aggregated attestations. pub static BEACON_PROCESSOR_AGGREGATED_ATTESTATION_VERIFIED_TOTAL: LazyLock> = @@ -284,9 +297,9 @@ pub static BEACON_PROCESSOR_AGGREGATED_ATTESTATION_IMPORTED_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_processor_aggregated_attestation_requeued_total", - "Total number of aggregated attestations that referenced an unknown block and were re-queued." - ) + "beacon_processor_aggregated_attestation_requeued_total", + "Total number of aggregated attestations that referenced an unknown block and were re-queued.", + ) }); // Sync committee messages. pub static BEACON_PROCESSOR_SYNC_MESSAGE_VERIFIED_TOTAL: LazyLock> = @@ -507,9 +520,9 @@ pub static BEACON_BLOCK_DELAY_GOSSIP: LazyLock> = LazyLock::new pub static BEACON_BLOCK_DELAY_GOSSIP_VERIFICATION: LazyLock> = LazyLock::new( || { try_create_int_gauge( - "beacon_block_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the block" - ) + "beacon_block_delay_gossip_verification", + "Keeps track of the time delay from the start of the slot to the point we propagate the block", + ) }, ); pub static BEACON_BLOCK_DELAY_FULL_VERIFICATION: LazyLock> = LazyLock::new(|| { @@ -522,9 +535,9 @@ pub static BEACON_BLOCK_DELAY_FULL_VERIFICATION: LazyLock> = La pub static BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_block_delay_gossip_arrived_late_total", - "Count of times when a gossip block arrived from the network later than the attestation deadline.", - ) + "beacon_block_delay_gossip_arrived_late_total", + "Count of times when a gossip block arrived from the network later than the attestation deadline.", + ) }); /* @@ -544,28 +557,30 @@ pub static BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: LazyLo "beacon_data_column_gossip_propagation_verification_delay_time", "Duration between when the data column sidecar is received over gossip and when it is verified for propagation.", // [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5] - decimal_buckets(-3,-1) + decimal_buckets(-3, -1), ) }); pub static BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram_with_buckets( - "beacon_data_column_gossip_slot_start_delay_time", - "Duration between when the data column sidecar is received over gossip and the start of the slot it belongs to.", - // Create a custom bucket list for greater granularity in block delay - Ok(vec![0.1, 0.2, 0.3,0.4,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0,5.0,6.0,7.0,8.0,9.0,10.0,15.0,20.0]) - // NOTE: Previous values, which we may want to switch back to. - // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] - //decimal_buckets(-1,2) - ) + "beacon_data_column_gossip_slot_start_delay_time", + "Duration between when the data column sidecar is received over gossip and the start of the slot it belongs to.", + // Create a custom bucket list for greater granularity in block delay + Ok(vec![ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, + 6.0, 7.0, 8.0, 9.0, 10.0, 15.0, 20.0, + ]), // NOTE: Previous values, which we may want to switch back to. + // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] + //decimal_buckets(-1,2) + ) }); pub static BEACON_BLOB_DELAY_GOSSIP_VERIFICATION: LazyLock> = LazyLock::new( || { try_create_int_gauge( - "beacon_blob_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the blob" - ) + "beacon_blob_delay_gossip_verification", + "Keeps track of the time delay from the start of the slot to the point we propagate the blob", + ) }, ); pub static BEACON_BLOB_DELAY_FULL_VERIFICATION: LazyLock> = LazyLock::new(|| { @@ -578,24 +593,25 @@ pub static BEACON_BLOB_DELAY_FULL_VERIFICATION: LazyLock> = Laz pub static BEACON_BLOB_RPC_SLOT_START_DELAY_TIME: LazyLock> = LazyLock::new( || { try_create_histogram_with_buckets( - "beacon_blob_rpc_slot_start_delay_time", - "Duration between when a blob is received over rpc and the start of the slot it belongs to.", - // Create a custom bucket list for greater granularity in block delay - Ok(vec![0.1, 0.2, 0.3,0.4,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0,5.0,6.0,7.0,8.0,9.0,10.0,15.0,20.0]) - // NOTE: Previous values, which we may want to switch back to. - // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] - //decimal_buckets(-1,2) - - ) + "beacon_blob_rpc_slot_start_delay_time", + "Duration between when a blob is received over rpc and the start of the slot it belongs to.", + // Create a custom bucket list for greater granularity in block delay + Ok(vec![ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, + 6.0, 7.0, 8.0, 9.0, 10.0, 15.0, 20.0, + ]), // NOTE: Previous values, which we may want to switch back to. + // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] + //decimal_buckets(-1,2) + ) }, ); pub static BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new( || { try_create_int_counter( - "beacon_blob_gossip_arrived_late_total", - "Count of times when a gossip blob arrived from the network later than the attestation deadline.", - ) + "beacon_blob_gossip_arrived_late_total", + "Count of times when a gossip blob arrived from the network later than the attestation deadline.", + ) }, ); @@ -607,32 +623,7 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_SENT_OPTIMISTIC_UPDATES: LazyLock > = LazyLock::new(|| { try_create_int_counter( "beacon_processor_reprocessing_queue_sent_optimistic_updates", - "Number of queued light client optimistic updates where as matching block has been imported." - ) -}); - -/* - * Sampling - */ -pub static SAMPLE_DOWNLOAD_RESULT: LazyLock> = LazyLock::new(|| { - try_create_int_counter_vec( - "beacon_sampling_sample_verify_result_total", - "Total count of individual sample download results", - &["result"], - ) -}); -pub static SAMPLE_VERIFY_RESULT: LazyLock> = LazyLock::new(|| { - try_create_int_counter_vec( - "beacon_sampling_sample_verify_result_total", - "Total count of individual sample verify results", - &["result"], - ) -}); -pub static SAMPLING_REQUEST_RESULT: LazyLock> = LazyLock::new(|| { - try_create_int_counter_vec( - "beacon_sampling_request_result_total", - "Total count of sample request results", - &["result"], + "Number of queued light client optimistic updates where as matching block has been imported.", ) }); @@ -683,13 +674,6 @@ pub(crate) fn register_process_result_metrics( } } -pub fn from_result(result: &std::result::Result) -> &str { - match result { - Ok(_) => SUCCESS, - Err(_) => FAILURE, - } -} - pub fn update_gossip_metrics( gossipsub: &Gossipsub, network_globals: &Arc>, @@ -780,7 +764,7 @@ pub fn update_sync_metrics(network_globals: &Arc>) let all_column_subnets = (0..network_globals.spec.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new); - let custody_column_subnets = network_globals.sampling_subnets.iter(); + let custody_column_subnets = network_globals.sampling_subnets(); // Iterate all subnet values to set to zero the empty entries in peers_per_column_subnet for subnet in all_column_subnets { @@ -794,7 +778,7 @@ pub fn update_sync_metrics(network_globals: &Arc>) // Registering this metric is a duplicate for supernodes but helpful for fullnodes. This way // operators can monitor the health of only the subnets of their interest without complex // Grafana queries. - for subnet in custody_column_subnets { + for subnet in custody_column_subnets.iter() { set_gauge_entry( &PEERS_PER_CUSTODY_COLUMN_SUBNET, &[&format!("{subnet}")], diff --git a/beacon_node/network/src/nat.rs b/beacon_node/network/src/nat.rs index ce9d241d43..f1c768e67b 100644 --- a/beacon_node/network/src/nat.rs +++ b/beacon_node/network/src/nat.rs @@ -3,8 +3,8 @@ //! Currently supported strategies: //! - UPnP -use anyhow::{bail, Context, Error}; -use igd_next::{aio::tokio as igd, PortMappingProtocol}; +use anyhow::{Context, Error, bail}; +use igd_next::{PortMappingProtocol, aio::tokio as igd}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; use tokio::time::sleep; diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 89384bc39e..51940ce68a 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -12,18 +12,21 @@ use beacon_chain::inclusion_list_verification::{ }; use beacon_chain::store::Error; use beacon_chain::{ + AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, + GossipVerifiedBlock, NotifyExecutionLayer, attestation_verification::{self, Error as AttnError, VerifiedAttestation}, data_availability_checker::AvailabilityCheckErrorCategory, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, observed_operations::ObservationOutcome, - single_attestation::single_attestation_to_attestation, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, - AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, - GossipVerifiedBlock, NotifyExecutionLayer, }; +use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; +use lighthouse_tracing::{ + SPAN_PROCESS_GOSSIP_BLOB, SPAN_PROCESS_GOSSIP_BLOCK, SPAN_PROCESS_GOSSIP_DATA_COLUMN, +}; use logging::crit; use operation_pool::ReceivedPreCapella; use slot_clock::SlotClock; @@ -34,23 +37,22 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; -use tokio::sync::mpsc; -use tracing::{debug, error, info, trace, warn}; +use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; use types::{ - beacon_block::BlockImportSource, Attestation, AttestationData, AttestationRef, - AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, - IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedInclusionList, SignedVoluntaryExit, SingleAttestation, Slot, - SubnetId, SyncCommitteeMessage, SyncSubnetId, + Attestation, AttestationData, AttestationRef, AttesterSlashing, BlobSidecar, DataColumnSidecar, + DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, LightClientFinalityUpdate, + LightClientOptimisticUpdate, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SingleAttestation, + Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, beacon_block::BlockImportSource, SignedInclusionList }; +use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; use beacon_processor::{ + DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, work_reprocessing_queue::{ QueuedAggregate, QueuedGossipBlock, QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, - DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, }; /// Set to `true` to introduce stricter penalties for peers who send some types of late consensus @@ -69,7 +71,7 @@ struct VerifiedUnaggregate { /// This implementation allows `Self` to be imported to fork choice and other functions on the /// `BeaconChain`. impl VerifiedAttestation for VerifiedUnaggregate { - fn attestation(&self) -> AttestationRef { + fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.attestation.to_ref() } @@ -85,8 +87,8 @@ impl VerifiedAttestation for VerifiedUnaggregate { } /// An attestation that failed validation by the `BeaconChain`. -struct RejectedUnaggregate { - attestation: Box>, +struct RejectedUnaggregate { + attestation: Box, error: AttnError, } @@ -102,7 +104,7 @@ struct VerifiedAggregate { /// This implementation allows `Self` to be imported to fork choice and other functions on the /// `BeaconChain`. impl VerifiedAttestation for VerifiedAggregate { - fn attestation(&self) -> AttestationRef { + fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.signed_aggregate.message().aggregate() } @@ -127,16 +129,11 @@ struct RejectedAggregate { /// Data for an aggregated or unaggregated attestation that failed verification. enum FailedAtt { Unaggregate { - attestation: Box>, + attestation: Box, subnet_id: SubnetId, should_import: bool, seen_timestamp: Duration, }, - // This variant is just a dummy variant for now, as SingleAttestation reprocessing is handled - // separately. - SingleUnaggregate { - attestation: Box, - }, Aggregate { attestation: Box>, seen_timestamp: Duration, @@ -151,15 +148,13 @@ impl FailedAtt { pub fn kind(&self) -> &'static str { match self { FailedAtt::Unaggregate { .. } => "unaggregated", - FailedAtt::SingleUnaggregate { .. } => "unaggregated", FailedAtt::Aggregate { .. } => "aggregated", } } pub fn attestation_data(&self) -> &AttestationData { match self { - FailedAtt::Unaggregate { attestation, .. } => attestation.data(), - FailedAtt::SingleUnaggregate { attestation, .. } => &attestation.data, + FailedAtt::Unaggregate { attestation, .. } => &attestation.data, FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate().data(), } } @@ -211,20 +206,24 @@ impl NetworkBeaconProcessor { self: Arc, message_id: MessageId, peer_id: PeerId, - attestation: Box>, + attestation: Box, subnet_id: SubnetId, should_import: bool, - reprocess_tx: Option>, + allow_reprocess: bool, seen_timestamp: Duration, ) { let result = match self .chain .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)) { - Ok(verified_attestation) => Ok(VerifiedUnaggregate { - indexed_attestation: verified_attestation.into_indexed_attestation(), - attestation, - }), + Ok(verified_attestation) => { + let attestation = + Box::new(verified_attestation.attestation().clone_as_attestation()); + Ok(VerifiedUnaggregate { + indexed_attestation: verified_attestation.into_indexed_attestation(), + attestation, + }) + } Err(error) => Err(RejectedUnaggregate { attestation, error }), }; @@ -233,7 +232,7 @@ impl NetworkBeaconProcessor { message_id, peer_id, subnet_id, - reprocess_tx, + allow_reprocess, should_import, seen_timestamp, ); @@ -241,8 +240,8 @@ impl NetworkBeaconProcessor { pub fn process_gossip_attestation_batch( self: Arc, - packages: GossipAttestationBatch, - reprocess_tx: Option>, + packages: GossipAttestationBatch, + allow_reprocess: bool, ) { let attestations_and_subnets = packages .iter() @@ -278,14 +277,19 @@ impl NetworkBeaconProcessor { #[allow(clippy::needless_collect)] // The clippy suggestion fails the borrow checker. let results = results .into_iter() - .map(|result| result.map(|verified| verified.into_indexed_attestation())) + .map(|result| { + result.map(|verified| { + let attestation = verified.attestation().clone_as_attestation(); + (verified.into_indexed_attestation(), attestation) + }) + }) .collect::>(); for (result, package) in results.into_iter().zip(packages.into_iter()) { let result = match result { - Ok(indexed_attestation) => Ok(VerifiedUnaggregate { + Ok((indexed_attestation, attestation)) => Ok(VerifiedUnaggregate { indexed_attestation, - attestation: package.attestation, + attestation: Box::new(attestation), }), Err(error) => Err(RejectedUnaggregate { attestation: package.attestation, @@ -298,7 +302,7 @@ impl NetworkBeaconProcessor { package.message_id, package.peer_id, package.subnet_id, - reprocess_tx.clone(), + allow_reprocess, package.should_import, package.seen_timestamp, ); @@ -310,11 +314,11 @@ impl NetworkBeaconProcessor { #[allow(clippy::too_many_arguments)] fn process_gossip_attestation_result( self: &Arc, - result: Result, RejectedUnaggregate>, + result: Result, RejectedUnaggregate>, message_id: MessageId, peer_id: PeerId, subnet_id: SubnetId, - reprocess_tx: Option>, + allow_reprocess: bool, should_import: bool, seen_timestamp: Duration, ) { @@ -398,7 +402,7 @@ impl NetworkBeaconProcessor { should_import, seen_timestamp, }, - reprocess_tx, + allow_reprocess, error, seen_timestamp, ); @@ -406,147 +410,6 @@ impl NetworkBeaconProcessor { } } - /// Process an unaggregated attestation requiring conversion. - /// - /// This function performs the conversion, and if successfull queues a new message to be - /// processed by `process_gossip_attestation`. If unsuccessful due to block unavailability, - /// a retry message will be pushed to the `reprocess_tx` if it is `Some`. - #[allow(clippy::too_many_arguments)] - pub fn process_gossip_attestation_to_convert( - self: Arc, - message_id: MessageId, - peer_id: PeerId, - single_attestation: Box, - subnet_id: SubnetId, - should_import: bool, - reprocess_tx: Option>, - seen_timestamp: Duration, - ) { - let conversion_result = self.chain.with_committee_cache( - single_attestation.data.target.root, - single_attestation - .data - .slot - .epoch(T::EthSpec::slots_per_epoch()), - |committee_cache, _| { - let slot = single_attestation.data.slot; - let committee_index = single_attestation.committee_index; - let Some(committee) = committee_cache.get_beacon_committee(slot, committee_index) - else { - return Ok(Err(AttnError::NoCommitteeForSlotAndIndex { - slot, - index: committee_index, - })); - }; - - Ok(single_attestation_to_attestation( - &single_attestation, - committee.committee, - )) - }, - ); - - match conversion_result { - Ok(Ok(attestation)) => { - let slot = attestation.data().slot; - if let Err(e) = self.send_unaggregated_attestation( - message_id.clone(), - peer_id, - attestation, - subnet_id, - should_import, - seen_timestamp, - ) { - error!( - error = %e, - %slot, - "Unable to queue converted SingleAttestation" - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - } - // Outermost error (from `with_committee_cache`) indicating that the block is not known - // and that this conversion should be retried. - Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => { - if let Some(sender) = reprocess_tx { - metrics::inc_counter( - &metrics::BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_REQUEUED_TOTAL, - ); - // We don't know the block, get the sync manager to handle the block lookup, and - // send the attestation to be scheduled for re-processing. - self.sync_tx - .send(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, - beacon_block_root, - )) - .unwrap_or_else(|_| { - warn!(msg = "UnknownBlockHash", "Failed to send to sync service") - }); - let processor = self.clone(); - // Do not allow this attestation to be re-processed beyond this point. - let reprocess_msg = - ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate { - beacon_block_root, - process_fn: Box::new(move || { - processor.process_gossip_attestation_to_convert( - message_id, - peer_id, - single_attestation, - subnet_id, - should_import, - None, - seen_timestamp, - ) - }), - }); - if sender.try_send(reprocess_msg).is_err() { - error!("Failed to send attestation for re-processing") - } - } else { - // We shouldn't make any further attempts to process this attestation. - // - // Don't downscore the peer since it's not clear if we requested this head - // block from them or not. - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - } - Ok(Err(error)) => { - // We already handled reprocessing above so do not attempt it in the error handler. - self.handle_attestation_verification_failure( - peer_id, - message_id, - FailedAtt::SingleUnaggregate { - attestation: single_attestation, - }, - None, - error, - seen_timestamp, - ); - } - Err(error) => { - // We already handled reprocessing above so do not attempt it in the error handler. - self.handle_attestation_verification_failure( - peer_id, - message_id, - FailedAtt::SingleUnaggregate { - attestation: single_attestation, - }, - None, - AttnError::BeaconChainError(Box::new(error)), - seen_timestamp, - ); - } - } - } - /// Process the aggregated attestation received from the gossip network and: /// /// - If it passes gossip propagation criteria, tell the network thread to forward it. @@ -559,7 +422,7 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, aggregate: Box>, - reprocess_tx: Option>, + allow_reprocess: bool, seen_timestamp: Duration, ) { let beacon_block_root = aggregate.message().aggregate().data().beacon_block_root; @@ -583,7 +446,7 @@ impl NetworkBeaconProcessor { beacon_block_root, message_id, peer_id, - reprocess_tx, + allow_reprocess, seen_timestamp, ); } @@ -591,7 +454,7 @@ impl NetworkBeaconProcessor { pub fn process_gossip_aggregate_batch( self: Arc, packages: Vec>, - reprocess_tx: Option>, + allow_reprocess: bool, ) { let aggregates = packages.iter().map(|package| package.aggregate.as_ref()); @@ -645,7 +508,7 @@ impl NetworkBeaconProcessor { package.beacon_block_root, package.message_id, package.peer_id, - reprocess_tx.clone(), + allow_reprocess, package.seen_timestamp, ); } @@ -657,7 +520,7 @@ impl NetworkBeaconProcessor { beacon_block_root: Hash256, message_id: MessageId, peer_id: PeerId, - reprocess_tx: Option>, + allow_reprocess: bool, seen_timestamp: Duration, ) { match result { @@ -736,7 +599,7 @@ impl NetworkBeaconProcessor { attestation: signed_aggregate, seen_timestamp, }, - reprocess_tx, + allow_reprocess, error, seen_timestamp, ); @@ -744,11 +607,17 @@ impl NetworkBeaconProcessor { } } + #[instrument( + name = SPAN_PROCESS_GOSSIP_DATA_COLUMN, + parent = None, + level = "debug", + skip_all, + fields(slot = %column_sidecar.slot(), block_root = ?column_sidecar.block_root(), index = column_sidecar.index), + )] pub async fn process_gossip_data_column_sidecar( self: &Arc, message_id: MessageId, peer_id: PeerId, - _peer_client: Client, subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_duration: Duration, @@ -764,7 +633,7 @@ impl NetworkBeaconProcessor { ); match self .chain - .verify_data_column_sidecar_for_gossip(column_sidecar.clone(), *subnet_id) + .verify_data_column_sidecar_for_gossip(column_sidecar.clone(), subnet_id) { Ok(gossip_verified_data_column) => { metrics::inc_counter( @@ -791,6 +660,7 @@ impl NetworkBeaconProcessor { duration, ); } + self.process_gossip_verified_data_column( peer_id, gossip_verified_data_column, @@ -800,6 +670,19 @@ impl NetworkBeaconProcessor { } Err(err) => { match err { + GossipDataColumnError::PriorKnownUnpublished => { + debug!( + %slot, + %block_root, + %index, + "Gossip data column already processed via the EL. Accepting the column sidecar without re-processing." + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Accept, + ); + } GossipDataColumnError::ParentUnknown { parent_root } => { debug!( action = "requesting parent", @@ -828,6 +711,7 @@ impl NetworkBeaconProcessor { | GossipDataColumnError::InvalidKzgProof { .. } | GossipDataColumnError::UnexpectedDataColumn | GossipDataColumnError::InvalidColumnIndex(_) + | GossipDataColumnError::MaxBlobsPerBlockExceeded { .. } | GossipDataColumnError::InconsistentCommitmentsLength { .. } | GossipDataColumnError::InconsistentProofsLength { .. } | GossipDataColumnError::NotFinalizedDescendant { .. } => { @@ -854,12 +738,11 @@ impl NetworkBeaconProcessor { // Data column is available via either the EL or reconstruction. // Do not penalise the peer. // Gossip filter should filter any duplicates received after this. - debug!( - %slot, - %block_root, - %index, - "Received already available column sidecar. Ignoring the column sidecar" - ) + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); } GossipDataColumnError::FutureSlot { .. } | GossipDataColumnError::PastFinalizedSlot { .. } => { @@ -888,6 +771,16 @@ impl NetworkBeaconProcessor { } #[allow(clippy::too_many_arguments)] + #[instrument( + name = SPAN_PROCESS_GOSSIP_BLOB, + parent = None, + level = "debug", + skip_all, + fields( + slot = ?blob_sidecar.slot(), + block_root = ?blob_sidecar.block_root(), + index = blob_sidecar.index), + )] pub async fn process_gossip_blob( self: &Arc, message_id: MessageId, @@ -949,7 +842,7 @@ impl NetworkBeaconProcessor { } Err(err) => { match err { - GossipBlobError::BlobParentUnknown { parent_root } => { + GossipBlobError::ParentUnknown { parent_root } => { debug!( action = "requesting parent", block_root = %root, @@ -1055,7 +948,7 @@ impl NetworkBeaconProcessor { } } - pub async fn process_gossip_verified_blob( + async fn process_gossip_verified_blob( self: &Arc, peer_id: PeerId, verified_blob: GossipVerifiedBlob, @@ -1071,7 +964,7 @@ impl NetworkBeaconProcessor { match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - info!( + debug!( %block_root, "Gossipsub blob processed - imported fully available block" ); @@ -1123,7 +1016,7 @@ impl NetworkBeaconProcessor { } } - pub async fn process_gossip_verified_data_column( + async fn process_gossip_verified_data_column( self: &Arc, peer_id: PeerId, verified_data_column: GossipVerifiedDataColumn, @@ -1141,10 +1034,10 @@ impl NetworkBeaconProcessor { .await; register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - match result { + match &result { Ok(availability) => match availability { AvailabilityProcessingStatus::Imported(block_root) => { - info!( + debug!( %block_root, "Gossipsub data column processed, imported fully available block" ); @@ -1163,8 +1056,44 @@ impl NetworkBeaconProcessor { "Processed data column, waiting for other components" ); - self.attempt_data_column_reconstruction(block_root, true) - .await; + if self + .chain + .data_availability_checker + .custody_context() + .should_attempt_reconstruction( + slot.epoch(T::EthSpec::slots_per_epoch()), + &self.chain.spec, + ) + { + // Instead of triggering reconstruction immediately, schedule it to be run. If + // another column arrives, it either completes availability or pushes + // reconstruction back a bit. + let cloned_self = Arc::clone(self); + let block_root = *block_root; + + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess( + ReprocessQueueMessage::DelayColumnReconstruction( + QueuedColumnReconstruction { + block_root, + slot: *slot, + process_fn: Box::pin(async move { + cloned_self + .attempt_data_column_reconstruction(block_root) + .await; + }), + }, + ), + ), + }) + .is_err() + { + warn!("Unable to send reconstruction to reprocessing"); + } + } } }, Err(BlockError::DuplicateFullyImported(_)) => { @@ -1188,6 +1117,16 @@ impl NetworkBeaconProcessor { ); } } + + // If a block is in the da_checker, sync maybe awaiting for an event when block is finally + // imported. A block can become imported both after processing a block or data column. If a + // importing a block results in `Imported`, notify. Do not notify of data column errors. + if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: true, + }); + } } /// Process the beacon block received from the gossip network and: @@ -1198,13 +1137,19 @@ impl NetworkBeaconProcessor { /// /// Raises a log if there are errors. #[allow(clippy::too_many_arguments)] + #[instrument( + name = SPAN_PROCESS_GOSSIP_BLOCK, + parent = None, + level = "debug", + skip_all, + fields(block_root = tracing::field::Empty), + )] pub async fn process_gossip_block( self: Arc, message_id: MessageId, peer_id: PeerId, peer_client: Client, block: Arc>, - reprocess_tx: mpsc::Sender, duplicate_cache: DuplicateCache, invalid_block_storage: InvalidBlockStorage, seen_duration: Duration, @@ -1215,18 +1160,17 @@ impl NetworkBeaconProcessor { peer_id, peer_client, block.clone(), - reprocess_tx.clone(), seen_duration, ) .await { let block_root = gossip_verified_block.block_root; + Span::current().record("block_root", block_root.to_string()); if let Some(handle) = duplicate_cache.check_and_insert(block_root) { self.process_gossip_verified_block( peer_id, gossip_verified_block, - reprocess_tx, invalid_block_storage, seen_duration, ) @@ -1246,13 +1190,12 @@ impl NetworkBeaconProcessor { /// if it passes gossip propagation criteria, tell the network thread to forward it. /// /// Returns the `GossipVerifiedBlock` if verification passes and raises a log if there are errors. - pub async fn process_gossip_unverified_block( + async fn process_gossip_unverified_block( self: &Arc, message_id: MessageId, peer_id: PeerId, peer_client: Client, block: Arc>, - reprocess_tx: mpsc::Sender, seen_duration: Duration, ) -> Option> { let block_delay = @@ -1262,10 +1205,7 @@ impl NetworkBeaconProcessor { let verification_result = self .chain .clone() - .verify_block_for_gossip( - block.clone(), - self.network_globals.custody_columns_count() as usize, - ) + .verify_block_for_gossip(block.clone()) .await; if verification_result.is_ok() { @@ -1412,7 +1352,8 @@ impl NetworkBeaconProcessor { | Err(e @ BlockError::ExecutionPayloadError(_)) | Err(e @ BlockError::ParentExecutionPayloadInvalid { .. }) | Err(e @ BlockError::KnownInvalidExecutionPayload(_)) - | Err(e @ BlockError::GenesisBlock) => { + | Err(e @ BlockError::GenesisBlock) + | Err(e @ BlockError::InvalidBlobCount { .. }) => { warn!(error = %e, "Could not verify block for gossip. Rejecting the block"); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( @@ -1482,24 +1423,28 @@ impl NetworkBeaconProcessor { let inner_self = self.clone(); let process_fn = Box::pin(async move { - let reprocess_tx = inner_self.reprocess_tx.clone(); let invalid_block_storage = inner_self.invalid_block_storage.clone(); inner_self .process_gossip_verified_block( peer_id, verified_block, - reprocess_tx, invalid_block_storage, seen_duration, ) .await; }); - if reprocess_tx - .try_send(ReprocessQueueMessage::EarlyBlock(QueuedGossipBlock { - beacon_block_slot: block_slot, - beacon_block_root: block_root, - process_fn, - })) + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(ReprocessQueueMessage::EarlyBlock( + QueuedGossipBlock { + beacon_block_slot: block_slot, + beacon_block_root: block_root, + process_fn, + }, + )), + }) .is_err() { error!( @@ -1528,11 +1473,11 @@ impl NetworkBeaconProcessor { /// Process the beacon block that has already passed gossip verification. /// /// Raises a log if there are errors. - pub async fn process_gossip_verified_block( + #[instrument(skip_all)] + async fn process_gossip_verified_block( self: Arc, peer_id: PeerId, verified_block: GossipVerifiedBlock, - reprocess_tx: mpsc::Sender, invalid_block_storage: InvalidBlockStorage, _seen_duration: Duration, ) { @@ -1540,52 +1485,44 @@ impl NetworkBeaconProcessor { let block = verified_block.block.block_cloned(); let block_root = verified_block.block_root; - // Note: okay to issue sampling request before the block is execution verified. If the - // proposer sends us a block with invalid blob transactions it can trigger us to issue - // sampling queries that will never resolve. This attack is equivalent to withholding data. - // Dismissed proposal to move this block to post-execution: https://github.com/sigp/lighthouse/pull/6492 - if block.num_expected_blobs() > 0 { - // Trigger sampling for block not yet execution valid. At this point column custodials are - // unlikely to have received their columns. Triggering sampling so early is only viable with - // either: - // - Sync delaying sampling until some latter window - // - Re-processing early sampling requests: https://github.com/sigp/lighthouse/pull/5569 - if self.chain.should_sample_slot(block.slot()) { - self.send_sync_message(SyncMessage::SampleBlock(block_root, block.slot())); - } - } - // Block is gossip valid. Attempt to fetch blobs from the EL using versioned hashes derived // from kzg commitments, without having to wait for all blobs to be sent from the peers. let publish_blobs = true; let self_clone = self.clone(); let block_clone = block.clone(); + let current_span = Span::current(); self.executor.spawn( async move { self_clone .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) .await - }, + } + .instrument(current_span), "fetch_blobs_gossip", ); let result = self .chain - .process_block_with_early_caching( + .process_block( block_root, verified_block, - BlockImportSource::Gossip, NotifyExecutionLayer::Yes, + BlockImportSource::Gossip, + || Ok(()), ) .await; register_process_result_metrics(&result, metrics::BlockSource::Gossip, "block"); match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - if reprocess_tx - .try_send(ReprocessQueueMessage::BlockImported { - block_root: *block_root, - parent_root: block.message().parent_root(), + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(ReprocessQueueMessage::BlockImported { + block_root: *block_root, + parent_root: block.message().parent_root(), + }), }) .is_err() { @@ -1628,7 +1565,7 @@ impl NetworkBeaconProcessor { "Block with unknown parent attempted to be processed" ); } - Err(ref e @ BlockError::ExecutionPayloadError(ref epe)) if !epe.penalize_peer() => { + Err(e @ BlockError::ExecutionPayloadError(epe)) if !epe.penalize_peer() => { debug!( error = %e, "Failed to verify execution payload" @@ -2067,7 +2004,10 @@ impl NetworkBeaconProcessor { Err(e) => { metrics::register_finality_update_error(&e); match e { - LightClientFinalityUpdateError::InvalidLightClientFinalityUpdate => { + LightClientFinalityUpdateError::MismatchedSignatureSlot { .. } + | LightClientFinalityUpdateError::MismatchedAttestedHeader { .. } + | LightClientFinalityUpdateError::MismatchedFinalizedHeader { .. } + | LightClientFinalityUpdateError::MismatchedProofOrSyncAggregate { .. } => { debug!( %peer_id, error = ?e, @@ -2099,6 +2039,7 @@ impl NetworkBeaconProcessor { error = ?e, "Light client error constructing finality update" ), + LightClientFinalityUpdateError::Ignore => {} } self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } @@ -2110,7 +2051,7 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, light_client_optimistic_update: LightClientOptimisticUpdate, - reprocess_tx: Option>, + allow_reprocess: bool, seen_timestamp: Duration, ) { match self.chain.verify_optimistic_update_for_gossip( @@ -2138,7 +2079,7 @@ impl NetworkBeaconProcessor { "Optimistic update for unknown block" ); - if let Some(sender) = reprocess_tx { + if allow_reprocess { let processor = self.clone(); let msg = ReprocessQueueMessage::UnknownLightClientOptimisticUpdate( QueuedLightClientUpdate { @@ -2148,14 +2089,21 @@ impl NetworkBeaconProcessor { message_id, peer_id, light_client_optimistic_update, - None, // Do not reprocess this message again. + false, // Do not reprocess this message again. seen_timestamp, ) }), }, ); - if sender.try_send(msg).is_err() { + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: true, + work: Work::Reprocess(msg), + }) + .is_err() + { error!("Failed to send optimistic update for re-processing") } } else { @@ -2173,7 +2121,9 @@ impl NetworkBeaconProcessor { } return; } - LightClientOptimisticUpdateError::InvalidLightClientOptimisticUpdate => { + LightClientOptimisticUpdateError::MismatchedSignatureSlot { .. } + | LightClientOptimisticUpdateError::MismatchedAttestedHeader { .. } + | LightClientOptimisticUpdateError::MismatchedSyncAggregate { .. } => { metrics::register_optimistic_update_error(&e); debug!( @@ -2212,6 +2162,7 @@ impl NetworkBeaconProcessor { "Light client error constructing optimistic update" ) } + LightClientOptimisticUpdateError::Ignore => {} } self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } @@ -2268,7 +2219,7 @@ impl NetworkBeaconProcessor { peer_id: PeerId, message_id: MessageId, failed_att: FailedAtt, - reprocess_tx: Option>, + allow_reprocess: bool, error: AttnError, seen_timestamp: Duration, ) { @@ -2508,7 +2459,7 @@ impl NetworkBeaconProcessor { block = ?beacon_block_root, "Attestation for unknown block" ); - if let Some(sender) = reprocess_tx { + if allow_reprocess { // We don't know the block, get the sync manager to handle the block lookup, and // send the attestation to be scheduled for re-processing. self.sync_tx @@ -2535,22 +2486,12 @@ impl NetworkBeaconProcessor { message_id, peer_id, attestation, - None, // Do not allow this attestation to be re-processed beyond this point. + false, // Do not allow this attestation to be re-processed beyond this point. seen_timestamp, ) }), }) } - FailedAtt::SingleUnaggregate { .. } => { - // This should never happen, as we handle the unknown head block case - // for `SingleAttestation`s separately and should not be able to hit - // an `UnknownHeadBlock` error. - error!( - block_root = ?beacon_block_root, - "Dropping SingleAttestation instead of requeueing" - ); - return; - } FailedAtt::Unaggregate { attestation, subnet_id, @@ -2570,7 +2511,7 @@ impl NetworkBeaconProcessor { attestation, subnet_id, should_import, - None, // Do not allow this attestation to be re-processed beyond this point. + false, // Do not allow this attestation to be re-processed beyond this point. seen_timestamp, ) }), @@ -2578,7 +2519,14 @@ impl NetworkBeaconProcessor { } }; - if sender.try_send(msg).is_err() { + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(msg), + }) + .is_err() + { error!("Failed to send attestation for re-processing") } } else { @@ -2646,19 +2594,6 @@ impl NetworkBeaconProcessor { "attn_no_committee", ); } - AttnError::NotExactlyOneAggregationBitSet(_) => { - /* - * The unaggregated attestation doesn't have only one signature. - * - * The peer has published an invalid consensus message. - */ - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "attn_too_many_agg_bits", - ); - } AttnError::NotExactlyOneCommitteeBitSet(_) => { /* * The attestation doesn't have only one committee bit set. @@ -2808,6 +2743,26 @@ impl NetworkBeaconProcessor { MessageAcceptance::Ignore, ); } + BeaconChainError::AttestationValidationError(e) => { + // Failures from `get_attesting_indices` end up here. + debug!( + %peer_id, + block_root = ?beacon_block_root, + attestation_slot = %failed_att.attestation_data().slot, + error = ?e, + "Rejecting attestation that failed validation" + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Reject, + ); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "attn_validation_error", + ); + } _ => { /* * Lighthouse hit an unexpected error whilst processing the attestation. It @@ -2832,6 +2787,20 @@ impl NetworkBeaconProcessor { } } } + AttnError::SszTypesError(_) => { + error!( + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Rejecting attestation due to a critical SSZ types error" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "attn_ssz_types_error", + ); + } } debug!( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index a898420f37..127ff10a7b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,28 +1,25 @@ use crate::sync::manager::BlockProcessType; -use crate::sync::SamplingId; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use beacon_chain::blob_verification::{GossipBlobError, observe_gossip_blob}; use beacon_chain::block_verification_types::RpcBlock; -use beacon_chain::data_column_verification::{observe_gossip_data_column, GossipDataColumnError}; +use beacon_chain::data_column_verification::{GossipDataColumnError, observe_gossip_data_column}; use beacon_chain::fetch_blobs::{ - fetch_and_process_engine_blobs, BlobsOrDataColumns, FetchEngineBlobError, -}; -use beacon_chain::observed_data_sidecars::DoNotObserve; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, + EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, }; +use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ - work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend, DuplicateCache, - GossipAggregatePackage, GossipAttestationPackage, Work, WorkEvent as BeaconWorkEvent, + BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, + WorkEvent as BeaconWorkEvent, }; +use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, LightClientUpdatesByRangeRequest, }; -use lighthouse_network::rpc::InboundRequestId; +use lighthouse_network::service::api_types::CustodyBackfillBatchId; use lighthouse_network::{ - rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, Client, MessageId, NetworkGlobals, PeerId, PubsubMessage, + rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, }; use rand::prelude::SliceRandom; use std::path::PathBuf; @@ -30,7 +27,7 @@ use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, error::TrySendError}; -use tracing::{debug, error, trace, warn, Instrument}; +use tracing::{debug, error, instrument, trace, warn}; use types::*; pub use sync_methods::ChainSegmentProcessId; @@ -61,7 +58,6 @@ pub struct NetworkBeaconProcessor { pub chain: Arc>, pub network_tx: mpsc::UnboundedSender>, pub sync_tx: mpsc::UnboundedSender>, - pub reprocess_tx: mpsc::Sender, pub network_globals: Arc>, pub invalid_block_storage: InvalidBlockStorage, pub executor: TaskExecutor, @@ -75,78 +71,34 @@ impl NetworkBeaconProcessor { self.beacon_processor_send.try_send(event) } - /// Create a new `Work` event for some `SingleAttestation`. - pub fn send_single_attestation( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - single_attestation: SingleAttestation, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, - ) -> Result<(), Error> { - let processor = self.clone(); - let process_individual = move |package: GossipAttestationPackage| { - let reprocess_tx = processor.reprocess_tx.clone(); - processor.process_gossip_attestation_to_convert( - package.message_id, - package.peer_id, - package.attestation, - package.subnet_id, - package.should_import, - Some(reprocess_tx), - package.seen_timestamp, - ) - }; - - self.try_send(BeaconWorkEvent { - drop_during_sync: true, - work: Work::GossipAttestationToConvert { - attestation: Box::new(GossipAttestationPackage { - message_id, - peer_id, - attestation: Box::new(single_attestation), - subnet_id, - should_import, - seen_timestamp, - }), - process_individual: Box::new(process_individual), - }, - }) - } - /// Create a new `Work` event for some unaggregated attestation. pub fn send_unaggregated_attestation( self: &Arc, message_id: MessageId, peer_id: PeerId, - attestation: Attestation, + attestation: SingleAttestation, subnet_id: SubnetId, should_import: bool, seen_timestamp: Duration, ) -> Result<(), Error> { // Define a closure for processing individual attestations. let processor = self.clone(); - let process_individual = - move |package: GossipAttestationPackage>| { - let reprocess_tx = processor.reprocess_tx.clone(); - processor.process_gossip_attestation( - package.message_id, - package.peer_id, - package.attestation, - package.subnet_id, - package.should_import, - Some(reprocess_tx), - package.seen_timestamp, - ) - }; + let process_individual = move |package: GossipAttestationPackage| { + processor.process_gossip_attestation( + package.message_id, + package.peer_id, + package.attestation, + package.subnet_id, + package.should_import, + true, + package.seen_timestamp, + ) + }; // Define a closure for processing batches of attestations. let processor = self.clone(); - let process_batch = move |attestations| { - let reprocess_tx = processor.reprocess_tx.clone(); - processor.process_gossip_attestation_batch(attestations, Some(reprocess_tx)) - }; + let process_batch = + move |attestations| processor.process_gossip_attestation_batch(attestations, true); self.try_send(BeaconWorkEvent { drop_during_sync: true, @@ -176,22 +128,19 @@ impl NetworkBeaconProcessor { // Define a closure for processing individual attestations. let processor = self.clone(); let process_individual = move |package: GossipAggregatePackage| { - let reprocess_tx = processor.reprocess_tx.clone(); processor.process_gossip_aggregate( package.message_id, package.peer_id, package.aggregate, - Some(reprocess_tx), + true, package.seen_timestamp, ) }; // Define a closure for processing batches of attestations. let processor = self.clone(); - let process_batch = move |aggregates| { - let reprocess_tx = processor.reprocess_tx.clone(); - processor.process_gossip_aggregate_batch(aggregates, Some(reprocess_tx)) - }; + let process_batch = + move |aggregates| processor.process_gossip_aggregate_batch(aggregates, true); let beacon_block_root = aggregate.message().aggregate().data().beacon_block_root; self.try_send(BeaconWorkEvent { @@ -221,7 +170,6 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { - let reprocess_tx = processor.reprocess_tx.clone(); let invalid_block_storage = processor.invalid_block_storage.clone(); let duplicate_cache = processor.duplicate_cache.clone(); processor @@ -230,7 +178,6 @@ impl NetworkBeaconProcessor { peer_id, peer_client, block, - reprocess_tx, duplicate_cache, invalid_block_storage, seen_timestamp, @@ -279,7 +226,6 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - peer_client: Client, subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_timestamp: Duration, @@ -290,7 +236,6 @@ impl NetworkBeaconProcessor { .process_gossip_data_column_sidecar( message_id, peer_id, - peer_client, subnet_id, column_sidecar, seen_timestamp, @@ -447,12 +392,11 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - let reprocess_tx = processor.reprocess_tx.clone(); processor.process_gossip_optimistic_update( message_id, peer_id, light_client_optimistic_update, - Some(reprocess_tx), + true, seen_timestamp, ) }; @@ -573,40 +517,21 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some sampling columns, and reports the verification result - /// back to sync. - pub fn send_rpc_validate_data_columns( + pub fn send_historic_data_columns( self: &Arc, - block_root: Hash256, - data_columns: Vec>>, - seen_timestamp: Duration, - id: SamplingId, + batch_id: CustodyBackfillBatchId, + data_columns: DataColumnSidecarList, + expected_cgc: u64, ) -> Result<(), Error> { - let s = self.clone(); - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::RpcVerifyDataColumn(Box::pin(async move { - let result = s - .clone() - .validate_rpc_data_columns(block_root, data_columns, seen_timestamp) - .await; - // Sync handles these results - s.send_sync_message(SyncMessage::SampleVerified { id, result }); - })), - }) - } + let processor = self.clone(); + let process_fn = + move || processor.process_historic_data_columns(batch_id, data_columns, expected_cgc); + + let work = Work::ChainSegmentBackfill(Box::new(process_fn)); - /// Create a new `Work` event with a block sampling completed result - pub fn send_sampling_completed( - self: &Arc, - block_root: Hash256, - ) -> Result<(), Error> { - let nbp = self.clone(); self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::SamplingResult(Box::pin(async move { - nbp.process_sampling_completed(block_root).await; - })), + drop_during_sync: true, + work, }) } @@ -616,33 +541,23 @@ impl NetworkBeaconProcessor { process_id: ChainSegmentProcessId, blocks: Vec>, ) -> Result<(), Error> { - let is_backfill = matches!(&process_id, ChainSegmentProcessId::BackSyncBatchId { .. }); debug!(blocks = blocks.len(), id = ?process_id, "Batch sending for process"); - let processor = self.clone(); - let process_fn = async move { - let notify_execution_layer = if processor - .network_globals - .sync_state - .read() - .is_syncing_finalized() - { - NotifyExecutionLayer::No - } else { - NotifyExecutionLayer::Yes - }; - processor - .process_chain_segment(process_id, blocks, notify_execution_layer) - .await; - }; - let process_fn = Box::pin(process_fn); // Back-sync batches are dispatched with a different `Work` variant so // they can be rate-limited. - let work = if is_backfill { - Work::ChainSegmentBackfill(process_fn) - } else { - Work::ChainSegment(process_fn) + let work = match process_id { + ChainSegmentProcessId::RangeBatchId(_, _) => { + let process_fn = async move { + processor.process_chain_segment(process_id, blocks).await; + }; + Work::ChainSegment(Box::pin(process_fn)) + } + ChainSegmentProcessId::BackSyncBatchId(_) => { + let process_fn = + move || processor.process_chain_segment_backfill(process_id, blocks); + Work::ChainSegmentBackfill(Box::new(process_fn)) + } }; self.try_send(BeaconWorkEvent { @@ -745,7 +660,7 @@ impl NetworkBeaconProcessor { self: &Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, - request: DataColumnsByRootRequest, + request: DataColumnsByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { @@ -867,16 +782,26 @@ impl NetworkBeaconProcessor { block_root: Hash256, publish_blobs: bool, ) { - let custody_columns = self.network_globals.sampling_columns.clone(); + if self.chain.config.disable_get_blobs { + return; + } + let epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); + let custody_columns = self.chain.sampling_columns_for_epoch(epoch); let self_cloned = self.clone(); let publish_fn = move |blobs_or_data_column| { if publish_blobs { match blobs_or_data_column { - BlobsOrDataColumns::Blobs(blobs) => { - self_cloned.publish_blobs_gradually(blobs, block_root); + EngineGetBlobsOutput::Blobs(blobs) => { + self_cloned.publish_blobs_gradually( + blobs.into_iter().map(|b| b.to_blob()).collect(), + block_root, + ); } - BlobsOrDataColumns::DataColumns(columns) => { - self_cloned.publish_data_columns_gradually(columns, block_root); + EngineGetBlobsOutput::CustodyColumns(columns) => { + self_cloned.publish_data_columns_gradually( + columns.into_iter().map(|c| c.clone_arc()).collect(), + block_root, + ); } }; } @@ -889,11 +814,6 @@ impl NetworkBeaconProcessor { custody_columns, publish_fn, ) - .instrument(tracing::info_span!( - "", - service = "fetch_engine_blobs", - block_root = format!("{:?}", block_root) - )) .await { Ok(Some(availability)) => match availability { @@ -936,31 +856,15 @@ impl NetworkBeaconProcessor { } } - /// Attempt to reconstruct all data columns if the following conditions satisfies: - /// - Our custody requirement is all columns - /// - We have >= 50% of columns, but not all columns - /// - /// Returns `Some(AvailabilityProcessingStatus)` if reconstruction is successfully performed, - /// otherwise returns `None`. - /// - /// The `publish_columns` parameter controls whether reconstructed columns should be published - /// to the gossip network. - async fn attempt_data_column_reconstruction( - self: &Arc, - block_root: Hash256, - publish_columns: bool, - ) -> Option { - // Only supernodes attempt reconstruction - if !self.network_globals.is_supernode() { - return None; - } - + /// Attempts to reconstruct all data columns if the conditions checked in + /// [`DataAvailabilityCheckerInner::check_and_set_reconstruction_started`] are satisfied. + #[instrument(level = "debug", skip_all, fields(?block_root))] + async fn attempt_data_column_reconstruction(self: &Arc, block_root: Hash256) { let result = self.chain.reconstruct_data_columns(block_root).await; + match result { Ok(Some((availability_processing_status, data_columns_to_publish))) => { - if publish_columns { - self.publish_data_columns_gradually(data_columns_to_publish, block_root); - } + self.publish_data_columns_gradually(data_columns_to_publish, block_root); match &availability_processing_status { AvailabilityProcessingStatus::Imported(hash) => { debug!( @@ -973,21 +877,21 @@ impl NetworkBeaconProcessor { AvailabilityProcessingStatus::MissingComponents(_, _) => { debug!( result = "imported all custody columns", - block_hash = %block_root, + %block_root, "Block components still missing block after reconstruction" ); } } - - Some(availability_processing_status) } Ok(None) => { // reason is tracked via the `KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL` metric trace!( - block_hash = %block_root, + %block_root, "Reconstruction not required for block" ); - None + } + Err(BlockError::DuplicateFullyImported(_)) => { + debug!("Block already imported in parallel with reconstruction"); } Err(e) => { error!( @@ -995,7 +899,6 @@ impl NetworkBeaconProcessor { error = ?e, "Error during data column reconstruction" ); - None } } } @@ -1008,7 +911,7 @@ impl NetworkBeaconProcessor { /// publisher exists for a blob, it will eventually get published here. fn publish_blobs_gradually( self: &Arc, - mut blobs: Vec>, + mut blobs: Vec>>, block_root: Hash256, ) { let self_clone = self.clone(); @@ -1028,7 +931,7 @@ impl NetworkBeaconProcessor { // Permute the blobs and split them into batches. // The hope is that we won't need to publish some blobs because we will receive them // on gossip from other nodes. - blobs.shuffle(&mut rand::thread_rng()); + blobs.shuffle(&mut rand::rng()); let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; let mut publish_count = 0usize; @@ -1039,8 +942,8 @@ impl NetworkBeaconProcessor { while blobs_iter.peek().is_some() { let batch = blobs_iter.by_ref().take(batch_size); let publishable = batch - .filter_map(|unobserved| match unobserved.observe(&chain) { - Ok(observed) => Some(observed.clone_blob()), + .filter_map(|blob| match observe_gossip_blob(&blob, &chain) { + Ok(()) => Some(blob), Err(GossipBlobError::RepeatBlob { .. }) => None, Err(e) => { warn!( @@ -1084,6 +987,7 @@ impl NetworkBeaconProcessor { /// by some nodes on the network as soon as possible. Our hope is that some columns arrive from /// other nodes in the meantime, obviating the need for us to publish them. If no other /// publisher exists for a column, it will eventually get published here. + #[instrument(level="debug", skip_all, fields(?block_root, data_column_count=data_columns_to_publish.len()))] fn publish_data_columns_gradually( self: &Arc, mut data_columns_to_publish: DataColumnSidecarList, @@ -1110,11 +1014,11 @@ impl NetworkBeaconProcessor { // Permute the columns and split them into batches. // The hope is that we won't need to publish some columns because we will receive them // on gossip from other nodes. - data_columns_to_publish.shuffle(&mut rand::thread_rng()); + data_columns_to_publish.shuffle(&mut rand::rng()); let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; let blob_publication_batches = chain.config.blob_publication_batches; - let number_of_columns = chain.spec.number_of_columns as usize; + let number_of_columns = T::EthSpec::number_of_columns(); let batch_size = number_of_columns / blob_publication_batches; let mut publish_count = 0usize; @@ -1163,16 +1067,13 @@ impl NetworkBeaconProcessor { #[cfg(test)] use { - beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend}, - beacon_processor::BeaconProcessorChannels, - slot_clock::ManualSlotClock, - store::MemoryStore, - tokio::sync::mpsc::UnboundedSender, + beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, + slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, }; #[cfg(test)] pub(crate) type TestBeaconChainType = - Witness, E, MemoryStore, MemoryStore>; + Witness, MemoryStore>; #[cfg(test)] impl NetworkBeaconProcessor> { @@ -1189,8 +1090,6 @@ impl NetworkBeaconProcessor> { let BeaconProcessorChannels { beacon_processor_tx, beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx: _work_reprocessing_rx, } = <_>::default(); let (network_tx, _network_rx) = mpsc::unbounded_channel(); @@ -1201,7 +1100,6 @@ impl NetworkBeaconProcessor> { chain, network_tx, sync_tx, - reprocess_tx: work_reprocessing_tx, network_globals, invalid_block_storage: InvalidBlockStorage::Disabled, executor, diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 7c3c854ed8..ac24b648e0 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -1,23 +1,30 @@ use crate::metrics; -use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; +use crate::network_beacon_processor::{FUTURE_SLOT_TOLERANCE, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::SyncMessage; -use beacon_chain::{BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; -use itertools::{process_results, Itertools}; +use beacon_chain::{BeaconChainError, BeaconChainTypes, BlockProcessStatus, WhenSlotSkipped}; +use itertools::{Itertools, process_results}; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, }; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, ReportSource, Response, SyncInfo}; +use lighthouse_tracing::{ + SPAN_HANDLE_BLOBS_BY_RANGE_REQUEST, SPAN_HANDLE_BLOBS_BY_ROOT_REQUEST, + SPAN_HANDLE_BLOCKS_BY_RANGE_REQUEST, SPAN_HANDLE_BLOCKS_BY_ROOT_REQUEST, + SPAN_HANDLE_DATA_COLUMNS_BY_RANGE_REQUEST, SPAN_HANDLE_DATA_COLUMNS_BY_ROOT_REQUEST, + SPAN_HANDLE_LIGHT_CLIENT_BOOTSTRAP, SPAN_HANDLE_LIGHT_CLIENT_FINALITY_UPDATE, + SPAN_HANDLE_LIGHT_CLIENT_OPTIMISTIC_UPDATE, SPAN_HANDLE_LIGHT_CLIENT_UPDATES_BY_RANGE, +}; use methods::LightClientUpdatesByRangeRequest; use slot_clock::SlotClock; -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::{HashMap, HashSet, hash_map::Entry}; use std::sync::Arc; use tokio_stream::StreamExt; -use tracing::{debug, error, warn}; +use tracing::{Span, debug, error, field, instrument, warn}; use types::blob_sidecar::BlobIdentifier; -use types::{Epoch, EthSpec, Hash256, Slot}; +use types::{ColumnIndex, Epoch, EthSpec, Hash256, Slot}; impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -70,14 +77,14 @@ impl NetworkBeaconProcessor { let local = self.chain.status_message(); let start_slot = |epoch: Epoch| epoch.start_slot(T::EthSpec::slots_per_epoch()); - let irrelevant_reason = if local.fork_digest != remote.fork_digest { + let irrelevant_reason = if local.fork_digest() != remote.fork_digest() { // The node is on a different network/fork Some(format!( "Incompatible forks Ours:{} Theirs:{}", - hex::encode(local.fork_digest), - hex::encode(remote.fork_digest) + hex::encode(local.fork_digest()), + hex::encode(remote.fork_digest()) )) - } else if remote.head_slot + } else if *remote.head_slot() > self .chain .slot() @@ -88,11 +95,11 @@ impl NetworkBeaconProcessor { // current slot. This could be because they are using a different genesis time, or that // their or our system's clock is incorrect. Some("Different system clocks or genesis time".to_string()) - } else if (remote.finalized_epoch == local.finalized_epoch - && remote.finalized_root == local.finalized_root) - || remote.finalized_root.is_zero() - || local.finalized_root.is_zero() - || remote.finalized_epoch > local.finalized_epoch + } else if (remote.finalized_epoch() == local.finalized_epoch() + && remote.finalized_root() == local.finalized_root()) + || remote.finalized_root().is_zero() + || local.finalized_root().is_zero() + || remote.finalized_epoch() > local.finalized_epoch() { // Fast path. Remote finalized checkpoint is either identical, or genesis, or we are at // genesis, or they are ahead. In all cases, we should allow this peer to connect to us @@ -100,7 +107,7 @@ impl NetworkBeaconProcessor { None } else { // Remote finalized epoch is less than ours. - let remote_finalized_slot = start_slot(remote.finalized_epoch); + let remote_finalized_slot = start_slot(*remote.finalized_epoch()); if remote_finalized_slot < self.chain.store.get_oldest_block_slot() { // Peer's finalized checkpoint is older than anything in our DB. We are unlikely // to be able to help them sync. @@ -112,7 +119,7 @@ impl NetworkBeaconProcessor { if self .chain .block_root_at_slot(remote_finalized_slot, WhenSlotSkipped::Prev) - .map(|root_opt| root_opt != Some(remote.finalized_root)) + .map(|root_opt| root_opt != Some(*remote.finalized_root())) .map_err(Box::new)? { Some("Different finalized chain".to_string()) @@ -138,10 +145,11 @@ impl NetworkBeaconProcessor { } Ok(None) => { let info = SyncInfo { - head_slot: status.head_slot, - head_root: status.head_root, - finalized_epoch: status.finalized_epoch, - finalized_root: status.finalized_root, + head_slot: *status.head_slot(), + head_root: *status.head_root(), + finalized_epoch: *status.finalized_epoch(), + finalized_root: *status.finalized_root(), + earliest_available_slot: status.earliest_available_slot().ok().cloned(), }; self.send_sync_message(SyncMessage::AddPeer(peer_id, info)); } @@ -154,12 +162,22 @@ impl NetworkBeaconProcessor { } /// Handle a `BlocksByRoot` request from the peer. + #[instrument( + name = SPAN_HANDLE_BLOCKS_BY_ROOT_REQUEST, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub async fn handle_blocks_by_root_request( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, request: BlocksByRootRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -171,7 +189,7 @@ impl NetworkBeaconProcessor { } /// Handle a `BlocksByRoot` request from the peer. - pub async fn handle_blocks_by_root_request_inner( + async fn handle_blocks_by_root_request_inner( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, @@ -244,12 +262,22 @@ impl NetworkBeaconProcessor { } /// Handle a `BlobsByRoot` request from the peer. + #[instrument( + name = SPAN_HANDLE_BLOBS_BY_ROOT_REQUEST, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_blobs_by_root_request( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, request: BlobsByRootRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -259,27 +287,58 @@ impl NetworkBeaconProcessor { } /// Handle a `BlobsByRoot` request from the peer. - pub fn handle_blobs_by_root_request_inner( + fn handle_blobs_by_root_request_inner( &self, peer_id: PeerId, inbound_request_id: InboundRequestId, request: BlobsByRootRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { - let Some(requested_root) = request.blob_ids.as_slice().first().map(|id| id.block_root) - else { - // No blob ids requested. - return Ok(()); - }; - let requested_indices = request - .blob_ids - .as_slice() - .iter() - .map(|id| id.index) - .collect::>(); + let requested_roots: HashSet = + request.blob_ids.iter().map(|id| id.block_root).collect(); + let mut send_blob_count = 0; + let fulu_start_slot = self + .chain + .spec + .fulu_fork_epoch + .map(|epoch| epoch.start_slot(T::EthSpec::slots_per_epoch())); + let mut blob_list_results = HashMap::new(); + + let slots_by_block_root: HashMap = request + .blob_ids + .iter() + .flat_map(|blob_id| { + let block_root = blob_id.block_root; + self.chain + .data_availability_checker + .get_cached_block(&block_root) + .and_then(|status| match status { + BlockProcessStatus::NotValidated(block, _source) => Some(block), + BlockProcessStatus::ExecutionValidated(block) => Some(block), + BlockProcessStatus::Unknown => None, + }) + .or_else(|| self.chain.early_attester_cache.get_block(block_root)) + .map(|block| (block_root, block.slot())) + }) + .collect(); + for id in request.blob_ids.as_slice() { + let BlobIdentifier { + block_root: root, + index, + } = id; + + let slot = slots_by_block_root.get(root); + + // Skip if slot is >= fulu_start_slot + if let (Some(slot), Some(fulu_slot)) = (slot, fulu_start_slot) + && *slot >= fulu_slot + { + continue; + } + // First attempt to get the blobs from the RPC cache. if let Ok(Some(blob)) = self.chain.data_availability_checker.get_blob(id) { self.send_response( @@ -289,11 +348,6 @@ impl NetworkBeaconProcessor { ); send_blob_count += 1; } else { - let BlobIdentifier { - block_root: root, - index, - } = id; - let blob_list_result = match blob_list_results.entry(root) { Entry::Vacant(entry) => { entry.insert(self.chain.get_blobs_checking_early_attester_cache(root)) @@ -303,16 +357,15 @@ impl NetworkBeaconProcessor { match blob_list_result.as_ref() { Ok(blobs_sidecar_list) => { - 'inner: for blob_sidecar in blobs_sidecar_list.iter() { - if blob_sidecar.index == *index { - self.send_response( - peer_id, - inbound_request_id, - Response::BlobsByRoot(Some(blob_sidecar.clone())), - ); - send_blob_count += 1; - break 'inner; - } + if let Some(blob_sidecar) = + blobs_sidecar_list.iter().find(|b| b.index == *index) + { + self.send_response( + peer_id, + inbound_request_id, + Response::BlobsByRoot(Some(blob_sidecar.clone())), + ); + send_blob_count += 1; } } Err(e) => { @@ -326,10 +379,10 @@ impl NetworkBeaconProcessor { } } } + debug!( %peer_id, - %requested_root, - ?requested_indices, + ?requested_roots, returned = send_blob_count, "BlobsByRoot outgoing response processed" ); @@ -338,12 +391,36 @@ impl NetworkBeaconProcessor { } /// Handle a `DataColumnsByRoot` request from the peer. + #[instrument( + name = SPAN_HANDLE_DATA_COLUMNS_BY_ROOT_REQUEST, + parent = None, + level = "debug", + skip_all, + fields( + peer_id = %peer_id, + client = tracing::field::Empty, + non_custody_indices = tracing::field::Empty, + ) + )] pub fn handle_data_columns_by_root_request( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, - request: DataColumnsByRootRequest, + request: DataColumnsByRootRequest, ) { + let requested_columns = request + .data_column_ids + .iter() + .flat_map(|id| id.columns.clone()) + .unique() + .collect::>(); + self.record_data_column_request_in_span( + &peer_id, + &requested_columns, + None, + Span::current(), + ); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -353,18 +430,26 @@ impl NetworkBeaconProcessor { } /// Handle a `DataColumnsByRoot` request from the peer. - pub fn handle_data_columns_by_root_request_inner( + fn handle_data_columns_by_root_request_inner( &self, peer_id: PeerId, inbound_request_id: InboundRequestId, - request: DataColumnsByRootRequest, + request: DataColumnsByRootRequest, ) -> Result<(), (RpcErrorResponse, &'static str)> { let mut send_data_column_count = 0; + // Only attempt lookups for columns the node has advertised and is responsible for maintaining custody of. + let available_columns = self.chain.custody_columns_for_epoch(None); for data_column_ids_by_root in request.data_column_ids.as_slice() { + let indices_to_retrieve = data_column_ids_by_root + .columns + .iter() + .copied() + .filter(|c| available_columns.contains(c)) + .collect::>(); match self.chain.get_data_columns_checking_all_caches( data_column_ids_by_root.block_root, - data_column_ids_by_root.columns.as_slice(), + &indices_to_retrieve, ) { Ok(data_columns) => { send_data_column_count += data_columns.len(); @@ -377,12 +462,12 @@ impl NetworkBeaconProcessor { } } Err(e) => { - // TODO(das): lower log level when feature is stabilized - error!( + // The node is expected to be able to serve these columns, but it fails to retrieve them. + warn!( block_root = ?data_column_ids_by_root.block_root, %peer_id, error = ?e, - "Error getting data column" + "Error getting data column for by root request " ); return Err((RpcErrorResponse::ServerError, "Error getting data column")); } @@ -399,12 +484,22 @@ impl NetworkBeaconProcessor { Ok(()) } + #[instrument( + name = SPAN_HANDLE_LIGHT_CLIENT_UPDATES_BY_RANGE, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_light_client_updates_by_range( self: &Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, request: LightClientUpdatesByRangeRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -419,7 +514,7 @@ impl NetworkBeaconProcessor { } /// Handle a `LightClientUpdatesByRange` request from the peer. - pub fn handle_light_client_updates_by_range_request_inner( + fn handle_light_client_updates_by_range_request_inner( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, @@ -490,12 +585,22 @@ impl NetworkBeaconProcessor { } /// Handle a `LightClientBootstrap` request from the peer. + #[instrument( + name = SPAN_HANDLE_LIGHT_CLIENT_BOOTSTRAP, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_light_client_bootstrap( self: &Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, request: LightClientBootstrapRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_single_item( peer_id, inbound_request_id, @@ -520,11 +625,21 @@ impl NetworkBeaconProcessor { } /// Handle a `LightClientOptimisticUpdate` request from the peer. + #[instrument( + name = SPAN_HANDLE_LIGHT_CLIENT_OPTIMISTIC_UPDATE, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_light_client_optimistic_update( self: &Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_single_item( peer_id, inbound_request_id, @@ -544,11 +659,21 @@ impl NetworkBeaconProcessor { } /// Handle a `LightClientFinalityUpdate` request from the peer. + #[instrument( + name = SPAN_HANDLE_LIGHT_CLIENT_FINALITY_UPDATE, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_light_client_finality_update( self: &Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_single_item( peer_id, inbound_request_id, @@ -568,12 +693,22 @@ impl NetworkBeaconProcessor { } /// Handle a `BlocksByRange` request from the peer. + #[instrument( + name = SPAN_HANDLE_BLOCKS_BY_RANGE_REQUEST, + parent = None, + level = "debug", + skip_all, + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub async fn handle_blocks_by_range_request( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, req: BlocksByRangeRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -585,7 +720,7 @@ impl NetworkBeaconProcessor { } /// Handle a `BlocksByRange` request from the peer. - pub async fn handle_blocks_by_range_request_inner( + async fn handle_blocks_by_range_request_inner( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, @@ -699,7 +834,7 @@ impl NetworkBeaconProcessor { Err(e) => { if matches!( e, - BeaconChainError::ExecutionLayerErrorPayloadReconstruction(_block_hash, ref boxed_error) + BeaconChainError::ExecutionLayerErrorPayloadReconstruction(_block_hash, boxed_error) if matches!(**boxed_error, execution_layer::Error::EngineError(_)) ) { warn!( @@ -854,12 +989,22 @@ impl NetworkBeaconProcessor { } /// Handle a `BlobsByRange` request from the peer. + #[instrument( + name = SPAN_HANDLE_BLOBS_BY_RANGE_REQUEST, + parent = None, + skip_all, + level = "debug", + fields(peer_id = %peer_id, client = tracing::field::Empty) + )] pub fn handle_blobs_by_range_request( self: Arc, peer_id: PeerId, inbound_request_id: InboundRequestId, req: BlobsByRangeRequest, ) { + let client = self.network_globals.client(&peer_id); + Span::current().record("client", field::display(client.kind)); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -883,6 +1028,34 @@ impl NetworkBeaconProcessor { ); let request_start_slot = Slot::from(req.start_slot); + let request_start_epoch = request_start_slot.epoch(T::EthSpec::slots_per_epoch()); + let fork_name = self.chain.spec.fork_name_at_epoch(request_start_epoch); + // Should not send more than max request blob sidecars + if req.max_blobs_requested(request_start_epoch, &self.chain.spec) + > self.chain.spec.max_request_blob_sidecars(fork_name) as u64 + { + return Err(( + RpcErrorResponse::InvalidRequest, + "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`", + )); + } + + let effective_count = if let Some(fulu_epoch) = self.chain.spec.fulu_fork_epoch { + let fulu_start_slot = fulu_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let request_end_slot = request_start_slot.saturating_add(req.count) - 1; + + // If the request_start_slot is at or after a Fulu slot, return an empty response + if request_start_slot >= fulu_start_slot { + return Ok(()); + // For the case that the request slots spans across the Fulu fork slot + } else if request_end_slot >= fulu_start_slot { + (fulu_start_slot - request_start_slot).as_u64() + } else { + req.count + } + } else { + req.count + }; let data_availability_boundary_slot = match self.chain.data_availability_boundary() { Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), @@ -920,7 +1093,7 @@ impl NetworkBeaconProcessor { } let block_roots = - self.get_block_roots_for_slot_range(req.start_slot, req.count, "BlobsByRange")?; + self.get_block_roots_for_slot_range(req.start_slot, effective_count, "BlobsByRange")?; let current_slot = self .chain @@ -944,12 +1117,18 @@ impl NetworkBeaconProcessor { match self.chain.get_blobs(&root) { Ok(blob_sidecar_list) => { for blob_sidecar in blob_sidecar_list.iter() { - blobs_sent += 1; - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - inbound_request_id, - response: Response::BlobsByRange(Some(blob_sidecar.clone())), - }); + // Due to skip slots, blobs could be out of the range, we ensure they + // are in the range before sending + if blob_sidecar.slot() >= request_start_slot + && blob_sidecar.slot() < request_start_slot + effective_count + { + blobs_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + inbound_request_id, + response: Response::BlobsByRange(Some(blob_sidecar.clone())), + }); + } } } Err(e) => { @@ -975,12 +1154,27 @@ impl NetworkBeaconProcessor { } /// Handle a `DataColumnsByRange` request from the peer. + #[instrument( + name = SPAN_HANDLE_DATA_COLUMNS_BY_RANGE_REQUEST, + parent = None, + skip_all, + level = "debug", + fields(peer_id = %peer_id, non_custody_indices = tracing::field::Empty, client = tracing::field::Empty) + )] pub fn handle_data_columns_by_range_request( &self, peer_id: PeerId, inbound_request_id: InboundRequestId, req: DataColumnsByRangeRequest, ) { + let epoch = Slot::new(req.start_slot).epoch(T::EthSpec::slots_per_epoch()); + self.record_data_column_request_in_span( + &peer_id, + &req.columns, + Some(epoch), + Span::current(), + ); + self.terminate_response_stream( peer_id, inbound_request_id, @@ -990,7 +1184,7 @@ impl NetworkBeaconProcessor { } /// Handle a `DataColumnsByRange` request from the peer. - pub fn handle_data_columns_by_range_request_inner( + fn handle_data_columns_by_range_request_inner( &self, peer_id: PeerId, inbound_request_id: InboundRequestId, @@ -1007,39 +1201,48 @@ impl NetworkBeaconProcessor { if req.max_requested::() > self.chain.spec.max_request_data_column_sidecars { return Err(( RpcErrorResponse::InvalidRequest, - "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`", + "Request exceeded `MAX_REQUEST_DATA_COLUMN_SIDECARS`", )); } let request_start_slot = Slot::from(req.start_slot); - let data_availability_boundary_slot = match self.chain.data_availability_boundary() { - Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), - None => { - debug!("Deneb fork is disabled"); - return Err((RpcErrorResponse::InvalidRequest, "Deneb fork is disabled")); - } - }; + let column_data_availability_boundary_slot = + match self.chain.column_data_availability_boundary() { + Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), + None => { + debug!("Fulu fork is disabled"); + return Err((RpcErrorResponse::InvalidRequest, "Fulu fork is disabled")); + } + }; - let oldest_data_column_slot = self - .chain - .store - .get_data_column_info() - .oldest_data_column_slot - .unwrap_or(data_availability_boundary_slot); + let earliest_custodied_data_column_slot = + match self.chain.earliest_custodied_data_column_epoch() { + Some(earliest_custodied_epoch) => { + let earliest_custodied_slot = + earliest_custodied_epoch.start_slot(T::EthSpec::slots_per_epoch()); + // Ensure the earliest columns we serve are within the data availability window + if earliest_custodied_slot < column_data_availability_boundary_slot { + column_data_availability_boundary_slot + } else { + earliest_custodied_slot + } + } + None => column_data_availability_boundary_slot, + }; - if request_start_slot < oldest_data_column_slot { + if request_start_slot < earliest_custodied_data_column_slot { debug!( %request_start_slot, - %oldest_data_column_slot, - %data_availability_boundary_slot, - "Range request start slot is older than data availability boundary." + %earliest_custodied_data_column_slot, + %column_data_availability_boundary_slot, + "Range request start slot is older than the earliest custodied data column slot." ); - return if data_availability_boundary_slot < oldest_data_column_slot { + return if earliest_custodied_data_column_slot > column_data_availability_boundary_slot { Err(( RpcErrorResponse::ResourceUnavailable, - "blobs pruned within boundary", + "columns pruned within boundary", )) } else { Err(( @@ -1053,18 +1256,37 @@ impl NetworkBeaconProcessor { self.get_block_roots_for_slot_range(req.start_slot, req.count, "DataColumnsByRange")?; let mut data_columns_sent = 0; + // Only attempt lookups for columns the node has advertised and is responsible for maintaining custody of. + let request_start_epoch = request_start_slot.epoch(T::EthSpec::slots_per_epoch()); + let available_columns = self + .chain + .custody_columns_for_epoch(Some(request_start_epoch)); + + let indices_to_retrieve = req + .columns + .iter() + .copied() + .filter(|c| available_columns.contains(c)) + .collect::>(); + for root in block_roots { - for index in &req.columns { + for index in &indices_to_retrieve { match self.chain.get_data_column(&root, index) { Ok(Some(data_column_sidecar)) => { - data_columns_sent += 1; - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - inbound_request_id, - response: Response::DataColumnsByRange(Some( - data_column_sidecar.clone(), - )), - }); + // Due to skip slots, data columns could be out of the range, we ensure they + // are in the range before sending + if data_column_sidecar.slot() >= request_start_slot + && data_column_sidecar.slot() < request_start_slot + req.count + { + data_columns_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + inbound_request_id, + response: Response::DataColumnsByRange(Some( + data_column_sidecar.clone(), + )), + }); + } } Ok(None) => {} // no-op Err(e) => { @@ -1144,4 +1366,29 @@ impl NetworkBeaconProcessor { } } } + + fn record_data_column_request_in_span( + &self, + peer_id: &PeerId, + requested_indices: &[ColumnIndex], + epoch_opt: Option, + span: Span, + ) { + let non_custody_indices = { + let custody_columns = self + .chain + .data_availability_checker + .custody_context() + .custody_columns_for_epoch(epoch_opt, &self.chain.spec); + requested_indices + .iter() + .filter(|subnet_id| !custody_columns.contains(subnet_id)) + .collect::>() + }; + // This field is used to identify if peers are sending requests on columns we don't custody. + span.record("non_custody_indices", field::debug(non_custody_indices)); + + let client = self.network_globals.client(peer_id); + span.record("client", field::display(client.kind)); + } } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 31b17a41a4..e49ae134fe 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -1,31 +1,39 @@ use crate::metrics::{self, register_process_result_metrics}; -use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; +use crate::network_beacon_processor::{FUTURE_SLOT_TOLERANCE, NetworkBeaconProcessor}; use crate::sync::BatchProcessResult; +use crate::sync::manager::CustodyBatchProcessResult; use crate::sync::{ - manager::{BlockProcessType, SyncMessage}, ChainId, + manager::{BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::data_availability_checker::AvailabilityCheckError; use beacon_chain::data_availability_checker::MaybeAvailableBlock; -use beacon_chain::data_column_verification::verify_kzg_for_data_column_list; +use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ - validator_monitor::get_slot_delay_ms, AvailabilityProcessingStatus, BeaconChainTypes, - BlockError, ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, + AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, + HistoricalBlockError, NotifyExecutionLayer, validator_monitor::get_slot_delay_ms, }; use beacon_processor::{ - work_reprocessing_queue::{QueuedRpcBlock, ReprocessQueueMessage}, AsyncFn, BlockingFn, DuplicateCache, + work_reprocessing_queue::{QueuedRpcBlock, ReprocessQueueMessage}, }; +use beacon_processor::{Work, WorkEvent}; use lighthouse_network::PeerAction; +use lighthouse_network::service::api_types::CustodyBackfillBatchId; +use lighthouse_tracing::{ + SPAN_CUSTODY_BACKFILL_SYNC_IMPORT_COLUMNS, SPAN_PROCESS_CHAIN_SEGMENT, + SPAN_PROCESS_CHAIN_SEGMENT_BACKFILL, SPAN_PROCESS_RPC_BLOBS, SPAN_PROCESS_RPC_BLOCK, + SPAN_PROCESS_RPC_CUSTODY_COLUMNS, +}; +use logging::crit; use std::sync::Arc; use std::time::Duration; use store::KzgCommitment; -use tokio::sync::mpsc; -use tracing::{debug, error, info, warn}; +use tracing::{debug, debug_span, error, info, instrument, warn}; use types::beacon_block_body::format_kzg_commitments; use types::blob_sidecar::FixedBlobSidecarList; -use types::{BlockImportSource, DataColumnSidecar, DataColumnSidecarList, Epoch, Hash256}; +use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] @@ -57,14 +65,12 @@ impl NetworkBeaconProcessor { process_type: BlockProcessType, ) -> AsyncFn { let process_fn = async move { - let reprocess_tx = self.reprocess_tx.clone(); let duplicate_cache = self.duplicate_cache.clone(); self.process_rpc_block( block_root, block, seen_timestamp, process_type, - reprocess_tx, duplicate_cache, ) .await; @@ -100,13 +106,19 @@ impl NetworkBeaconProcessor { /// Attempt to process a block received from a direct RPC request. #[allow(clippy::too_many_arguments)] + #[instrument( + name = SPAN_PROCESS_RPC_BLOCK, + parent = None, + level = "debug", + skip_all, + fields(?block_root), + )] pub async fn process_rpc_block( self: Arc>, block_root: Hash256, block: RpcBlock, seen_timestamp: Duration, process_type: BlockProcessType, - reprocess_tx: mpsc::Sender, duplicate_cache: DuplicateCache, ) { // Check if the block is already being imported through another source @@ -131,14 +143,20 @@ impl NetworkBeaconProcessor { ignore_fn, }); - if reprocess_tx.try_send(reprocess_msg).is_err() { + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { error!(source = "rpc", %block_root,"Failed to inform block import") }; return; }; let slot = block.slot(); - let block_has_data = block.as_block().num_expected_blobs() > 0; let parent_root = block.message().parent_root(); let commitments_formatted = block.as_block().commitments_formatted(); @@ -154,11 +172,12 @@ impl NetworkBeaconProcessor { let signed_beacon_block = block.block_cloned(); let result = self .chain - .process_block_with_early_caching( + .process_block( block_root, block, - BlockImportSource::Lookup, NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), ) .await; register_process_result_metrics(&result, metrics::BlockSource::Rpc, "block"); @@ -176,7 +195,14 @@ impl NetworkBeaconProcessor { block_root: *hash, parent_root, }; - if reprocess_tx.try_send(reprocess_msg).is_err() { + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { error!( source = "rpc", block_root = %hash, @@ -204,17 +230,6 @@ impl NetworkBeaconProcessor { _ => {} } - // RPC block imported or execution validated. If the block was already imported by gossip we - // receive Err(BlockError::AlreadyKnown). - if result.is_ok() && - // Block has at least one blob, so it produced columns - block_has_data && - // Block slot is within the DA boundary (should always be the case) and PeerDAS is activated - self.chain.should_sample_slot(slot) - { - self.send_sync_message(SyncMessage::SampleBlock(block_root, slot)); - } - // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, @@ -245,6 +260,13 @@ impl NetworkBeaconProcessor { } /// Attempt to process a list of blobs received from a direct RPC request. + #[instrument( + name = SPAN_PROCESS_RPC_BLOBS, + parent = None, + level = "debug", + skip_all, + fields(?block_root), + )] pub async fn process_rpc_blobs( self: Arc>, block_root: Hash256, @@ -277,15 +299,15 @@ impl NetworkBeaconProcessor { "RPC blobs received" ); - if let Ok(current_slot) = self.chain.slot() { - if current_slot == slot { - // Note: this metric is useful to gauge how long it takes to receive blobs requested - // over rpc. Since we always send the request for block components at `slot_clock.single_lookup_delay()` - // we can use that as a baseline to measure against. - let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); + if let Ok(current_slot) = self.chain.slot() + && current_slot == slot + { + // Note: this metric is useful to gauge how long it takes to receive blobs requested + // over rpc. Since we always send the request for block components at `slot_clock.single_lookup_delay()` + // we can use that as a baseline to measure against. + let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); - metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); - } + metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); } let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; @@ -315,14 +337,8 @@ impl NetworkBeaconProcessor { "Blobs have already been imported" ); } - Err(e) => { - warn!( - error = ?e, - block_hash = %block_root, - %slot, - "Error when importing rpc blobs" - ); - } + // Errors are handled and logged in `block_lookups` + Err(_) => {} } // Sync handles these results @@ -332,6 +348,13 @@ impl NetworkBeaconProcessor { }); } + #[instrument( + name = SPAN_PROCESS_RPC_CUSTODY_COLUMNS, + parent = None, + level = "debug", + skip_all, + fields(?block_root), + )] pub async fn process_rpc_custody_columns( self: Arc>, block_root: Hash256, @@ -344,11 +367,11 @@ impl NetworkBeaconProcessor { return; }; - if let Ok(current_slot) = self.chain.slot() { - if current_slot == slot { - let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); - metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); - } + if let Ok(current_slot) = self.chain.slot() + && current_slot == slot + { + let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); + metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); } let mut indices = custody_columns.iter().map(|d| d.index).collect::>(); @@ -360,7 +383,7 @@ impl NetworkBeaconProcessor { "RPC custody data columns received" ); - let mut result = self + let result = self .chain .process_rpc_custody_columns(custody_columns) .await; @@ -381,17 +404,6 @@ impl NetworkBeaconProcessor { block_hash = %block_root, "Missing components over rpc" ); - // Attempt reconstruction here before notifying sync, to avoid sending out more requests - // that we may no longer need. - // We don't publish columns reconstructed from rpc columns to the gossip network, - // as these are likely historic columns. - let publish_columns = false; - if let Some(availability) = self - .attempt_data_column_reconstruction(block_root, publish_columns) - .await - { - result = Ok(availability) - } } }, Err(BlockError::DuplicateFullyImported(_)) => { @@ -400,13 +412,8 @@ impl NetworkBeaconProcessor { "Custody columns have already been imported" ); } - Err(e) => { - warn!( - error = ?e, - block_hash = %block_root, - "Error when importing rpc custody columns" - ); - } + // Errors are handled and logged in `block_lookups` + Err(_) => {} } self.send_sync_message(SyncMessage::BlockComponentProcessed { @@ -415,46 +422,148 @@ impl NetworkBeaconProcessor { }); } - /// Validate a list of data columns received from RPC requests - pub async fn validate_rpc_data_columns( - self: Arc>, - _block_root: Hash256, - data_columns: Vec>>, - _seen_timestamp: Duration, - ) -> Result<(), String> { - verify_kzg_for_data_column_list(data_columns.iter(), &self.chain.kzg) - .map_err(|err| format!("{err:?}")) - } - - /// Process a sampling completed event, inserting it into fork-choice - pub async fn process_sampling_completed( - self: Arc>, - block_root: Hash256, + pub fn process_historic_data_columns( + &self, + batch_id: CustodyBackfillBatchId, + downloaded_columns: DataColumnSidecarList, + expected_cgc: u64, ) { - self.chain.process_sampling_completed(block_root).await; + let _guard = debug_span!( + SPAN_CUSTODY_BACKFILL_SYNC_IMPORT_COLUMNS, + epoch = %batch_id.epoch, + columns_received_count = downloaded_columns.len() + ) + .entered(); + + let sent_columns = downloaded_columns.len(); + let result = match self.chain.import_historical_data_column_batch( + batch_id.epoch, + downloaded_columns, + expected_cgc, + ) { + Ok(imported_columns) => { + metrics::inc_counter_by( + &metrics::BEACON_PROCESSOR_CUSTODY_BACKFILL_COLUMN_IMPORT_SUCCESS_TOTAL, + imported_columns as u64, + ); + CustodyBatchProcessResult::Success { + sent_columns, + imported_columns, + } + } + Err(e) => { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_CUSTODY_BACKFILL_BATCH_FAILED_TOTAL, + ); + let peer_action: Option = match &e { + HistoricalDataColumnError::NoBlockFound { + data_column_block_root, + expected_block_root, + } => { + debug!( + error = "no_block_found", + ?data_column_block_root, + ?expected_block_root, + "Custody backfill batch processing error" + ); + // The peer is faulty if they send blocks with bad roots. + Some(PeerAction::LowToleranceError) + } + HistoricalDataColumnError::MissingDataColumns { .. } => { + warn!( + error = ?e, + "Custody backfill batch processing error", + ); + // The peer is faulty if they don't return data columns + // that they advertised as available. + Some(PeerAction::LowToleranceError) + } + HistoricalDataColumnError::InvalidKzg => { + warn!( + error = ?e, + "Custody backfill batch processing error", + ); + // The peer is faulty if they don't return data columns + // with valid kzg commitments. + Some(PeerAction::LowToleranceError) + } + HistoricalDataColumnError::BeaconChainError(e) => { + match &**e { + beacon_chain::BeaconChainError::FailedColumnCustodyInfoUpdate => {} + _ => { + warn!( + error = ?e, + "Custody backfill batch processing error", + ); + } + } + + // This is an interal error, don't penalize the peer + None + } + HistoricalDataColumnError::IndexOutOfBounds => { + error!( + error = ?e, + "Custody backfill batch out of bounds error" + ); + // This should never occur, don't penalize the peer. + None + } + HistoricalDataColumnError::StoreError(e) => { + warn!(error = ?e, "Custody backfill batch processing error"); + // This is an internal error, don't penalize the peer. + None + } + }; + CustodyBatchProcessResult::Error { peer_action } + } + }; + self.send_sync_message(SyncMessage::CustodyBatchProcessed { result, batch_id }); } /// Attempt to import the chain segment (`blocks`) to the beacon chain, informing the sync /// thread if more blocks are needed to process it. + #[instrument( + name = SPAN_PROCESS_CHAIN_SEGMENT, + parent = None, + level = "debug", + skip_all, + fields(process_id = ?process_id, downloaded_blocks = downloaded_blocks.len()) + )] pub async fn process_chain_segment( &self, - sync_type: ChainSegmentProcessId, + process_id: ChainSegmentProcessId, downloaded_blocks: Vec>, - notify_execution_layer: NotifyExecutionLayer, ) { - let result = match sync_type { - // this a request from the range sync - ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { - let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); - let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); - let sent_blocks = downloaded_blocks.len(); + let ChainSegmentProcessId::RangeBatchId(chain_id, epoch) = process_id else { + // This is a request from range sync, this should _never_ happen + crit!( + error = "process_chain_segment called on a variant other than RangeBatchId", + "Please notify the devs" + ); + return; + }; - match self - .process_blocks(downloaded_blocks.iter(), notify_execution_layer) - .await - { - (imported_blocks, Ok(_)) => { - debug!( + let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); + let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); + let sent_blocks = downloaded_blocks.len(); + let notify_execution_layer = if self + .network_globals + .sync_state + .read() + .is_syncing_finalized() + { + NotifyExecutionLayer::No + } else { + NotifyExecutionLayer::Yes + }; + + let result = match self + .process_blocks(downloaded_blocks.iter(), notify_execution_layer) + .await + { + (imported_blocks, Ok(_)) => { + debug!( batch_epoch = %epoch, first_block_slot = start_slot, chain = chain_id, @@ -462,13 +571,13 @@ impl NetworkBeaconProcessor { processed_blocks = sent_blocks, service= "sync", "Batch processed"); - BatchProcessResult::Success { - sent_blocks, - imported_blocks, - } - } - (imported_blocks, Err(e)) => { - debug!( + BatchProcessResult::Success { + sent_blocks, + imported_blocks, + } + } + (imported_blocks, Err(e)) => { + debug!( batch_epoch = %epoch, first_block_slot = start_slot, chain = chain_id, @@ -477,33 +586,61 @@ impl NetworkBeaconProcessor { error = %e.message, service = "sync", "Batch processing failed"); - match e.peer_action { - Some(penalty) => BatchProcessResult::FaultyFailure { - imported_blocks, - penalty, - }, - None => BatchProcessResult::NonFaultyFailure, - } - } + match e.peer_action { + Some(penalty) => BatchProcessResult::FaultyFailure { + imported_blocks, + penalty, + }, + None => BatchProcessResult::NonFaultyFailure, } } - // this a request from the Backfill sync - ChainSegmentProcessId::BackSyncBatchId(epoch) => { - let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); - let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); - let sent_blocks = downloaded_blocks.len(); - let n_blobs = downloaded_blocks - .iter() - .map(|wrapped| wrapped.n_blobs()) - .sum::(); - let n_data_columns = downloaded_blocks - .iter() - .map(|wrapped| wrapped.n_data_columns()) - .sum::(); + }; - match self.process_backfill_blocks(downloaded_blocks) { - (imported_blocks, Ok(_)) => { - debug!( + self.send_sync_message(SyncMessage::BatchProcessed { + sync_type: process_id, + result, + }); + } + + /// Attempt to import the chain segment (`blocks`) to the beacon chain, informing the sync + /// thread if more blocks are needed to process it. + #[instrument( + name = SPAN_PROCESS_CHAIN_SEGMENT_BACKFILL, + parent = None, + level = "debug", + skip_all, + fields(downloaded_blocks = downloaded_blocks.len()) + )] + pub fn process_chain_segment_backfill( + &self, + process_id: ChainSegmentProcessId, + downloaded_blocks: Vec>, + ) { + let ChainSegmentProcessId::BackSyncBatchId(epoch) = process_id else { + // this a request from RangeSync, this should _never_ happen + crit!( + error = + "process_chain_segment_backfill called on a variant other than BackSyncBatchId", + "Please notify the devs" + ); + return; + }; + + let start_slot = downloaded_blocks.first().map(|b| b.slot().as_u64()); + let end_slot = downloaded_blocks.last().map(|b| b.slot().as_u64()); + let sent_blocks = downloaded_blocks.len(); + let n_blobs = downloaded_blocks + .iter() + .map(|wrapped| wrapped.n_blobs()) + .sum::(); + let n_data_columns = downloaded_blocks + .iter() + .map(|wrapped| wrapped.n_data_columns()) + .sum::(); + + let result = match self.process_backfill_blocks(downloaded_blocks) { + (imported_blocks, Ok(_)) => { + debug!( batch_epoch = %epoch, first_block_slot = start_slot, keep_execution_payload = !self.chain.store.get_config().prune_payloads, @@ -513,37 +650,39 @@ impl NetworkBeaconProcessor { processed_data_columns = n_data_columns, service= "sync", "Backfill batch processed"); - BatchProcessResult::Success { - sent_blocks, - imported_blocks, - } - } - (_, Err(e)) => { - debug!( - batch_epoch = %epoch, - first_block_slot = start_slot, - last_block_slot = end_slot, - processed_blobs = n_blobs, - error = %e.message, - service = "sync", - "Backfill batch processing failed" - ); - match e.peer_action { - Some(penalty) => BatchProcessResult::FaultyFailure { - imported_blocks: 0, - penalty, - }, - None => BatchProcessResult::NonFaultyFailure, - } - } + BatchProcessResult::Success { + sent_blocks, + imported_blocks, + } + } + (_, Err(e)) => { + debug!( + batch_epoch = %epoch, + first_block_slot = start_slot, + last_block_slot = end_slot, + processed_blobs = n_blobs, + error = %e.message, + service = "sync", + "Backfill batch processing failed" + ); + match e.peer_action { + Some(penalty) => BatchProcessResult::FaultyFailure { + imported_blocks: 0, + penalty, + }, + None => BatchProcessResult::NonFaultyFailure, } } }; - self.send_sync_message(SyncMessage::BatchProcessed { sync_type, result }); + self.send_sync_message(SyncMessage::BatchProcessed { + sync_type: process_id, + result, + }); } /// Helper function to process blocks batches which only consumes the chain and blocks to process. + #[instrument(skip_all)] async fn process_blocks<'a>( &self, downloaded_blocks: impl Iterator>, @@ -559,15 +698,6 @@ impl NetworkBeaconProcessor { metrics::inc_counter(&metrics::BEACON_PROCESSOR_CHAIN_SEGMENT_SUCCESS_TOTAL); if !imported_blocks.is_empty() { self.chain.recompute_head_at_current_slot().await; - - for (block_root, block_slot) in &imported_blocks { - if self.chain.should_sample_slot(*block_slot) { - self.send_sync_message(SyncMessage::SampleBlock( - *block_root, - *block_slot, - )); - } - } } (imported_blocks.len(), Ok(())) } @@ -586,6 +716,7 @@ impl NetworkBeaconProcessor { } /// Helper function to process backfill block batches which only consumes the chain and blocks to process. + #[instrument(skip_all)] fn process_backfill_blocks( &self, downloaded_blocks: Vec>, @@ -620,7 +751,7 @@ impl NetworkBeaconProcessor { peer_action: Some(PeerAction::LowToleranceError), message: format!("Failed to check block availability : {:?}", e), }), - ) + ); } }, }; @@ -673,6 +804,16 @@ impl NetworkBeaconProcessor { // The peer is faulty if they bad signatures. Some(PeerAction::LowToleranceError) } + HistoricalBlockError::MissingOldestBlockRoot { slot } => { + warn!( + %slot, + error = "missing_oldest_block_root", + "Backfill batch processing error" + ); + // This is an internal error, do not penalize the peer. + None + } + HistoricalBlockError::ValidatorPubkeyCacheTimeout => { warn!( error = "pubkey_cache_timeout", diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 292e894870..ed04fe7bb9 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -6,35 +6,45 @@ use crate::{ ChainSegmentProcessId, DuplicateCache, InvalidBlockStorage, NetworkBeaconProcessor, }, service::NetworkMessage, - sync::{manager::BlockProcessType, SyncMessage}, + sync::{SyncMessage, manager::BlockProcessType}, }; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::custody_context::NodeCustodyType; +use beacon_chain::data_column_verification::validate_data_column_sidecar_for_gossip; use beacon_chain::kzg_utils::blobs_to_data_column_sidecars; +use beacon_chain::observed_data_sidecars::DoNotObserve; use beacon_chain::test_utils::{ - get_kzg, test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, - EphemeralHarnessType, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, get_kzg, + test_spec, }; use beacon_chain::{BeaconChain, WhenSlotSkipped}; use beacon_processor::{work_reprocessing_queue::*, *}; +use gossipsub::MessageAcceptance; use itertools::Itertools; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, MetaDataV3}; use lighthouse_network::rpc::InboundRequestId; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, MetaDataV3, +}; use lighthouse_network::{ + Client, MessageId, NetworkConfig, NetworkGlobals, PeerId, Response, discv5::enr::{self, CombinedKey}, rpc::methods::{MetaData, MetaDataV2}, types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}, - Client, MessageId, NetworkConfig, NetworkGlobals, PeerId, Response, }; +use matches::assert_matches; use slot_clock::SlotClock; +use ssz_types::RuntimeVariableList; +use std::collections::HashSet; use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use types::blob_sidecar::FixedBlobSidecarList; +use types::blob_sidecar::{BlobIdentifier, FixedBlobSidecarList}; use types::{ - Attestation, AttesterSlashing, BlobSidecar, BlobSidecarList, DataColumnSidecarList, - DataColumnSubnetId, Epoch, Hash256, MainnetEthSpec, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedVoluntaryExit, Slot, SubnetId, + AttesterSlashing, BlobSidecar, BlobSidecarList, ChainSpec, DataColumnSidecarList, + DataColumnSubnetId, Epoch, EthSpec, Hash256, MainnetEthSpec, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedVoluntaryExit, SingleAttestation, Slot, + SubnetId, }; type E = MainnetEthSpec; @@ -56,16 +66,16 @@ struct TestRig { next_block: Arc>, next_blobs: Option>, next_data_columns: Option>, - attestations: Vec<(Attestation, SubnetId)>, - next_block_attestations: Vec<(Attestation, SubnetId)>, + attestations: Vec<(SingleAttestation, SubnetId)>, + next_block_attestations: Vec<(SingleAttestation, SubnetId)>, next_block_aggregate_attestations: Vec>, attester_slashing: AttesterSlashing, proposer_slashing: ProposerSlashing, voluntary_exit: SignedVoluntaryExit, beacon_processor_tx: BeaconProcessorSend, work_journal_rx: mpsc::Receiver<&'static str>, - _network_rx: mpsc::UnboundedReceiver>, - _sync_rx: mpsc::UnboundedReceiver>, + network_rx: mpsc::UnboundedReceiver>, + sync_rx: mpsc::UnboundedReceiver>, duplicate_cache: DuplicateCache, network_beacon_processor: Arc>, _harness: BeaconChainHarness, @@ -83,24 +93,44 @@ impl Drop for TestRig { impl TestRig { pub async fn new(chain_length: u64) -> Self { + // This allows for testing voluntary exits without building out a massive chain. + let mut spec = test_spec::(); + spec.shard_committee_period = 2; Self::new_parametric( chain_length, - BeaconProcessorConfig::default().enable_backfill_rate_limiting, + BeaconProcessorConfig::default(), + NodeCustodyType::Fullnode, + spec, ) .await } - pub async fn new_parametric(chain_length: u64, enable_backfill_rate_limiting: bool) -> Self { + pub async fn new_supernode(chain_length: u64) -> Self { // This allows for testing voluntary exits without building out a massive chain. let mut spec = test_spec::(); spec.shard_committee_period = 2; - let spec = Arc::new(spec); + Self::new_parametric( + chain_length, + BeaconProcessorConfig::default(), + NodeCustodyType::Supernode, + spec, + ) + .await + } + pub async fn new_parametric( + chain_length: u64, + beacon_processor_config: BeaconProcessorConfig, + node_custody_type: NodeCustodyType, + spec: ChainSpec, + ) -> Self { + let spec = Arc::new(spec); let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() + .node_custody_type(node_custody_type) .chain_config(<_>::default()) .build(); @@ -126,13 +156,21 @@ impl TestRig { "precondition: current slot is one after head" ); + // Ensure there is a blob in the next block. Required for some tests. + harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .execution_block_generator() + .set_min_blob_count(1); let (next_block_tuple, next_state) = harness .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap()) .await; let head_state_root = head.beacon_state_root(); let attestations = harness - .get_unaggregated_attestations( + .get_single_attestations( &AttestationStrategy::AllValidators, &head.beacon_state, head_state_root, @@ -149,7 +187,7 @@ impl TestRig { ); let next_block_attestations = harness - .get_unaggregated_attestations( + .get_single_attestations( &AttestationStrategy::AllValidators, &next_state, next_block_tuple.0.state_root(), @@ -183,20 +221,14 @@ impl TestRig { let chain = harness.chain.clone(); - let (network_tx, _network_rx) = mpsc::unbounded_channel(); + let (network_tx, network_rx) = mpsc::unbounded_channel(); - let beacon_processor_config = BeaconProcessorConfig { - enable_backfill_rate_limiting, - ..Default::default() - }; let BeaconProcessorChannels { beacon_processor_tx, beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx, } = BeaconProcessorChannels::new(&beacon_processor_config); - let (sync_tx, _sync_rx) = mpsc::unbounded_channel(); + let (sync_tx, sync_rx) = mpsc::unbounded_channel(); // Default metadata let meta_data = if spec.is_peer_das_scheduled() { @@ -237,7 +269,6 @@ impl TestRig { chain: harness.chain.clone(), network_tx, sync_tx, - reprocess_tx: work_reprocessing_tx.clone(), network_globals: network_globals.clone(), invalid_block_storage: InvalidBlockStorage::Disabled, executor: executor.clone(), @@ -252,8 +283,6 @@ impl TestRig { } .spawn_manager( beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx, Some(work_journal_tx), harness.chain.slot_clock.clone(), chain.spec.maximum_gossip_clock_disparity(), @@ -269,6 +298,8 @@ impl TestRig { let (blob_sidecars, data_columns) = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let kzg = get_kzg(&chain.spec); + let epoch = block.slot().epoch(E::slots_per_epoch()); + let sampling_indices = chain.sampling_columns_for_epoch(epoch); let custody_columns: DataColumnSidecarList = blobs_to_data_column_sidecars( &blobs.iter().collect_vec(), kzg_proofs.clone().into_iter().collect_vec(), @@ -278,7 +309,7 @@ impl TestRig { ) .unwrap() .into_iter() - .filter(|c| network_globals.sampling_columns.contains(&c.index)) + .filter(|c| sampling_indices.contains(&c.index)) .collect::>(); (None, Some(custody_columns)) @@ -304,8 +335,8 @@ impl TestRig { voluntary_exit, beacon_processor_tx, work_journal_rx, - _network_rx, - _sync_rx, + network_rx, + sync_rx, duplicate_cache, network_beacon_processor, _harness: harness, @@ -355,7 +386,6 @@ impl TestRig { .send_gossip_data_column_sidecar( junk_message_id(), junk_peer_id(), - Client::default(), DataColumnSubnetId::from_column_index(data_column.index, &self.chain.spec), data_column.clone(), Duration::from_secs(0), @@ -364,22 +394,12 @@ impl TestRig { } } - pub fn custody_columns_count(&self) -> usize { - self.network_beacon_processor - .network_globals - .custody_columns_count() as usize - } - pub fn enqueue_rpc_block(&self) { let block_root = self.next_block.canonical_root(); self.network_beacon_processor .send_rpc_beacon_block( block_root, - RpcBlock::new_without_blobs( - Some(block_root), - self.next_block.clone(), - self.custody_columns_count(), - ), + RpcBlock::new_without_blobs(Some(block_root), self.next_block.clone()), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 0 }, ) @@ -391,11 +411,7 @@ impl TestRig { self.network_beacon_processor .send_rpc_beacon_block( block_root, - RpcBlock::new_without_blobs( - Some(block_root), - self.next_block.clone(), - self.custody_columns_count(), - ), + RpcBlock::new_without_blobs(Some(block_root), self.next_block.clone()), std::time::Duration::default(), BlockProcessType::SingleBlock { id: 1 }, ) @@ -429,23 +445,44 @@ impl TestRig { } } - pub fn enqueue_blobs_by_range_request(&self, count: u64) { + pub fn enqueue_blobs_by_range_request(&self, start_slot: u64, count: u64) { self.network_beacon_processor .send_blobs_by_range_request( PeerId::random(), InboundRequestId::new_unchecked(42, 24), - BlobsByRangeRequest { + BlobsByRangeRequest { start_slot, count }, + ) + .unwrap(); + } + + pub fn enqueue_blobs_by_root_request(&self, blob_ids: RuntimeVariableList) { + self.network_beacon_processor + .send_blobs_by_roots_request( + PeerId::random(), + InboundRequestId::new_unchecked(42, 24), + BlobsByRootRequest { blob_ids }, + ) + .unwrap(); + } + + pub fn enqueue_data_columns_by_range_request(&self, count: u64, columns: Vec) { + self.network_beacon_processor + .send_data_columns_by_range_request( + PeerId::random(), + InboundRequestId::new_unchecked(42, 24), + DataColumnsByRangeRequest { start_slot: 0, count, + columns, }, ) .unwrap(); } - pub fn enqueue_backfill_batch(&self) { + pub fn enqueue_backfill_batch(&self, epoch: Epoch) { self.network_beacon_processor .send_chain_segment( - ChainSegmentProcessId::BackSyncBatchId(Epoch::default()), + ChainSegmentProcessId::BackSyncBatchId(epoch), Vec::default(), ) .unwrap(); @@ -590,10 +627,46 @@ impl TestRig { } pub async fn assert_event_journal(&mut self, expected: &[&str]) { - self.assert_event_journal_with_timeout(expected, STANDARD_TIMEOUT) + self.assert_event_journal_with_timeout(expected, STANDARD_TIMEOUT, false, false) .await } + pub async fn assert_event_journal_completes_with_timeout( + &mut self, + expected: &[WorkType], + timeout: Duration, + ) { + self.assert_event_journal_with_timeout( + &expected + .iter() + .map(Into::<&'static str>::into) + .chain(std::iter::once(WORKER_FREED)) + .chain(std::iter::once(NOTHING_TO_DO)) + .collect::>(), + timeout, + false, + false, + ) + .await + } + + pub async fn assert_event_journal_does_not_complete_with_timeout( + &mut self, + expected: &[WorkType], + timeout: Duration, + ) { + self.assert_not_in_event_journal_with_timeout( + &expected + .iter() + .map(Into::<&'static str>::into) + .chain(std::iter::once(WORKER_FREED)) + .chain(std::iter::once(NOTHING_TO_DO)) + .collect::>(), + timeout, + ) + .await + } + pub async fn assert_event_journal_completes(&mut self, expected: &[WorkType]) { self.assert_event_journal( &expected @@ -616,11 +689,21 @@ impl TestRig { &mut self, expected: &[&str], timeout: Duration, + ignore_worker_freed: bool, + ignore_nothing_to_do: bool, ) { let mut events = Vec::with_capacity(expected.len()); let drain_future = async { while let Some(event) = self.work_journal_rx.recv().await { + if event == WORKER_FREED && ignore_worker_freed { + continue; + } + + if event == NOTHING_TO_DO && ignore_nothing_to_do { + continue; + } + events.push(event); // Break as soon as we collect the desired number of events. @@ -643,6 +726,120 @@ impl TestRig { assert_eq!(events, expected); } + + /// Assert that the `BeaconProcessor` event journal is not as `expected`. + pub async fn assert_not_in_event_journal_with_timeout( + &mut self, + expected: &[&str], + timeout: Duration, + ) { + let mut events = Vec::with_capacity(expected.len()); + + let drain_future = async { + while let Some(event) = self.work_journal_rx.recv().await { + events.push(event); + + // Break as soon as we collect the desired number of events. + if events.len() >= expected.len() { + break; + } + } + }; + + // Panic if we don't time out. + tokio::select! { + _ = tokio::time::sleep(timeout) => {}, + _ = drain_future => panic!( + "Got events before timeout. Expected no events but got {:?}", + events + ), + } + + assert_ne!(events, expected); + } + + /// Listen for network messages and collect them for a specified duration or until reaching a count. + /// + /// Returns None if no messages were received, or Some(Vec) containing the received messages. + /// + /// # Arguments + /// + /// * `timeout` - Maximum duration to listen for messages + /// * `count` - Optional maximum number of messages to collect before returning + pub async fn receive_network_messages_with_timeout( + &mut self, + timeout: Duration, + count: Option, + ) -> Option>> { + let mut events = vec![]; + + let timeout_future = tokio::time::sleep(timeout); + tokio::pin!(timeout_future); + + loop { + // Break if we've received the requested count of messages + if let Some(target_count) = count + && events.len() >= target_count + { + break; + } + + tokio::select! { + _ = &mut timeout_future => break, + maybe_msg = self.network_rx.recv() => { + match maybe_msg { + Some(msg) => events.push(msg), + None => break, // Channel closed + } + } + } + } + + if events.is_empty() { + None + } else { + Some(events) + } + } + + /// Listen for sync messages and collect them for a specified duration or until reaching a count. + /// + /// Returns None if no messages were received, or Some(Vec) containing the received messages. + pub async fn receive_sync_messages_with_timeout( + &mut self, + timeout: Duration, + count: Option, + ) -> Option>> { + let mut events = vec![]; + + let timeout_future = tokio::time::sleep(timeout); + tokio::pin!(timeout_future); + + loop { + // Break if we've received the requested count of messages + if let Some(target_count) = count + && events.len() >= target_count + { + break; + } + + tokio::select! { + _ = &mut timeout_future => break, + maybe_msg = self.sync_rx.recv() => { + match maybe_msg { + Some(msg) => events.push(msg), + None => break, // Channel closed + } + } + } + } + + if events.is_empty() { + None + } else { + Some(events) + } + } } fn junk_peer_id() -> PeerId { @@ -653,6 +850,152 @@ fn junk_message_id() -> MessageId { MessageId::new(&[]) } +// Test that column reconstruction is delayed for columns that arrive +// at the beginning of the slot. +#[tokio::test] +async fn data_column_reconstruction_at_slot_start() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new_supernode(SMALL_CHAIN).await; + + let slot_start = rig + .chain + .slot_clock + .start_of(rig.next_block.slot()) + .unwrap(); + + rig.chain + .slot_clock + .set_current_time(slot_start - rig.chain.spec.maximum_gossip_clock_disparity()); + + assert_eq!( + rig.chain.slot().unwrap(), + rig.next_block.slot() - 1, + "chain should be at the correct slot" + ); + + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + + if num_data_columns > 0 { + // Reconstruction is delayed by 100ms, we should not be able to complete + // reconstruction up to this point + rig.assert_event_journal_does_not_complete_with_timeout( + &[WorkType::ColumnReconstruction], + Duration::from_millis(100), + ) + .await; + + // We've waited at least 150ms, reconstruction can now be triggered + rig.assert_event_journal_completes_with_timeout( + &[WorkType::ColumnReconstruction], + Duration::from_millis(200), + ) + .await; + } +} + +// Test that column reconstruction happens immediately for columns that arrive at the +// reconstruction deadline. +#[tokio::test] +async fn data_column_reconstruction_at_deadline() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new_supernode(SMALL_CHAIN).await; + + let slot_start = rig + .chain + .slot_clock + .start_of(rig.next_block.slot()) + .unwrap(); + + // We push the slot clock to 3 seconds into the slot, this is the deadline to trigger reconstruction. + let slot_duration = rig.chain.slot_clock.slot_duration().as_millis() as u64; + let reconstruction_deadline_millis = + (slot_duration * RECONSTRUCTION_DEADLINE.0) / RECONSTRUCTION_DEADLINE.1; + rig.chain + .slot_clock + .set_current_time(slot_start + Duration::from_millis(reconstruction_deadline_millis)); + + let min_columns_for_reconstruction = E::number_of_columns() / 2; + for i in 0..min_columns_for_reconstruction { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + + // Since we're at the reconstruction deadline, reconstruction should be triggered immediately + rig.assert_event_journal_with_timeout( + &[WorkType::ColumnReconstruction.into()], + Duration::from_millis(50), + false, + false, + ) + .await; +} + +// Test the column reconstruction is delayed for columns that arrive for a previous slot. +#[tokio::test] +async fn data_column_reconstruction_at_next_slot() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new_supernode(SMALL_CHAIN).await; + + let slot_start = rig + .chain + .slot_clock + .start_of(rig.next_block.slot()) + .unwrap(); + + rig.chain + .slot_clock + .set_current_time(slot_start - rig.chain.spec.maximum_gossip_clock_disparity()); + + assert_eq!( + rig.chain.slot().unwrap(), + rig.next_block.slot() - 1, + "chain should be at the correct slot" + ); + + // We push the slot clock to the next slot. + rig.chain + .slot_clock + .set_current_time(slot_start + Duration::from_secs(12)); + + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + + if num_data_columns > 0 { + // Since we are in the next slot reconstruction for the previous slot should be delayed again + rig.assert_event_journal_does_not_complete_with_timeout( + &[WorkType::ColumnReconstruction], + Duration::from_millis(100), + ) + .await; + + // We've waited at least 150ms, reconstruction can now be triggered + rig.assert_event_journal_completes_with_timeout( + &[WorkType::ColumnReconstruction], + Duration::from_millis(200), + ) + .await; + } +} + /// Blocks that arrive early should be queued for later processing. #[tokio::test] async fn import_gossip_block_acceptably_early() { @@ -753,6 +1096,61 @@ async fn import_gossip_block_unacceptably_early() { ); } +/// Data columns that have already been processed but unobserved should be propagated without re-importing. +#[tokio::test] +async fn accept_processed_gossip_data_columns_without_import() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new(SMALL_CHAIN).await; + + // GIVEN the data columns have already been processed but unobserved. + // 1. verify data column with `DoNotObserve` to create verified but unobserved data columns. + // 2. put verified but unobserved data columns into the data availability cache. + let verified_data_columns: Vec<_> = rig + .next_data_columns + .clone() + .unwrap() + .into_iter() + .map(|data_column| { + let subnet_id = + DataColumnSubnetId::from_column_index(data_column.index, &rig.chain.spec); + validate_data_column_sidecar_for_gossip::<_, DoNotObserve>( + data_column, + subnet_id, + &rig.chain, + ) + .expect("should be valid data column") + }) + .collect(); + + let block_root = rig.next_block.canonical_root(); + rig.chain + .data_availability_checker + .put_gossip_verified_data_columns(block_root, rig.next_block.slot(), verified_data_columns) + .expect("should put data columns into availability cache"); + + // WHEN an already processed but unobserved data column is received via gossip + rig.enqueue_gossip_data_columns(0); + + // THEN the data column should be propagated without re-importing (not sure if there's an easy way to test this) + let network_message = rig + .receive_network_messages_with_timeout(Duration::from_millis(100), Some(1)) + .await + .and_then(|mut vec| vec.pop()) + .expect("should receive network messages"); + + assert_matches!( + network_message, + NetworkMessage::ValidationResult { + propagation_source: _, + message_id: _, + validation_result: MessageAcceptance::Accept, + } + ); +} + /// Blocks that arrive on-time should be processed normally. #[tokio::test] async fn import_gossip_block_at_current_slot() { @@ -1012,6 +1410,8 @@ async fn requeue_unknown_block_gossip_attestation_without_import() { NOTHING_TO_DO, ], Duration::from_secs(1) + QUEUED_ATTESTATION_DELAY, + false, + false, ) .await; @@ -1052,6 +1452,8 @@ async fn requeue_unknown_block_gossip_aggregated_attestation_without_import() { NOTHING_TO_DO, ], Duration::from_secs(1) + QUEUED_ATTESTATION_DELAY, + false, + false, ) .await; @@ -1157,11 +1559,25 @@ async fn test_rpc_block_reprocessing() { tokio::time::sleep(QUEUED_RPC_BLOCK_DELAY).await; rig.assert_event_journal(&[WorkType::RpcBlock.into()]).await; - // Add an extra delay for block processing - tokio::time::sleep(Duration::from_millis(10)).await; - // head should update to next block now since the duplicate - // cache handle was dropped. - assert_eq!(next_block_root, rig.head_root()); + + let max_retries = 3; + let mut success = false; + for _ in 0..max_retries { + // Add an extra delay for block processing + tokio::time::sleep(Duration::from_millis(10)).await; + // head should update to the next block now since the duplicate + // cache handle was dropped. + if next_block_root == rig.head_root() { + success = true; + break; + } + } + assert!( + success, + "expected head_root to be {:?} but was {:?}", + next_block_root, + rig.head_root() + ); } /// Ensure that backfill batches get rate-limited and processing is scheduled at specified intervals. @@ -1172,8 +1588,8 @@ async fn test_backfill_sync_processing() { // (not straight forward to manipulate `TestingSlotClock` due to cloning of `SlotClock` in code) // and makes the test very slow, hence timing calculation is unit tested separately in // `work_reprocessing_queue`. - for _ in 0..1 { - rig.enqueue_backfill_batch(); + for i in 0..1 { + rig.enqueue_backfill_batch(Epoch::new(i)); // ensure queued batch is not processed until later rig.assert_no_events_for(Duration::from_millis(100)).await; // A new batch should be processed within a slot. @@ -1184,6 +1600,8 @@ async fn test_backfill_sync_processing() { NOTHING_TO_DO, ], rig.chain.slot_clock.slot_duration(), + false, + false, ) .await; } @@ -1192,11 +1610,20 @@ async fn test_backfill_sync_processing() { /// Ensure that backfill batches get processed as fast as they can when rate-limiting is disabled. #[tokio::test] async fn test_backfill_sync_processing_rate_limiting_disabled() { - let enable_backfill_rate_limiting = false; - let mut rig = TestRig::new_parametric(SMALL_CHAIN, enable_backfill_rate_limiting).await; + let beacon_processor_config = BeaconProcessorConfig { + enable_backfill_rate_limiting: false, + ..Default::default() + }; + let mut rig = TestRig::new_parametric( + SMALL_CHAIN, + beacon_processor_config, + NodeCustodyType::Fullnode, + test_spec::(), + ) + .await; - for _ in 0..3 { - rig.enqueue_backfill_batch(); + for i in 0..3 { + rig.enqueue_backfill_batch(Epoch::new(i)); } // ensure all batches are processed @@ -1207,6 +1634,8 @@ async fn test_backfill_sync_processing_rate_limiting_disabled() { WorkType::ChainSegmentBackfill.into(), ], Duration::from_millis(100), + true, + true, ) .await; } @@ -1217,8 +1646,9 @@ async fn test_blobs_by_range() { return; }; let mut rig = TestRig::new(64).await; + let start_slot = 0; let slot_count = 32; - rig.enqueue_blobs_by_range_request(slot_count); + rig.enqueue_blobs_by_range_request(start_slot, slot_count); let mut blob_count = 0; for slot in 0..slot_count { @@ -1236,7 +1666,73 @@ async fn test_blobs_by_range() { .unwrap_or(0); } let mut actual_count = 0; - while let Some(next) = rig._network_rx.recv().await { + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::BlobsByRange(blob), + inbound_request_id: _, + } = next + { + if blob.is_some() { + actual_count += 1; + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + if test_spec::().fulu_fork_epoch.is_some() { + assert_eq!(0, actual_count, "Post-Fulu should return 0 blobs"); + } else { + assert_eq!(blob_count, actual_count); + } +} + +#[tokio::test] +async fn test_blobs_by_range_spans_fulu_fork() { + // Only test for Electra & Fulu fork transition + if test_spec::().electra_fork_epoch.is_none() { + return; + }; + let mut spec = test_spec::(); + spec.fulu_fork_epoch = Some(Epoch::new(1)); + spec.gloas_fork_epoch = Some(Epoch::new(2)); + + // This test focuses on Electra→Fulu blob counts (epoch 0 to 1). Build 62 blocks since no need for Gloas activation at slot 64. + let mut rig = TestRig::new_parametric( + 62, + BeaconProcessorConfig::default(), + NodeCustodyType::Fullnode, + spec, + ) + .await; + + let start_slot = 16; + // This will span from epoch 0 (Electra) to epoch 1 (Fulu) + let slot_count = 32; + + rig.enqueue_blobs_by_range_request(start_slot, slot_count); + + let mut blob_count = 0; + for slot in start_slot..slot_count { + let root = rig + .chain + .block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None) + .unwrap(); + blob_count += root + .map(|root| { + rig.chain + .get_blobs(&root) + .map(|list| list.len()) + .unwrap_or(0) + }) + .unwrap_or(0); + } + + let mut actual_count = 0; + + while let Some(next) = rig.network_rx.recv().await { if let NetworkMessage::SendResponse { peer_id: _, response: Response::BlobsByRange(blob), @@ -1254,3 +1750,225 @@ async fn test_blobs_by_range() { } assert_eq!(blob_count, actual_count); } + +#[tokio::test] +async fn test_blobs_by_root() { + if test_spec::().deneb_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new(64).await; + + // Get the block root of a sample slot, e.g., slot 1 + let block_root = rig + .chain + .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) + .unwrap() + .unwrap(); + + let blobs = rig.chain.get_blobs(&block_root).unwrap(); + let blob_count = blobs.len(); + + let blob_ids: Vec = (0..blob_count) + .map(|index| BlobIdentifier { + block_root, + index: index as u64, + }) + .collect(); + + let blob_ids_list = RuntimeVariableList::new(blob_ids, blob_count).unwrap(); + + rig.enqueue_blobs_by_root_request(blob_ids_list); + + let mut blob_count = 0; + let root = rig + .chain + .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) + .unwrap(); + blob_count += root + .map(|root| { + rig.chain + .get_blobs(&root) + .map(|list| list.len()) + .unwrap_or(0) + }) + .unwrap_or(0); + + let mut actual_count = 0; + + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::BlobsByRoot(blob), + inbound_request_id: _, + } = next + { + if blob.is_some() { + actual_count += 1; + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + assert_eq!(blob_count, actual_count); +} + +#[tokio::test] +async fn test_blobs_by_root_post_fulu_should_return_empty() { + // Only test for Fulu fork + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new(64).await; + + let block_root = rig + .chain + .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) + .unwrap() + .unwrap(); + + let blob_ids = vec![BlobIdentifier { + block_root, + index: 0, + }]; + + let blob_ids_list = RuntimeVariableList::new(blob_ids, 1).unwrap(); + + rig.enqueue_blobs_by_root_request(blob_ids_list); + + let mut actual_count = 0; + + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::BlobsByRoot(blob), + inbound_request_id: _, + } = next + { + if blob.is_some() { + actual_count += 1; + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + // Post-Fulu should return 0 blobs + assert_eq!(0, actual_count); +} + +/// Ensure that data column processing that results in block import sends a sync notification +#[tokio::test] +async fn test_data_column_import_notifies_sync() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + } + + let mut rig = TestRig::new(SMALL_CHAIN).await; + let block_root = rig.next_block.canonical_root(); + + // Enqueue the block first to prepare for data column processing + rig.enqueue_gossip_block(); + rig.assert_event_journal_completes(&[WorkType::GossipBlock]) + .await; + rig.receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) + .await + .expect("should receive sync message"); + + // Enqueue data columns which should trigger block import when complete + let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); + if num_data_columns > 0 { + for i in 0..num_data_columns { + rig.enqueue_gossip_data_columns(i); + rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) + .await; + } + + // Verify block import succeeded + assert_eq!( + rig.head_root(), + block_root, + "block should be imported and become head" + ); + + // Check that sync was notified of the successful import + let sync_messages = rig + .receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) + .await + .expect("should receive sync message"); + + // Verify we received the expected GossipBlockProcessResult message + assert_eq!( + sync_messages.len(), + 1, + "should receive exactly one sync message" + ); + match &sync_messages[0] { + SyncMessage::GossipBlockProcessResult { + block_root: msg_block_root, + imported, + } => { + assert_eq!(*msg_block_root, block_root, "block root should match"); + assert!(*imported, "block should be marked as imported"); + } + other => panic!("expected GossipBlockProcessResult, got {:?}", other), + } + } +} + +#[tokio::test] +async fn test_data_columns_by_range_request_only_returns_requested_columns() { + if test_spec::().fulu_fork_epoch.is_none() { + return; + }; + + let mut rig = TestRig::new(64).await; + let slot_count = 4; + + let all_custody_columns = rig + .chain + .sampling_columns_for_epoch(rig.chain.epoch().unwrap()); + let available_columns: Vec = all_custody_columns.to_vec(); + + let requested_columns = vec![available_columns[0], available_columns[2]]; + + rig.enqueue_data_columns_by_range_request(slot_count, requested_columns.clone()); + + let mut received_columns = Vec::new(); + + while let Some(next) = rig.network_rx.recv().await { + if let NetworkMessage::SendResponse { + peer_id: _, + response: Response::DataColumnsByRange(data_column), + inbound_request_id: _, + } = next + { + if let Some(column) = data_column { + received_columns.push(column.index); + } else { + break; + } + } else { + panic!("unexpected message {:?}", next); + } + } + + for received_index in &received_columns { + assert!( + requested_columns.contains(received_index), + "Received column index {} was not in requested columns {:?}", + received_index, + requested_columns + ); + } + + let unique_received: HashSet<_> = received_columns.into_iter().collect(); + assert!( + !unique_received.is_empty(), + "Should have received at least some data columns" + ); +} diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 9c112dba86..113b3cdd32 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -70,8 +70,8 @@ impl StoreItem for PersistedDht { mod tests { use super::*; use std::str::FromStr; - use store::config::StoreConfig; use store::MemoryStore; + use store::config::StoreConfig; use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { @@ -86,5 +86,9 @@ mod tests { .unwrap(); let dht: PersistedDht = store.get_item(&DHT_DB_KEY).unwrap().unwrap(); assert_eq!(dht.enrs, enrs); + + // This hardcoded length check is for database schema compatibility. If the on-disk format + // of `PersistedDht` changes, we need a DB schema change. + assert_eq!(dht.as_store_bytes().len(), 136); } } diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index fa50876e0d..f8b2d61ce6 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -10,22 +10,20 @@ use crate::service::NetworkMessage; use crate::status::status_message; use crate::sync::SyncMessage; use beacon_chain::{BeaconChain, BeaconChainTypes}; -use beacon_processor::{ - work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend, DuplicateCache, -}; +use beacon_processor::{BeaconProcessorSend, DuplicateCache}; use futures::prelude::*; use lighthouse_network::rpc::*; use lighthouse_network::{ - service::api_types::{AppRequestId, SyncRequestId}, MessageId, NetworkGlobals, PeerId, PubsubMessage, Response, + service::api_types::{AppRequestId, SyncRequestId}, }; -use logging::crit; use logging::TimeLatch; +use logging::crit; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use tracing::{debug, error, info_span, trace, warn, Instrument}; +use tracing::{debug, error, trace, warn}; use types::{BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, SignedBeaconBlock}; /// Handles messages from the network and routes them to the appropriate service to be handled. @@ -87,7 +85,6 @@ impl Router { executor: task_executor::TaskExecutor, invalid_block_storage: InvalidBlockStorage, beacon_processor_send: BeaconProcessorSend, - beacon_processor_reprocess_tx: mpsc::Sender, fork_context: Arc, ) -> Result>, String> { trace!("Service starting"); @@ -103,7 +100,6 @@ impl Router { chain: beacon_chain.clone(), network_tx: network_send.clone(), sync_tx: sync_send.clone(), - reprocess_tx: beacon_processor_reprocess_tx, network_globals: network_globals.clone(), invalid_block_storage, executor: executor.clone(), @@ -136,7 +132,6 @@ impl Router { debug!("Network message router started"); UnboundedReceiverStream::new(handler_recv) .for_each(move |msg| future::ready(handler.handle_message(msg))) - .instrument(info_span!("", service = "router")) .await; }, "router", @@ -191,11 +186,11 @@ impl Router { /* RPC - Related functionality */ /// A new RPC request has been received from the network. - fn handle_rpc_request( + fn handle_rpc_request( &mut self, peer_id: PeerId, inbound_request_id: InboundRequestId, // Use ResponseId here - request_type: RequestType, + request_type: RequestType, ) { if !self.network_globals.peers.read().is_connected(&peer_id) { debug!(%peer_id, request = ?request_type, "Dropping request of disconnected peer"); @@ -354,17 +349,6 @@ impl Router { timestamp_now(), ), ), - PubsubMessage::SingleAttestation(subnet_attestation) => self - .handle_beacon_processor_send_result( - self.network_beacon_processor.send_single_attestation( - message_id, - peer_id, - subnet_attestation.1, - subnet_attestation.0, - should_process, - timestamp_now(), - ), - ), PubsubMessage::BeaconBlock(block) => self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_beacon_block( message_id, @@ -394,7 +378,6 @@ impl Router { .send_gossip_data_column_sidecar( message_id, peer_id, - self.network_globals.client(&peer_id), subnet_id, column_sidecar, timestamp_now(), diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 77204b455d..0869b442ae 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,29 +1,32 @@ +use crate::NetworkConfig; use crate::metrics; use crate::nat; use crate::network_beacon_processor::InvalidBlockStorage; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; use crate::subnet_service::{SubnetService, SubnetServiceMessage, Subscription}; -use crate::NetworkConfig; use beacon_chain::{BeaconChain, BeaconChainTypes}; -use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; +use beacon_processor::BeaconProcessorSend; use futures::channel::mpsc::Sender; use futures::future::OptionFuture; use futures::prelude::*; + +use lighthouse_network::Enr; +use lighthouse_network::identity::Keypair; use lighthouse_network::rpc::InboundRequestId; use lighthouse_network::rpc::RequestType; +use lighthouse_network::rpc::methods::RpcResponse; use lighthouse_network::service::Network; use lighthouse_network::types::GossipKind; -use lighthouse_network::Enr; -use lighthouse_network::{prometheus_client::registry::Registry, MessageAcceptance}; use lighthouse_network::{ - rpc::{GoodbyeReason, RpcErrorResponse}, Context, PeerAction, PubsubMessage, ReportSource, Response, Subnet, + rpc::{GoodbyeReason, RpcErrorResponse}, }; +use lighthouse_network::{MessageAcceptance, prometheus_client::registry::Registry}; use lighthouse_network::{ - service::api_types::AppRequestId, - types::{core_topics_to_subscribe, GossipEncoding, GossipTopic}, MessageId, NetworkEvent, NetworkGlobals, PeerId, + service::api_types::AppRequestId, + types::{GossipEncoding, GossipTopic, core_topics_to_subscribe}, }; use logging::crit; use std::collections::BTreeSet; @@ -33,10 +36,11 @@ use strum::IntoStaticStr; use task_executor::ShutdownReason; use tokio::sync::mpsc; use tokio::time::Sleep; -use tracing::{debug, error, info, info_span, trace, warn, Instrument}; +use tracing::{debug, error, info, trace, warn}; +use typenum::Unsigned; use types::{ - ChainSpec, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, - Unsigned, ValidatorSubscription, + EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, + ValidatorSubscription, }; mod tests; @@ -105,6 +109,12 @@ pub enum NetworkMessage { ConnectTrustedPeer(Enr), /// Disconnect from a trusted peer and remove it from the `trusted_peers` mapping. DisconnectTrustedPeer(Enr), + /// Custody group count changed due to a change in validators' weight. + /// Subscribe to new subnets and update ENR metadata. + CustodyCountChanged { + new_custody_group_count: u64, + sampling_count: u64, + }, } /// Messages triggered by validators that may trigger a subscription to a subnet. @@ -179,11 +189,11 @@ pub struct NetworkService { store: Arc>, /// A collection of global variables, accessible outside of the network service. network_globals: Arc>, - /// A delay that expires when a new fork takes place. - next_fork_update: Pin>>, - /// A delay that expires when we need to subscribe to a new fork's topics. - next_fork_subscriptions: Pin>>, - /// A delay that expires when we need to unsubscribe from old fork topics. + /// A delay that expires when the fork digest changes. + next_digest_update: Pin>>, + /// A delay that expires when we need to subscribe to a new set of topics. + next_topic_subscriptions: Pin>>, + /// A delay that expires when we need to unsubscribe from old topics. next_unsubscribe: Pin>>, /// Shutdown beacon node after sync is complete. shutdown_after_sync: bool, @@ -204,7 +214,7 @@ impl NetworkService { executor: task_executor::TaskExecutor, libp2p_registry: Option<&'_ mut Registry>, beacon_processor_send: BeaconProcessorSend, - beacon_processor_reprocess_tx: mpsc::Sender, + local_keypair: Keypair, ) -> Result< ( NetworkService, @@ -243,8 +253,10 @@ impl NetworkService { let enr_fork_id = beacon_chain.enr_fork_id(); // keep track of when our fork_id needs to be updated - let next_fork_update = Box::pin(next_fork_delay(&beacon_chain).into()); - let next_fork_subscriptions = Box::pin(next_fork_subscriptions_delay(&beacon_chain).into()); + let next_digest_update = Box::pin(next_digest_delay(&beacon_chain).into()); + // topics change when the fork digest changes + let next_topic_subscriptions = + Box::pin(next_topic_subscriptions_delay(&beacon_chain).into()); let next_unsubscribe = Box::pin(None.into()); let current_slot = beacon_chain @@ -258,8 +270,6 @@ impl NetworkService { &beacon_chain.spec, )); - debug!(fork_name = ?fork_context.current_fork(), "Current fork"); - // construct the libp2p service context let service_context = Context { config: config.clone(), @@ -270,7 +280,16 @@ impl NetworkService { }; // launch libp2p service - let (mut libp2p, network_globals) = Network::new(executor.clone(), service_context).await?; + let (mut libp2p, network_globals) = Network::new( + executor.clone(), + service_context, + beacon_chain + .data_availability_checker + .custody_context() + .custody_group_count_at_head(&beacon_chain.spec), + local_keypair, + ) + .await?; // Repopulate the DHT with stored ENR's if discovery is not disabled. if !config.disable_discovery { @@ -300,7 +319,6 @@ impl NetworkService { executor.clone(), invalid_block_storage, beacon_processor_send, - beacon_processor_reprocess_tx, fork_context.clone(), )?; @@ -332,8 +350,8 @@ impl NetworkService { router_send, store, network_globals: network_globals.clone(), - next_fork_update, - next_fork_subscriptions, + next_digest_update, + next_topic_subscriptions, next_unsubscribe, shutdown_after_sync: config.shutdown_after_sync, metrics_enabled: config.metrics_enabled, @@ -352,7 +370,7 @@ impl NetworkService { executor: task_executor::TaskExecutor, libp2p_registry: Option<&'_ mut Registry>, beacon_processor_send: BeaconProcessorSend, - beacon_processor_reprocess_tx: mpsc::Sender, + local_keypair: Keypair, ) -> Result<(Arc>, NetworkSenders), String> { let (network_service, network_globals, network_senders) = Self::build( beacon_chain, @@ -360,7 +378,7 @@ impl NetworkService { executor.clone(), libp2p_registry, beacon_processor_send, - beacon_processor_reprocess_tx, + local_keypair, ) .await?; @@ -377,30 +395,16 @@ impl NetworkService { let fork_context = &self.fork_context; let spec = &self.beacon_chain.spec; let current_slot = self.beacon_chain.slot().unwrap_or(spec.genesis_slot); - let current_fork = fork_context.current_fork(); + let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - let mut result = vec![fork_context - .to_context_bytes(current_fork) - .unwrap_or_else(|| { - panic!( - "{} fork bytes should exist as it's initialized in ForkContext", - current_fork - ) - })]; + let mut result = vec![fork_context.context_bytes(current_epoch)]; - if let Some((next_fork, fork_epoch)) = spec.next_fork_epoch::(current_slot) { - if current_slot.saturating_add(Slot::new(SUBSCRIBE_DELAY_SLOTS)) - >= fork_epoch.start_slot(T::EthSpec::slots_per_epoch()) - { - let next_fork_context_bytes = - fork_context.to_context_bytes(next_fork).unwrap_or_else(|| { - panic!( - "context bytes should exist as spec.next_fork_epoch({}) returned Some({})", - current_slot, next_fork - ) - }); - result.push(next_fork_context_bytes); - } + if let Some(next_digest_epoch) = spec.next_digest_epoch(current_epoch) + && current_slot.saturating_add(Slot::new(SUBSCRIBE_DELAY_SLOTS)) + >= next_digest_epoch.start_slot(T::EthSpec::slots_per_epoch()) + { + let next_digest = fork_context.context_bytes(next_digest_epoch); + result.push(next_digest); } result @@ -442,7 +446,7 @@ impl NetworkService { event = self.libp2p.next_event() => self.on_libp2p_event(event, &mut shutdown_sender).await, - Some(_) = &mut self.next_fork_update => self.update_next_fork(), + Some(_) = &mut self.next_digest_update => self.update_next_fork_digest(), Some(_) = &mut self.next_unsubscribe => { let new_enr_fork_id = self.beacon_chain.enr_fork_id(); @@ -451,13 +455,13 @@ impl NetworkService { self.next_unsubscribe = Box::pin(None.into()); } - Some(_) = &mut self.next_fork_subscriptions => { - if let Some((fork_name, _)) = self.beacon_chain.duration_to_next_fork() { - let fork_version = self.beacon_chain.spec.fork_version_for_name(fork_name); - let fork_digest = ChainSpec::compute_fork_digest(fork_version, self.beacon_chain.genesis_validators_root); + Some(_) = &mut self.next_topic_subscriptions => { + if let Some((epoch, _)) = self.beacon_chain.duration_to_next_digest() { + let fork_name = self.beacon_chain.spec.fork_name_at_epoch(epoch); + let fork_digest = self.beacon_chain.compute_fork_digest(epoch); info!("Subscribing to new fork topics"); self.libp2p.subscribe_new_fork_topics(fork_name, fork_digest); - self.next_fork_subscriptions = Box::pin(None.into()); + self.next_topic_subscriptions = Box::pin(None.into()); } else { error!( "Fork subscription scheduled but no fork scheduled"); @@ -465,7 +469,7 @@ impl NetworkService { } } } - }.instrument(info_span!("", service = "network")); + }; executor.spawn(service_fut, "network"); } @@ -539,23 +543,7 @@ impl NetworkService { // the attestation, else we just just propagate the Attestation. let should_process = self.subnet_service.should_process_attestation( Subnet::Attestation(subnet_id), - attestation.data(), - ); - self.send_to_router(RouterMessage::PubsubMessage( - id, - source, - message, - should_process, - )); - } - PubsubMessage::SingleAttestation(ref subnet_and_attestation) => { - let subnet_id = subnet_and_attestation.0; - let single_attestation = &subnet_and_attestation.1; - // checks if we have an aggregator for the slot. If so, we should process - // the attestation, else we just just propagate the Attestation. - let should_process = self.subnet_service.should_process_attestation( - Subnet::Attestation(subnet_id), - &single_attestation.data, + &attestation.data, ); self.send_to_router(RouterMessage::PubsubMessage( id, @@ -632,10 +620,11 @@ impl NetworkService { error, inbound_request_id, reason, - } => { - self.libp2p - .send_error_response(peer_id, inbound_request_id, error, reason); - } + } => self.libp2p.send_response( + peer_id, + inbound_request_id, + RpcResponse::Error(error, reason.into()), + ), NetworkMessage::ValidationResult { propagation_source, message_id, @@ -705,7 +694,7 @@ impl NetworkService { let mut subscribed_topics: Vec = vec![]; for topic_kind in core_topics_to_subscribe::( - self.fork_context.current_fork(), + self.fork_context.current_fork_name(), &self.network_globals.as_topic_config(), &self.fork_context.spec, ) { @@ -745,6 +734,22 @@ impl NetworkService { ); } } + NetworkMessage::CustodyCountChanged { + new_custody_group_count, + sampling_count, + } => { + // subscribe to `sampling_count` subnets + self.libp2p + .subscribe_new_data_column_subnets(sampling_count); + if self + .network_globals + .config + .advertise_false_custody_group_count + .is_none() + { + self.libp2p.update_enr_cgc(new_custody_group_count); + } + } } } @@ -817,31 +822,53 @@ impl NetworkService { } } - fn update_next_fork(&mut self) { + fn update_next_fork_digest(&mut self) { let new_enr_fork_id = self.beacon_chain.enr_fork_id(); + // if we are unable to read the slot clock we assume that it is prior to genesis + let current_epoch = self.beacon_chain.epoch().unwrap_or( + self.beacon_chain + .spec + .genesis_slot + .epoch(T::EthSpec::slots_per_epoch()), + ); let new_fork_digest = new_enr_fork_id.fork_digest; let fork_context = &self.fork_context; - if let Some(new_fork_name) = fork_context.from_context_bytes(new_fork_digest) { - info!( - old_fork = ?fork_context.current_fork(), - new_fork = ?new_fork_name, - "Transitioned to new fork" - ); - fork_context.update_current_fork(*new_fork_name); + if let Some(new_fork_name) = fork_context.get_fork_from_context_bytes(new_fork_digest) { + if fork_context.current_fork_name() == *new_fork_name { + info!( + epoch = ?current_epoch, + "BPO Fork Triggered" + ) + } else { + info!( + old_fork = ?fork_context.current_fork_name(), + new_fork = ?new_fork_name, + "Transitioned to new fork" + ); + new_fork_name.fork_ascii(); + } + + fork_context.update_current_fork(*new_fork_name, new_fork_digest, current_epoch); + if self.beacon_chain.spec.is_peer_das_scheduled() { + let next_fork_digest = fork_context + .next_fork_digest() + .unwrap_or_else(|| fork_context.current_fork_digest()); + self.libp2p.update_nfd(next_fork_digest); + } self.libp2p.update_fork_version(new_enr_fork_id); // Reinitialize the next_fork_update - self.next_fork_update = Box::pin(next_fork_delay(&self.beacon_chain).into()); + self.next_digest_update = Box::pin(next_digest_delay(&self.beacon_chain).into()); // Set the next_unsubscribe delay. let epoch_duration = self.beacon_chain.spec.seconds_per_slot * T::EthSpec::slots_per_epoch(); let unsubscribe_delay = Duration::from_secs(UNSUBSCRIBE_DELAY_EPOCHS * epoch_duration); - // Update the `next_fork_subscriptions` timer if the next fork is known. - self.next_fork_subscriptions = - Box::pin(next_fork_subscriptions_delay(&self.beacon_chain).into()); + // Update the `next_topic_subscriptions` timer if the next change in the fork digest is known. + self.next_topic_subscriptions = + Box::pin(next_topic_subscriptions_delay(&self.beacon_chain).into()); self.next_unsubscribe = Box::pin(Some(tokio::time::sleep(unsubscribe_delay)).into()); info!( remaining_epochs = UNSUBSCRIBE_DELAY_EPOCHS, @@ -858,7 +885,7 @@ impl NetworkService { fn subscribed_core_topics(&self) -> bool { let core_topics = core_topics_to_subscribe::( - self.fork_context.current_fork(), + self.fork_context.current_fork_name(), &self.network_globals.as_topic_config(), &self.fork_context.spec, ); @@ -871,23 +898,23 @@ impl NetworkService { } } -/// Returns a `Sleep` that triggers after the next change in the beacon chain fork version. +/// Returns a `Sleep` that triggers after the next change in the fork digest. /// If there is no scheduled fork, `None` is returned. -fn next_fork_delay( +fn next_digest_delay( beacon_chain: &BeaconChain, ) -> Option { beacon_chain - .duration_to_next_fork() - .map(|(_, until_fork)| tokio::time::sleep(until_fork)) + .duration_to_next_digest() + .map(|(_, until_epoch)| tokio::time::sleep(until_epoch)) } -/// Returns a `Sleep` that triggers `SUBSCRIBE_DELAY_SLOTS` before the next fork. +/// Returns a `Sleep` that triggers `SUBSCRIBE_DELAY_SLOTS` before the next fork digest changes. /// Returns `None` if there are no scheduled forks or we are already past `current_slot + SUBSCRIBE_DELAY_SLOTS > fork_slot`. -fn next_fork_subscriptions_delay( +fn next_topic_subscriptions_delay( beacon_chain: &BeaconChain, ) -> Option { - if let Some((_, duration_to_fork)) = beacon_chain.duration_to_next_fork() { - let duration_to_subscription = duration_to_fork.saturating_sub(Duration::from_secs( + if let Some((_, duration_to_epoch)) = beacon_chain.duration_to_next_digest() { + let duration_to_subscription = duration_to_epoch.saturating_sub(Duration::from_secs( beacon_chain.spec.seconds_per_slot * SUBSCRIBE_DELAY_SLOTS, )); if !duration_to_subscription.is_zero() { diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index 15c3321e94..8ff1e0488d 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -2,16 +2,17 @@ #![cfg(test)] use crate::persisted_dht::load_dht; use crate::{NetworkConfig, NetworkService}; -use beacon_chain::test_utils::BeaconChainHarness; use beacon_chain::BeaconChainTypes; +use beacon_chain::test_utils::BeaconChainHarness; use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; use futures::StreamExt; +use lighthouse_network::identity::secp256k1; use lighthouse_network::types::{GossipEncoding, GossipKind}; use lighthouse_network::{Enr, GossipTopic}; use std::str::FromStr; use std::sync::Arc; use tokio::runtime::Runtime; -use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; +use types::{Epoch, EthSpec, MinimalEthSpec, SubnetId}; impl NetworkService { fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { @@ -58,8 +59,6 @@ fn test_dht_persistence() { let BeaconProcessorChannels { beacon_processor_tx, beacon_processor_rx: _beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx: _work_reprocessing_rx, } = <_>::default(); let _network_service = NetworkService::start( @@ -68,7 +67,7 @@ fn test_dht_persistence() { executor, None, beacon_processor_tx, - work_reprocessing_tx, + secp256k1::Keypair::generate().into(), ) .await .unwrap(); @@ -109,8 +108,8 @@ fn test_removing_topic_weight_on_old_topics() { .mock_execution_layer() .build() .chain; - let (next_fork_name, _) = beacon_chain.duration_to_next_fork().expect("next fork"); - assert_eq!(next_fork_name, ForkName::Capella); + let (next_fork_epoch, _) = beacon_chain.duration_to_next_digest().expect("next fork"); + assert_eq!(Some(next_fork_epoch), spec.capella_fork_epoch); // Build network service. let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { @@ -137,7 +136,7 @@ fn test_removing_topic_weight_on_old_topics() { executor.clone(), None, beacon_processor_channels.beacon_processor_tx, - beacon_processor_channels.work_reprocessing_tx, + secp256k1::Keypair::generate().into(), ) .await .unwrap() @@ -193,9 +192,8 @@ fn test_removing_topic_weight_on_old_topics() { beacon_chain.slot_clock.advance_slot(); } - // Run `NetworkService::update_next_fork()`. runtime.block_on(async { - network_service.update_next_fork(); + network_service.update_next_fork_digest(); }); // Check that topic_weight on the old topics has been zeroed. diff --git a/beacon_node/network/src/status.rs b/beacon_node/network/src/status.rs index 1210926d34..c571a40485 100644 --- a/beacon_node/network/src/status.rs +++ b/beacon_node/network/src/status.rs @@ -1,7 +1,8 @@ use beacon_chain::{BeaconChain, BeaconChainTypes}; -use types::{EthSpec, FixedBytesExtended, Hash256}; +use fixed_bytes::FixedBytesExtended; +use types::{EthSpec, Hash256}; -use lighthouse_network::rpc::StatusMessage; +use lighthouse_network::rpc::{StatusMessage, methods::StatusMessageV2}; /// Trait to produce a `StatusMessage` representing the state of the given `beacon_chain`. /// /// NOTE: The purpose of this is simply to obtain a `StatusMessage` from the `BeaconChain` without @@ -29,11 +30,28 @@ pub(crate) fn status_message(beacon_chain: &BeaconChain) finalized_checkpoint.root = Hash256::zero(); } - StatusMessage { + // NOTE: We are making an assumption that `get_data_column_custody_info` wont fail. + let earliest_available_data_column_slot = beacon_chain + .store + .get_data_column_custody_info() + .ok() + .flatten() + .and_then(|info| info.earliest_data_column_slot); + + // If data_column_custody_info.earliest_data_column_slot is `None`, + // no recent cgc changes have occurred and no cgc backfill is in progress. + let earliest_available_slot = + if let Some(earliest_available_data_column_slot) = earliest_available_data_column_slot { + earliest_available_data_column_slot + } else { + beacon_chain.store.get_anchor_info().oldest_block_slot + }; + StatusMessage::V2(StatusMessageV2 { fork_digest, finalized_root: finalized_checkpoint.root, finalized_epoch: finalized_checkpoint.epoch, head_root: cached_head.head_block_root(), head_slot: cached_head.head_slot(), - } + earliest_available_slot, + }) } diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs deleted file mode 100644 index dd4724b261..0000000000 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ /dev/null @@ -1,681 +0,0 @@ -//! This service keeps track of which shard subnet the beacon node should be subscribed to at any -//! given time. It schedules subscriptions to shard subnets, requests peer discoveries and -//! determines whether attestations should be aggregated and/or passed to the beacon node. - -use super::SubnetServiceMessage; -use std::collections::HashSet; -use std::collections::{HashMap, VecDeque}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; - -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use delay_map::{HashMapDelay, HashSetDelay}; -use futures::prelude::*; -use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; -use slot_clock::SlotClock; -use tracing::{debug, error, info, trace, warn}; -use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; - -use crate::metrics; - -/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the -/// slot is less than this number, skip the peer discovery process. -/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. -pub(crate) const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; -/// The fraction of a slot that we subscribe to a subnet before the required slot. -/// -/// Currently a whole slot ahead. -const ADVANCE_SUBSCRIBE_SLOT_FRACTION: u32 = 1; - -/// The number of slots after an aggregator duty where we remove the entry from -/// `aggregate_validators_on_subnet` delay map. -const UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY: u32 = 2; - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub(crate) enum SubscriptionKind { - /// Long lived subscriptions. - /// - /// These have a longer duration and are advertised in our ENR. - LongLived, - /// Short lived subscriptions. - /// - /// Subscribing to these subnets has a short duration and we don't advertise it in our ENR. - ShortLived, -} - -/// A particular subnet at a given slot. -#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)] -pub struct ExactSubnet { - /// The `SubnetId` associated with this subnet. - pub subnet_id: SubnetId, - /// The `Slot` associated with this subnet. - pub slot: Slot, -} - -pub struct AttestationService { - /// Queued events to return to the driving service. - events: VecDeque, - - /// A reference to the beacon chain to process received attestations. - pub(crate) beacon_chain: Arc>, - - /// Subnets we are currently subscribed to as short lived subscriptions. - /// - /// Once they expire, we unsubscribe from these. - /// We subscribe to subnets when we are an aggregator for an exact subnet. - short_lived_subscriptions: HashMapDelay, - - /// Subnets we are currently subscribed to as long lived subscriptions. - /// - /// We advertise these in our ENR. When these expire, the subnet is removed from our ENR. - /// These are required of all beacon nodes. The exact number is determined by the chain - /// specification. - long_lived_subscriptions: HashSet, - - /// Short lived subscriptions that need to be executed in the future. - scheduled_short_lived_subscriptions: HashSetDelay, - - /// A collection timeouts to track the existence of aggregate validator subscriptions at an - /// `ExactSubnet`. - aggregate_validators_on_subnet: Option>, - - /// The waker for the current thread. - waker: Option, - - /// The discovery mechanism of lighthouse is disabled. - discovery_disabled: bool, - - /// We are always subscribed to all subnets. - subscribe_all_subnets: bool, - - /// Our Discv5 node_id. - node_id: NodeId, - - /// Future used to manage subscribing and unsubscribing from long lived subnets. - next_long_lived_subscription_event: Pin>, - - /// Whether this node is a block proposer-only node. - proposer_only: bool, -} - -impl AttestationService { - /* Public functions */ - - /// Establish the service based on the passed configuration. - pub fn new(beacon_chain: Arc>, node_id: NodeId, config: &NetworkConfig) -> Self { - let slot_duration = beacon_chain.slot_clock.slot_duration(); - - if config.subscribe_all_subnets { - info!("Subscribing to all subnets"); - } else { - info!( - subnets_per_node = beacon_chain.spec.subnets_per_node, - subscription_duration_in_epochs = beacon_chain.spec.epochs_per_subnet_subscription, - "Deterministic long lived subnets enabled" - ); - } - - let track_validators = !config.import_all_attestations; - let aggregate_validators_on_subnet = - track_validators.then(|| HashSetDelay::new(slot_duration)); - let mut service = AttestationService { - events: VecDeque::with_capacity(10), - beacon_chain, - short_lived_subscriptions: HashMapDelay::new(slot_duration), - long_lived_subscriptions: HashSet::default(), - scheduled_short_lived_subscriptions: HashSetDelay::default(), - aggregate_validators_on_subnet, - waker: None, - discovery_disabled: config.disable_discovery, - subscribe_all_subnets: config.subscribe_all_subnets, - node_id, - next_long_lived_subscription_event: { - // Set a dummy sleep. Calculating the current subnet subscriptions will update this - // value with a smarter timing - Box::pin(tokio::time::sleep(Duration::from_secs(1))) - }, - proposer_only: config.proposer_only, - }; - - // If we are not subscribed to all subnets, handle the deterministic set of subnets - if !config.subscribe_all_subnets { - service.recompute_long_lived_subnets(); - } - - service - } - - /// Return count of all currently subscribed subnets (long-lived **and** short-lived). - #[cfg(test)] - pub fn subscription_count(&self) -> usize { - if self.subscribe_all_subnets { - self.beacon_chain.spec.attestation_subnet_count as usize - } else { - let count = self - .short_lived_subscriptions - .keys() - .chain(self.long_lived_subscriptions.iter()) - .collect::>() - .len(); - count - } - } - - /// Returns whether we are subscribed to a subnet for testing purposes. - #[cfg(test)] - pub(crate) fn is_subscribed( - &self, - subnet_id: &SubnetId, - subscription_kind: SubscriptionKind, - ) -> bool { - match subscription_kind { - SubscriptionKind::LongLived => self.long_lived_subscriptions.contains(subnet_id), - SubscriptionKind::ShortLived => self.short_lived_subscriptions.contains_key(subnet_id), - } - } - - #[cfg(test)] - pub(crate) fn long_lived_subscriptions(&self) -> &HashSet { - &self.long_lived_subscriptions - } - - /// Processes a list of validator subscriptions. - /// - /// This will: - /// - Register new validators as being known. - /// - Search for peers for required subnets. - /// - Request subscriptions for subnets on specific slots when required. - /// - Build the timeouts for each of these events. - /// - /// This returns a result simply for the ergonomics of using ?. The result can be - /// safely dropped. - pub fn validator_subscriptions( - &mut self, - subscriptions: impl Iterator, - ) -> Result<(), String> { - // If the node is in a proposer-only state, we ignore all subnet subscriptions. - if self.proposer_only { - return Ok(()); - } - - // Maps each subnet_id subscription to it's highest slot - let mut subnets_to_discover: HashMap = HashMap::new(); - - // Registers the validator with the attestation service. - for subscription in subscriptions { - metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_REQUESTS); - - trace!(?subscription, "Validator subscription"); - - // Compute the subnet that is associated with this subscription - let subnet_id = match SubnetId::compute_subnet::( - subscription.slot, - subscription.attestation_committee_index, - subscription.committee_count_at_slot, - &self.beacon_chain.spec, - ) { - Ok(subnet_id) => subnet_id, - Err(e) => { - warn!( - error = ?e, - "Failed to compute subnet id for validator subscription" - ); - continue; - } - }; - // Ensure each subnet_id inserted into the map has the highest slot as it's value. - // Higher slot corresponds to higher min_ttl in the `SubnetDiscovery` entry. - if let Some(slot) = subnets_to_discover.get(&subnet_id) { - if subscription.slot > *slot { - subnets_to_discover.insert(subnet_id, subscription.slot); - } - } else if !self.discovery_disabled { - subnets_to_discover.insert(subnet_id, subscription.slot); - } - - let exact_subnet = ExactSubnet { - subnet_id, - slot: subscription.slot, - }; - - // Determine if the validator is an aggregator. If so, we subscribe to the subnet and - // if successful add the validator to a mapping of known aggregators for that exact - // subnet. - - if subscription.is_aggregator { - metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); - if let Err(e) = self.subscribe_to_short_lived_subnet(exact_subnet) { - warn!(error = e, "Subscription to subnet error"); - } else { - trace!(?exact_subnet, "Subscribed to subnet for aggregator duties"); - } - } - } - - // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the - // required subnets. - if !self.discovery_disabled { - if let Err(e) = self.discover_peers_request( - subnets_to_discover - .into_iter() - .map(|(subnet_id, slot)| ExactSubnet { subnet_id, slot }), - ) { - warn!(error = e, "Discovery lookup request error"); - }; - } - - Ok(()) - } - - fn recompute_long_lived_subnets(&mut self) { - // Ensure the next computation is scheduled even if assigning subnets fails. - let next_subscription_event = self - .recompute_long_lived_subnets_inner() - .unwrap_or_else(|_| self.beacon_chain.slot_clock.slot_duration()); - - debug!("Recomputing deterministic long lived subnets"); - self.next_long_lived_subscription_event = - Box::pin(tokio::time::sleep(next_subscription_event)); - - if let Some(waker) = self.waker.as_ref() { - waker.wake_by_ref(); - } - } - - /// Gets the long lived subnets the node should be subscribed to during the current epoch and - /// the remaining duration for which they remain valid. - fn recompute_long_lived_subnets_inner(&mut self) -> Result { - let current_epoch = self.beacon_chain.epoch().map_err(|e| { - if !self - .beacon_chain - .slot_clock - .is_prior_to_genesis() - .unwrap_or(false) - { - error!(err = ?e,"Failed to get the current epoch from clock") - } - })?; - - let (subnets, next_subscription_epoch) = SubnetId::compute_subnets_for_epoch::( - self.node_id.raw(), - current_epoch, - &self.beacon_chain.spec, - ) - .map_err(|e| error!(err = e, "Could not compute subnets for current epoch"))?; - - let next_subscription_slot = - next_subscription_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let next_subscription_event = self - .beacon_chain - .slot_clock - .duration_to_slot(next_subscription_slot) - .ok_or_else(|| { - error!("Failed to compute duration to next to long lived subscription event") - })?; - - self.update_long_lived_subnets(subnets.collect()); - - Ok(next_subscription_event) - } - - /// Updates the long lived subnets. - /// - /// New subnets are registered as subscribed, removed subnets as unsubscribed and the Enr - /// updated accordingly. - fn update_long_lived_subnets(&mut self, mut subnets: HashSet) { - info!(subnets = ?subnets.iter().collect::>(),"Subscribing to long-lived subnets"); - for subnet in &subnets { - // Add the events for those subnets that are new as long lived subscriptions. - if !self.long_lived_subscriptions.contains(subnet) { - // Check if this subnet is new and send the subscription event if needed. - if !self.short_lived_subscriptions.contains_key(subnet) { - debug!( - ?subnet, - subscription_kind = ?SubscriptionKind::LongLived, - "Subscribing to subnet" - ); - self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( - *subnet, - ))); - } - self.queue_event(SubnetServiceMessage::EnrAdd(Subnet::Attestation(*subnet))); - if !self.discovery_disabled { - self.queue_event(SubnetServiceMessage::DiscoverPeers(vec![SubnetDiscovery { - subnet: Subnet::Attestation(*subnet), - min_ttl: None, - }])) - } - } - } - - // Update the long_lived_subnets set and check for subnets that are being removed - std::mem::swap(&mut self.long_lived_subscriptions, &mut subnets); - for subnet in subnets { - if !self.long_lived_subscriptions.contains(&subnet) { - self.handle_removed_subnet(subnet, SubscriptionKind::LongLived); - } - } - } - - /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip - /// verification, re-propagates and returns false. - pub fn should_process_attestation( - &self, - subnet: SubnetId, - attestation: &Attestation, - ) -> bool { - // Proposer-only mode does not need to process attestations - if self.proposer_only { - return false; - } - self.aggregate_validators_on_subnet - .as_ref() - .map(|tracked_vals| { - tracked_vals.contains_key(&ExactSubnet { - subnet_id: subnet, - slot: attestation.data().slot, - }) - }) - .unwrap_or(true) - } - - /* Internal private functions */ - - /// Adds an event to the event queue and notifies that this service is ready to be polled - /// again. - fn queue_event(&mut self, ev: SubnetServiceMessage) { - self.events.push_back(ev); - if let Some(waker) = &self.waker { - waker.wake_by_ref() - } - } - /// Checks if there are currently queued discovery requests and the time required to make the - /// request. - /// - /// If there is sufficient time, queues a peer discovery request for all the required subnets. - fn discover_peers_request( - &mut self, - exact_subnets: impl Iterator, - ) -> Result<(), &'static str> { - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let discovery_subnets: Vec = exact_subnets - .filter_map(|exact_subnet| { - // Check if there is enough time to perform a discovery lookup. - if exact_subnet.slot - >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) - { - // Send out an event to start looking for peers. - // Require the peer for an additional slot to ensure we keep the peer for the - // duration of the subscription. - let min_ttl = self - .beacon_chain - .slot_clock - .duration_to_slot(exact_subnet.slot + 1) - .map(|duration| std::time::Instant::now() + duration); - Some(SubnetDiscovery { - subnet: Subnet::Attestation(exact_subnet.subnet_id), - min_ttl, - }) - } else { - // We may want to check the global PeerInfo to see estimated timeouts for each - // peer before they can be removed. - warn!( - subnet_id = ?exact_subnet, - "Not enough time for a discovery search" - ); - None - } - }) - .collect(); - - if !discovery_subnets.is_empty() { - self.queue_event(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); - } - Ok(()) - } - - // Subscribes to the subnet if it should be done immediately, or schedules it if required. - fn subscribe_to_short_lived_subnet( - &mut self, - ExactSubnet { subnet_id, slot }: ExactSubnet, - ) -> Result<(), &'static str> { - let slot_duration = self.beacon_chain.slot_clock.slot_duration(); - - // The short time we schedule the subscription before it's actually required. This - // ensures we are subscribed on time, and allows consecutive subscriptions to the same - // subnet to overlap, reducing subnet churn. - let advance_subscription_duration = slot_duration / ADVANCE_SUBSCRIBE_SLOT_FRACTION; - // The time to the required slot. - let time_to_subscription_slot = self - .beacon_chain - .slot_clock - .duration_to_slot(slot) - .unwrap_or_default(); // If this is a past slot we will just get a 0 duration. - - // Calculate how long before we need to subscribe to the subnet. - let time_to_subscription_start = - time_to_subscription_slot.saturating_sub(advance_subscription_duration); - - // The time after a duty slot where we no longer need it in the `aggregate_validators_on_subnet` - // delay map. - let time_to_unsubscribe = - time_to_subscription_slot + UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY * slot_duration; - if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { - tracked_vals.insert_at(ExactSubnet { subnet_id, slot }, time_to_unsubscribe); - } - - // If the subscription should be done in the future, schedule it. Otherwise subscribe - // immediately. - if time_to_subscription_start.is_zero() { - // This is a current or past slot, we subscribe immediately. - self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1)?; - } else { - // This is a future slot, schedule subscribing. - trace!(subnet = ?subnet_id, ?time_to_subscription_start,"Scheduling subnet subscription"); - self.scheduled_short_lived_subscriptions - .insert_at(ExactSubnet { subnet_id, slot }, time_to_subscription_start); - } - - Ok(()) - } - - /* A collection of functions that handle the various timeouts */ - - /// Registers a subnet as subscribed. - /// - /// Checks that the time in which the subscription would end is not in the past. If we are - /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send - /// out the appropriate events. - /// - /// On determinist long lived subnets, this is only used for short lived subscriptions. - fn subscribe_to_short_lived_subnet_immediately( - &mut self, - subnet_id: SubnetId, - end_slot: Slot, - ) -> Result<(), &'static str> { - if self.subscribe_all_subnets { - // Case not handled by this service. - return Ok(()); - } - - let time_to_subscription_end = self - .beacon_chain - .slot_clock - .duration_to_slot(end_slot) - .unwrap_or_default(); - - // First check this is worth doing. - if time_to_subscription_end.is_zero() { - return Err("Time when subscription would end has already passed."); - } - - let subscription_kind = SubscriptionKind::ShortLived; - - // We need to check and add a subscription for the right kind, regardless of the presence - // of the subnet as a subscription of the other kind. This is mainly since long lived - // subscriptions can be removed at any time when a validator goes offline. - - let (subscriptions, already_subscribed_as_other_kind) = ( - &mut self.short_lived_subscriptions, - self.long_lived_subscriptions.contains(&subnet_id), - ); - - match subscriptions.get(&subnet_id) { - Some(current_end_slot) => { - // We are already subscribed. Check if we need to extend the subscription. - if &end_slot > current_end_slot { - trace!( - subnet = ?subnet_id, - prev_end_slot = %current_end_slot, - new_end_slot = %end_slot, - ?subscription_kind, - "Extending subscription to subnet" - ); - subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); - } - } - None => { - // This is a new subscription. Add with the corresponding timeout and send the - // notification. - subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); - - // Inform of the subscription. - if !already_subscribed_as_other_kind { - debug!( - subnet = ?subnet_id, - %end_slot, - ?subscription_kind, - "Subscribing to subnet" - ); - self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( - subnet_id, - ))); - } - } - } - - Ok(()) - } - - // Unsubscribes from a subnet that was removed if it does not continue to exist as a - // subscription of the other kind. For long lived subscriptions, it also removes the - // advertisement from our ENR. - fn handle_removed_subnet(&mut self, subnet_id: SubnetId, subscription_kind: SubscriptionKind) { - let exists_in_other_subscriptions = match subscription_kind { - SubscriptionKind::LongLived => self.short_lived_subscriptions.contains_key(&subnet_id), - SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains(&subnet_id), - }; - - if !exists_in_other_subscriptions { - // Subscription no longer exists as short lived or long lived. - debug!( - subnet = ?subnet_id, - ?subscription_kind, - "Unsubscribing from subnet" - ); - self.queue_event(SubnetServiceMessage::Unsubscribe(Subnet::Attestation( - subnet_id, - ))); - } - - if subscription_kind == SubscriptionKind::LongLived { - // Remove from our ENR even if we remain subscribed in other way. - self.queue_event(SubnetServiceMessage::EnrRemove(Subnet::Attestation( - subnet_id, - ))); - } - } -} - -impl Stream for AttestationService { - type Item = SubnetServiceMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // Update the waker if needed. - if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { - self.waker = Some(cx.waker().clone()); - } - } else { - self.waker = Some(cx.waker().clone()); - } - - // Send out any generated events. - if let Some(event) = self.events.pop_front() { - return Poll::Ready(Some(event)); - } - - // If we aren't subscribed to all subnets, handle the deterministic long-lived subnets - if !self.subscribe_all_subnets { - match self.next_long_lived_subscription_event.as_mut().poll(cx) { - Poll::Ready(_) => { - self.recompute_long_lived_subnets(); - // We re-wake the task as there could be other subscriptions to process - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Pending => {} - } - } - - // Process scheduled subscriptions that might be ready, since those can extend a soon to - // expire subscription. - match self.scheduled_short_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(ExactSubnet { subnet_id, slot }))) => { - if let Err(e) = - self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1) - { - debug!(subnet = ?subnet_id, err = e,"Failed to subscribe to short lived subnet"); - } - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Ready(Some(Err(e))) => { - error!( - error = e, - "Failed to check for scheduled subnet subscriptions" - ); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // Finally process any expired subscriptions. - match self.short_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { - self.handle_removed_subnet(subnet_id, SubscriptionKind::ShortLived); - // We re-wake the task as there could be other subscriptions to process - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Ready(Some(Err(e))) => { - error!(error = e, "Failed to check for subnet unsubscription times"); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // Poll to remove entries on expiration, no need to act on expiration events. - if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { - if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { - error!( - error = e, - "Failed to check for aggregate validator on subnet expirations" - ); - } - } - - Poll::Pending - } -} diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index 5340538e52..be491e56d3 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -13,9 +13,9 @@ use tokio::time::Instant; use beacon_chain::{BeaconChain, BeaconChainTypes}; use delay_map::HashSetDelay; use futures::prelude::*; -use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; +use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery, discv5::enr::NodeId}; use slot_clock::SlotClock; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{debug, error, info, warn}; use types::{ AttestationData, EthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, @@ -113,12 +113,6 @@ impl SubnetService { /* Public functions */ /// Establish the service based on the passed configuration. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] pub fn new(beacon_chain: Arc>, node_id: NodeId, config: &NetworkConfig) -> Self { let slot_duration = beacon_chain.slot_clock.slot_duration(); @@ -228,12 +222,6 @@ impl SubnetService { /// /// This returns a result simply for the ergonomics of using ?. The result can be /// safely dropped. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] pub fn validator_subscriptions(&mut self, subscriptions: impl Iterator) { // If the node is in a proposer-only state, we ignore all subnet subscriptions. if self.proposer_only { @@ -359,21 +347,15 @@ impl SubnetService { // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the // required subnets. - if !self.discovery_disabled { - if let Err(e) = self.discover_peers_request(subnets_to_discover.into_iter()) { - warn!(error = e, "Discovery lookup request error"); - }; - } + if !self.discovery_disabled + && let Err(e) = self.discover_peers_request(subnets_to_discover.into_iter()) + { + warn!(error = e, "Discovery lookup request error"); + }; } /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip /// verification, re-propagates and returns false. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] pub fn should_process_attestation( &self, subnet: Subnet, @@ -398,12 +380,6 @@ impl SubnetService { /// Adds an event to the event queue and notifies that this service is ready to be polled /// again. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn queue_event(&mut self, ev: SubnetServiceMessage) { self.events.push_back(ev); if let Some(waker) = &self.waker { @@ -415,11 +391,6 @@ impl SubnetService { /// /// If there is sufficient time, queues a peer discovery request for all the required subnets. // NOTE: Sending early subscriptions results in early searching for peers on subnets. - #[instrument(parent = None, - level = "info", - name = "subnet_service", - skip_all - )] fn discover_peers_request( &mut self, subnets_to_discover: impl Iterator, @@ -467,12 +438,6 @@ impl SubnetService { } // Subscribes to the subnet if it should be done immediately, or schedules it if required. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn subscribe_to_subnet( &mut self, ExactSubnet { subnet, slot }: ExactSubnet, @@ -525,12 +490,6 @@ impl SubnetService { } /// Adds a subscription event to the sync subnet. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn subscribe_to_sync_subnet( &mut self, subnet: Subnet, @@ -580,12 +539,6 @@ impl SubnetService { /// Checks that the time in which the subscription would end is not in the past. If we are /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send /// out the appropriate events. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn subscribe_to_subnet_immediately( &mut self, subnet: Subnet, @@ -641,12 +594,6 @@ impl SubnetService { } // Unsubscribes from a subnet that was removed. - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn handle_removed_subnet(&mut self, subnet: Subnet) { if !self.subscriptions.contains_key(&subnet) { // Subscription no longer exists as short lived subnet @@ -664,16 +611,10 @@ impl SubnetService { impl Stream for SubnetService { type Item = SubnetServiceMessage; - #[instrument(parent = None, - level = "info", - fields(service = "subnet_service"), - name = "subnet_service", - skip_all - )] fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // Update the waker if needed. if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { + if !waker.will_wake(cx.waker()) { self.waker = Some(cx.waker().clone()); } } else { @@ -730,13 +671,13 @@ impl Stream for SubnetService { } // Poll to remove entries on expiration, no need to act on expiration events. - if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { - if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { - error!( - error = e, - "Failed to check for aggregate validator on subnet expirations" - ); - } + if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() + && let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) + { + error!( + error = e, + "Failed to check for aggregate validator on subnet expirations" + ); } Poll::Pending diff --git a/beacon_node/network/src/subnet_service/sync_subnets.rs b/beacon_node/network/src/subnet_service/sync_subnets.rs deleted file mode 100644 index 59ec278a95..0000000000 --- a/beacon_node/network/src/subnet_service/sync_subnets.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! This service keeps track of which sync committee subnet the beacon node should be subscribed to at any -//! given time. It schedules subscriptions to sync committee subnets and requests peer discoveries. - -use std::collections::{hash_map::Entry, HashMap, VecDeque}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; - -use futures::prelude::*; -use tracing::{debug, error, trace, warn}; - -use super::SubnetServiceMessage; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use delay_map::HashSetDelay; -use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery}; -use slot_clock::SlotClock; -use types::{Epoch, EthSpec, SyncCommitteeSubscription, SyncSubnetId}; - -use crate::metrics; - -/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the -/// slot is less than this number, skip the peer discovery process. -/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. -const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; - -/// A particular subnet at a given slot. -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub struct ExactSubnet { - /// The `SyncSubnetId` associated with this subnet. - pub subnet_id: SyncSubnetId, - /// The epoch until which we need to stay subscribed to the subnet. - pub until_epoch: Epoch, -} -pub struct SyncCommitteeService { - /// Queued events to return to the driving service. - events: VecDeque, - - /// A reference to the beacon chain to process received attestations. - pub(crate) beacon_chain: Arc>, - - /// The collection of all currently subscribed subnets. - subscriptions: HashMap, - - /// A collection of timeouts for when to unsubscribe from a subnet. - unsubscriptions: HashSetDelay, - - /// The waker for the current thread. - waker: Option, - - /// The discovery mechanism of lighthouse is disabled. - discovery_disabled: bool, - - /// We are always subscribed to all subnets. - subscribe_all_subnets: bool, - - /// Whether this node is a block proposer-only node. - proposer_only: bool, -} - -impl SyncCommitteeService { - /* Public functions */ - - pub fn new(beacon_chain: Arc>, config: &NetworkConfig) -> Self { - let spec = &beacon_chain.spec; - let epoch_duration_secs = - beacon_chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); - let default_timeout = - epoch_duration_secs.saturating_mul(spec.epochs_per_sync_committee_period.as_u64()); - - SyncCommitteeService { - events: VecDeque::with_capacity(10), - beacon_chain, - subscriptions: HashMap::new(), - unsubscriptions: HashSetDelay::new(Duration::from_secs(default_timeout)), - waker: None, - subscribe_all_subnets: config.subscribe_all_subnets, - discovery_disabled: config.disable_discovery, - proposer_only: config.proposer_only, - } - } - - /// Return count of all currently subscribed subnets. - #[cfg(test)] - pub fn subscription_count(&self) -> usize { - use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; - if self.subscribe_all_subnets { - SYNC_COMMITTEE_SUBNET_COUNT as usize - } else { - self.subscriptions.len() - } - } - - /// Processes a list of sync committee subscriptions. - /// - /// This will: - /// - Search for peers for required subnets. - /// - Request subscriptions required subnets. - /// - Build the timeouts for each of these events. - /// - /// This returns a result simply for the ergonomics of using ?. The result can be - /// safely dropped. - pub fn validator_subscriptions( - &mut self, - subscriptions: Vec, - ) -> Result<(), String> { - // A proposer-only node does not subscribe to any sync-committees - if self.proposer_only { - return Ok(()); - } - - let mut subnets_to_discover = Vec::new(); - for subscription in subscriptions { - metrics::inc_counter(&metrics::SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS); - //NOTE: We assume all subscriptions have been verified before reaching this service - - // Registers the validator with the subnet service. - // This will subscribe to long-lived random subnets if required. - trace!(?subscription, "Sync committee subscription"); - - let subnet_ids = match SyncSubnetId::compute_subnets_for_sync_committee::( - &subscription.sync_committee_indices, - ) { - Ok(subnet_ids) => subnet_ids, - Err(e) => { - warn!( - error = ?e, - validator_index = subscription.validator_index, - "Failed to compute subnet id for sync committee subscription" - ); - continue; - } - }; - - for subnet_id in subnet_ids { - let exact_subnet = ExactSubnet { - subnet_id, - until_epoch: subscription.until_epoch, - }; - subnets_to_discover.push(exact_subnet.clone()); - if let Err(e) = self.subscribe_to_subnet(exact_subnet.clone()) { - warn!( - error = e, - validator_index = subscription.validator_index, - "Subscription to sync subnet error" - ); - } else { - trace!( - ?exact_subnet, - validator_index = subscription.validator_index, - "Subscribed to subnet for sync committee duties" - ); - } - } - } - // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the - // required subnets. - if !self.discovery_disabled { - if let Err(e) = self.discover_peers_request(subnets_to_discover.iter()) { - warn!(error = e, "Discovery lookup request error"); - }; - } - - // pre-emptively wake the thread to check for new events - if let Some(waker) = &self.waker { - waker.wake_by_ref(); - } - Ok(()) - } - - /* Internal private functions */ - - /// Checks if there are currently queued discovery requests and the time required to make the - /// request. - /// - /// If there is sufficient time, queues a peer discovery request for all the required subnets. - fn discover_peers_request<'a>( - &mut self, - exact_subnets: impl Iterator, - ) -> Result<(), &'static str> { - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - - let discovery_subnets: Vec = exact_subnets - .filter_map(|exact_subnet| { - let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); - // check if there is enough time to perform a discovery lookup - if until_slot >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) { - // if the slot is more than epoch away, add an event to start looking for peers - // add one slot to ensure we keep the peer for the subscription slot - let min_ttl = self - .beacon_chain - .slot_clock - .duration_to_slot(until_slot + 1) - .map(|duration| std::time::Instant::now() + duration); - Some(SubnetDiscovery { - subnet: Subnet::SyncCommittee(exact_subnet.subnet_id), - min_ttl, - }) - } else { - // We may want to check the global PeerInfo to see estimated timeouts for each - // peer before they can be removed. - warn!( - subnet_id = ?exact_subnet, - "Not enough time for a discovery search" - ); - None - } - }) - .collect(); - - if !discovery_subnets.is_empty() { - self.events - .push_back(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); - } - Ok(()) - } - - /// Adds a subscription event and an associated unsubscription event if required. - fn subscribe_to_subnet(&mut self, exact_subnet: ExactSubnet) -> Result<(), &'static str> { - // Return if we have subscribed to all subnets - if self.subscribe_all_subnets { - return Ok(()); - } - - // Return if we already have a subscription for exact_subnet - if self.subscriptions.get(&exact_subnet.subnet_id) == Some(&exact_subnet.until_epoch) { - return Ok(()); - } - - // Return if we already have subscription set to expire later than the current request. - if let Some(until_epoch) = self.subscriptions.get(&exact_subnet.subnet_id) { - if *until_epoch >= exact_subnet.until_epoch { - return Ok(()); - } - } - - // initialise timing variables - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); - // Calculate the duration to the unsubscription event. - let expected_end_subscription_duration = if current_slot >= until_slot { - warn!( - %current_slot, - ?exact_subnet, - "Sync committee subscription is past expiration" - ); - return Ok(()); - } else { - let slot_duration = self.beacon_chain.slot_clock.slot_duration(); - - // the duration until we no longer need this subscription. We assume a single slot is - // sufficient. - self.beacon_chain - .slot_clock - .duration_to_slot(until_slot) - .ok_or("Unable to determine duration to unsubscription slot")? - + slot_duration - }; - - if let Entry::Vacant(e) = self.subscriptions.entry(exact_subnet.subnet_id) { - // We are not currently subscribed and have no waiting subscription, create one - debug!(subnet = *exact_subnet.subnet_id, until_epoch = ?exact_subnet.until_epoch, "Subscribing to subnet"); - e.insert(exact_subnet.until_epoch); - self.events - .push_back(SubnetServiceMessage::Subscribe(Subnet::SyncCommittee( - exact_subnet.subnet_id, - ))); - - // add the subnet to the ENR bitfield - self.events - .push_back(SubnetServiceMessage::EnrAdd(Subnet::SyncCommittee( - exact_subnet.subnet_id, - ))); - - // add an unsubscription event to remove ourselves from the subnet once completed - self.unsubscriptions - .insert_at(exact_subnet.subnet_id, expected_end_subscription_duration); - } else { - // We are already subscribed, extend the unsubscription duration - self.unsubscriptions - .update_timeout(&exact_subnet.subnet_id, expected_end_subscription_duration); - } - - Ok(()) - } - - /// A queued unsubscription is ready. - fn handle_unsubscriptions(&mut self, subnet_id: SyncSubnetId) { - debug!(subnet = *subnet_id, "Unsubscribing from subnet"); - - self.subscriptions.remove(&subnet_id); - self.events - .push_back(SubnetServiceMessage::Unsubscribe(Subnet::SyncCommittee( - subnet_id, - ))); - - self.events - .push_back(SubnetServiceMessage::EnrRemove(Subnet::SyncCommittee( - subnet_id, - ))); - } -} - -impl Stream for SyncCommitteeService { - type Item = SubnetServiceMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // update the waker if needed - if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { - self.waker = Some(cx.waker().clone()); - } - } else { - self.waker = Some(cx.waker().clone()); - } - - // process any un-subscription events - match self.unsubscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(exact_subnet))) => self.handle_unsubscriptions(exact_subnet), - Poll::Ready(Some(Err(e))) => { - error!(error = e, "Failed to check for subnet unsubscription times"); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // process any generated events - if let Some(event) = self.events.pop_front() { - return Poll::Ready(Some(event)); - } - - Poll::Pending - } -} diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 7fdf9047fc..bee6569b7b 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -1,14 +1,14 @@ use super::*; +use beacon_chain::test_utils::generate_data_column_indices_rand_order; use beacon_chain::{ - builder::{BeaconChainBuilder, Witness}, - eth1_chain::CachingEth1Backend, - test_utils::get_kzg, BeaconChain, + builder::{BeaconChainBuilder, Witness}, + test_utils::get_kzg, }; -use genesis::{generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; +use genesis::{DEFAULT_ETH1_BLOCK_HASH, generate_deterministic_keypairs, interop_genesis_state}; use lighthouse_network::NetworkConfig; -use rand::rngs::StdRng; use rand::SeedableRng; +use rand::rngs::StdRng; use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::sync::{Arc, LazyLock}; use std::time::{Duration, SystemTime}; @@ -27,7 +27,6 @@ const TEST_LOG_LEVEL: Option<&str> = None; type TestBeaconChainType = Witness< SystemTimeSlotClock, - CachingEth1Backend, MainnetEthSpec, MemoryStore, MemoryStore, @@ -70,13 +69,14 @@ impl TestBeaconChain { .expect("should generate interop state"), ) .expect("should build state using recent genesis") - .dummy_eth1_backend() - .expect("should build dummy backend") .slot_clock(SystemTimeSlotClock::new( Slot::new(0), Duration::from_secs(recent_genesis_time()), Duration::from_millis(SLOT_DURATION_MILLIS), )) + .ordered_custody_column_indices(generate_data_column_indices_rand_order::< + MainnetEthSpec, + >()) .shutdown_sender(shutdown_tx) .rng(Box::new(StdRng::seed_from_u64(42))) .build() @@ -98,10 +98,9 @@ pub fn recent_genesis_time() -> u64 { fn get_tracing_subscriber(log_level: Option<&str>) { if let Some(level) = log_level { - tracing_subscriber::fmt() + let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_new(level).unwrap()) - .try_init() - .unwrap(); + .try_init(); } } @@ -134,11 +133,10 @@ async fn get_events_until_timeout + Unpin tokio::select! { Some(event) = stream.next() => { events.push(event); - if let Some(num) = num_events { - if events.len() == num { + if let Some(num) = num_events + && events.len() == num { break; } - } } _ = sleep.as_mut() => { break; @@ -485,7 +483,7 @@ mod test { // and 1 `DiscoverPeer` request corresponding to the bulk subnet discovery. assert_eq!(discover_peer_count, 1 + 1); // Generates a single discovery for permanent - // subscriptions and 1 for the subscription + // subscriptions and 1 for the subscription assert_eq!(enr_add_count, subnets_per_node); assert_eq!(unexpected_msg_count, 0); } @@ -588,7 +586,7 @@ mod test { println!("{events:?}"); let subscription_slot = current_slot + subscription_slot2 - 1; // one less do to the - // advance subscription time + // advance subscription time let wait_duration = subnet_service .beacon_chain .slot_clock diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index fcef06271f..6c0cbd7e55 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -9,26 +9,30 @@ //! sync as failed, log an error and attempt to retry once a new peer joins the node. use crate::network_beacon_processor::ChainSegmentProcessId; +use crate::sync::batch::{ + BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, +}; +use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::manager::BatchProcessResult; use crate::sync::network_context::{ RangeRequestId, RpcRequestSendError, RpcResponseError, SyncNetworkContext, }; -use crate::sync::range_sync::{ - BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, -}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::service::api_types::Id; use lighthouse_network::types::{BackFillState, NetworkGlobals}; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; +use std::collections::hash_map::DefaultHasher; use std::collections::{ - btree_map::{BTreeMap, Entry}, HashSet, + btree_map::{BTreeMap, Entry}, }; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; use std::sync::Arc; -use tracing::{debug, error, info, instrument, warn}; -use types::{Epoch, EthSpec}; +use tracing::{debug, error, info, warn}; +use types::{ColumnIndex, Epoch, EthSpec}; /// Blocks are downloaded in batches from peers. This constant specifies how many epochs worth of /// blocks per batch are requested _at most_. A batch may request less blocks to account for @@ -39,7 +43,7 @@ use types::{Epoch, EthSpec}; pub const BACKFILL_EPOCHS_PER_BATCH: u64 = 1; /// The maximum number of batches to queue before requesting more. -const BACKFILL_BATCH_BUFFER_SIZE: u8 = 20; +const BACKFILL_BATCH_BUFFER_SIZE: u8 = 5; /// The number of times to retry a batch before it is considered failed. const MAX_BATCH_DOWNLOAD_ATTEMPTS: u8 = 10; @@ -48,21 +52,27 @@ const MAX_BATCH_DOWNLOAD_ATTEMPTS: u8 = 10; /// after `MAX_BATCH_PROCESSING_ATTEMPTS` times, it is considered faulty. const MAX_BATCH_PROCESSING_ATTEMPTS: u8 = 10; -/// Custom configuration for the batch object. -struct BackFillBatchConfig {} +type RpcBlocks = Vec>; -impl BatchConfig for BackFillBatchConfig { +type BackFillBatchInfo = BatchInfo, RpcBlocks>; + +type BackFillSyncBatches = BTreeMap>; + +/// Custom configuration for the batch object. +struct BackFillBatchConfig { + marker: PhantomData, +} + +impl BatchConfig for BackFillBatchConfig { fn max_batch_download_attempts() -> u8 { MAX_BATCH_DOWNLOAD_ATTEMPTS } fn max_batch_processing_attempts() -> u8 { MAX_BATCH_PROCESSING_ATTEMPTS } - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; + fn batch_attempt_hash(data: &D) -> u64 { let mut hasher = DefaultHasher::new(); - blocks.hash(&mut hasher); + data.hash(&mut hasher); hasher.finish() } } @@ -120,7 +130,7 @@ pub struct BackFillSync { last_batch_downloaded: bool, /// Sorted map of batches undergoing some kind of processing. - batches: BTreeMap>, + batches: BackFillSyncBatches, /// The current processing batch, if any. current_processing_batch: Option, @@ -147,11 +157,6 @@ pub struct BackFillSync { } impl BackFillSync { - #[instrument(parent = None, - level = "info", - name = "backfill_sync", - skip_all - )] pub fn new( beacon_chain: Arc>, network_globals: Arc>, @@ -192,12 +197,6 @@ impl BackFillSync { } /// Pauses the backfill sync if it's currently syncing. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] pub fn pause(&mut self) { if let BackFillState::Syncing = self.state() { debug!(processed_epochs = %self.validated_batches, to_be_processed = %self.current_start,"Backfill sync paused"); @@ -209,12 +208,6 @@ impl BackFillSync { /// /// If resuming is successful, reports back the current syncing metrics. #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] pub fn start( &mut self, network: &mut SyncNetworkContext, @@ -226,9 +219,11 @@ impl BackFillSync { .network_globals .peers .read() - .synced_peers() + .synced_peers_for_epoch(self.to_be_downloaded) .next() .is_some() + // backfill can't progress if we do not have peers in the required subnets post peerdas. + && self.good_peers_on_sampling_subnets(self.to_be_downloaded, network) { // If there are peers to resume with, begin the resume. debug!(start_epoch = ?self.current_start, awaiting_batches = self.batches.len(), processing_target = ?self.processing_target, "Resuming backfill sync"); @@ -290,12 +285,6 @@ impl BackFillSync { /// A fully synced peer has joined us. /// If we are in a failed state, update a local variable to indicate we are able to restart /// the failed sync on the next attempt. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] pub fn fully_synced_peer_joined(&mut self) { if matches!(self.state(), BackFillState::Failed) { self.restart_failed_sync = true; @@ -304,12 +293,6 @@ impl BackFillSync { /// A peer has disconnected. /// If the peer has active batches, those are considered failed and re-requested. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn peer_disconnected(&mut self, peer_id: &PeerId) -> Result<(), BackFillError> { if matches!(self.state(), BackFillState::Failed) { @@ -324,12 +307,6 @@ impl BackFillSync { /// An RPC error has occurred. /// /// If the batch exists it is re-requested. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn inject_error( &mut self, @@ -340,12 +317,48 @@ impl BackFillSync { err: RpcResponseError, ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { + if let RpcResponseError::BlockComponentCouplingError(coupling_error) = &err { + match coupling_error { + CouplingError::DataColumnPeerFailure { + error, + faulty_peers, + exceeded_retries, + } => { + debug!(?batch_id, error, "Block components coupling error"); + // Note: we don't fail the batch here because a `CouplingError` is + // recoverable by requesting from other honest peers. + let mut failed_columns = HashSet::new(); + let mut failed_peers = HashSet::new(); + for (column, peer) in faulty_peers { + failed_columns.insert(*column); + failed_peers.insert(*peer); + } + + // Only retry if peer failure **and** retries haven't been exceeded + if !*exceeded_retries { + return self.retry_partial_batch( + network, + batch_id, + request_id, + failed_columns, + failed_peers, + ); + } + } + CouplingError::BlobPeerFailure(msg) => { + tracing::debug!(?batch_id, msg, "Blob peer failure"); + } + CouplingError::InternalError(msg) => { + error!(?batch_id, msg, "Block components coupling internal error"); + } + } + } // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer // TODO(das): removed peer_id matching as the node may request a different peer for data // columns. - if !batch.is_expecting_block(&request_id) { + if !batch.is_expecting_request_id(&request_id) { return Ok(()); } debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); @@ -367,12 +380,6 @@ impl BackFillSync { /// If this returns an error, the backfill sync has failed and will be restarted once new peers /// join the system. /// The sync manager should update the global sync state on failure. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn on_block_response( &mut self, @@ -395,12 +402,13 @@ impl BackFillSync { // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer, and that the // request_id matches - if !batch.is_expecting_block(&request_id) { + if !batch.is_expecting_request_id(&request_id) { return Ok(ProcessResult::Successful); } + let received = blocks.len(); match batch.download_completed(blocks, *peer_id) { - Ok(received) => { + Ok(_) => { let awaiting_batches = self.processing_target.saturating_sub(batch_id) / BACKFILL_EPOCHS_PER_BATCH; debug!( @@ -424,12 +432,6 @@ impl BackFillSync { /// The syncing process has failed. /// /// This resets past variables, to allow for a fresh start when resuming. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn fail_sync(&mut self, error: BackFillError) -> Result<(), BackFillError> { // Some errors shouldn't fail the chain. if matches!(error, BackFillError::Paused) { @@ -461,12 +463,6 @@ impl BackFillSync { /// Processes the batch with the given id. /// The batch must exist and be ready for processing - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn process_batch( &mut self, network: &mut SyncNetworkContext, @@ -494,7 +490,7 @@ impl BackFillSync { Err(e) => { return self .fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)) - .map(|_| ProcessResult::Successful) + .map(|_| ProcessResult::Successful); } Ok(v) => v, }; @@ -525,12 +521,6 @@ impl BackFillSync { /// The block processor has completed processing a batch. This function handles the result /// of the batch processor. /// If an error is returned the BackFill sync has failed. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] #[must_use = "A failure here indicates the backfill sync has failed and the global sync state should be updated"] pub fn on_batch_process_result( &mut self, @@ -683,12 +673,6 @@ impl BackFillSync { } /// Processes the next ready batch. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn process_completed_batches( &mut self, network: &mut SyncNetworkContext, @@ -709,11 +693,12 @@ impl BackFillSync { // Batch is not ready, nothing to process } BatchState::Poisoned => unreachable!("Poisoned batch"), - BatchState::Failed | BatchState::AwaitingDownload | BatchState::Processing(_) => { + // Batches can be in `AwaitingDownload` state if there weren't good data column subnet + // peers to send the request to. + BatchState::AwaitingDownload => return Ok(ProcessResult::Successful), + BatchState::Failed | BatchState::Processing(_) => { // these are all inconsistent states: // - Failed -> non recoverable batch. Chain should have been removed - // - AwaitingDownload -> A recoverable failed batch should have been - // re-requested. // - Processing -> `self.current_processing_batch` is None self.fail_sync(BackFillError::InvalidSyncState(String::from( "Invalid expected batch state", @@ -752,12 +737,6 @@ impl BackFillSync { /// /// If a previous batch has been validated and it had been re-processed, penalize the original /// peer. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn advance_chain(&mut self, network: &mut SyncNetworkContext, validating_epoch: Epoch) { // make sure this epoch produces an advancement if validating_epoch >= self.current_start { @@ -776,7 +755,7 @@ impl BackFillSync { // only for batches awaiting validation can we be sure the last attempt is // right, and thus, that any different attempt is wrong match batch.state() { - BatchState::AwaitingValidation(ref processed_attempt) => { + BatchState::AwaitingValidation(processed_attempt) => { for attempt in batch.attempts() { // The validated batch has been re-processed if attempt.hash != processed_attempt.hash { @@ -818,16 +797,17 @@ impl BackFillSync { } } BatchState::Downloading(..) => {} - BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => { + BatchState::AwaitingDownload => return, + BatchState::Failed | BatchState::Poisoned => { crit!("batch indicates inconsistent chain state while advancing chain") } BatchState::AwaitingProcessing(..) => {} BatchState::Processing(_) => { debug!(batch = %id, %batch, "Advancing chain while processing a batch"); - if let Some(processing_id) = self.current_processing_batch { - if id >= processing_id { - self.current_processing_batch = None; - } + if let Some(processing_id) = self.current_processing_batch + && id >= processing_id + { + self.current_processing_batch = None; } } } @@ -849,12 +829,6 @@ impl BackFillSync { /// These events occur when a peer has successfully responded with blocks, but the blocks we /// have received are incorrect or invalid. This indicates the peer has not performed as /// intended and can result in downvoting a peer. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn handle_invalid_batch( &mut self, network: &mut SyncNetworkContext, @@ -878,7 +852,7 @@ impl BackFillSync { for (id, batch) in self .batches .iter_mut() - .filter(|(&id, _batch)| id > batch_id) + .filter(|&(&id, ref _batch)| id > batch_id) { match batch .validation_failed() @@ -906,23 +880,21 @@ impl BackFillSync { } /// Requests the batch assigned to the given id from a given peer. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn send_batch( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, ) -> Result<(), BackFillError> { + if matches!(self.state(), BackFillState::Paused) { + return Err(BackFillError::Paused); + } if let Some(batch) = self.batches.get_mut(&batch_id) { + debug!(?batch_id, "Sending backfill batch"); let synced_peers = self .network_globals .peers .read() - .synced_peers() + .synced_peers_for_epoch(batch_id) .cloned() .collect::>(); @@ -933,6 +905,7 @@ impl BackFillSync { request, RangeRequestId::BackfillSync { batch_id }, &synced_peers, + &synced_peers, // All synced peers have imported up to the finalized slot so they must have their custody columns available &failed_peers, ) { Ok(request_id) => { @@ -970,7 +943,7 @@ impl BackFillSync { self.fail_sync(BackFillError::BatchDownloadFailed(batch_id))? } Ok(BatchOperationOutcome::Continue) => { - return self.send_batch(network, batch_id) + return self.send_batch(network, batch_id); } } } @@ -981,14 +954,55 @@ impl BackFillSync { Ok(()) } + /// Retries partial column requests within the batch by creating new requests for the failed columns. + pub fn retry_partial_batch( + &mut self, + network: &mut SyncNetworkContext, + batch_id: BatchId, + id: Id, + failed_columns: HashSet, + mut failed_peers: HashSet, + ) -> Result<(), BackFillError> { + if let Some(batch) = self.batches.get_mut(&batch_id) { + failed_peers.extend(&batch.failed_peers()); + let req = batch.to_blocks_by_range_request().0; + + let synced_peers = network + .network_globals() + .peers + .read() + .synced_peers_for_epoch(batch_id) + .cloned() + .collect::>(); + + match network.retry_columns_by_range( + id, + &synced_peers, + &failed_peers, + req, + &failed_columns, + ) { + Ok(_) => { + debug!( + ?batch_id, + id, "Retried column requests from different peers" + ); + return Ok(()); + } + Err(e) => { + debug!(?batch_id, id, e, "Failed to retry partial batch"); + } + } + } else { + return Err(BackFillError::InvalidSyncState( + "Batch should exist to be retried".to_string(), + )); + } + Ok(()) + } + /// When resuming a chain, this function searches for batches that need to be re-downloaded and /// transitions their state to redownload the batch. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn resume_batches(&mut self, network: &mut SyncNetworkContext) -> Result<(), BackFillError> { let batch_ids_to_retry = self .batches @@ -1013,12 +1027,6 @@ impl BackFillSync { /// Attempts to request the next required batches from the peer pool if the chain is syncing. It will exhaust the peer /// pool and left over batches until the batch buffer is reached or all peers are exhausted. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn request_batches( &mut self, network: &mut SyncNetworkContext, @@ -1043,12 +1051,6 @@ impl BackFillSync { /// Creates the next required batch from the chain. If there are no more batches required, /// `false` is returned. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn include_next_batch(&mut self, network: &mut SyncNetworkContext) -> Option { // don't request batches beyond genesis; if self.last_batch_downloaded { @@ -1058,7 +1060,7 @@ impl BackFillSync { // only request batches up to the buffer size limit // NOTE: we don't count batches in the AwaitingValidation state, to prevent stalling sync // if the current processing window is contained in a long range of skip slots. - let in_buffer = |batch: &BatchInfo| { + let in_buffer = |batch: &BackFillBatchInfo| { matches!( batch.state(), BatchState::Downloading(..) | BatchState::AwaitingProcessing(..) @@ -1074,6 +1076,11 @@ impl BackFillSync { return None; } + if !self.good_peers_on_sampling_subnets(self.to_be_downloaded, network) { + debug!("Waiting for peers to be available on custody column subnets"); + return None; + } + let batch_id = self.to_be_downloaded; // this batch could have been included already being an optimistic batch match self.batches.entry(batch_id) { @@ -1106,16 +1113,38 @@ impl BackFillSync { } } + /// Checks all sampling column subnets for peers. Returns `true` if there is at least one peer in + /// every sampling column subnet. + /// + /// Returns `true` if peerdas isn't enabled for the epoch. + fn good_peers_on_sampling_subnets( + &self, + epoch: Epoch, + network: &SyncNetworkContext, + ) -> bool { + if network.chain.spec.is_peer_das_enabled_for_epoch(epoch) { + // Require peers on all sampling column subnets before sending batches + network + .network_globals() + .sampling_subnets() + .iter() + .all(|subnet_id| { + let min_peer_count = 1; + network + .network_globals() + .peers + .read() + .has_good_peers_in_custody_subnet(subnet_id, min_peer_count) + }) + } else { + true + } + } + /// Resets the start epoch based on the beacon chain. /// /// This errors if the beacon chain indicates that backfill sync has already completed or is /// not required. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn reset_start_epoch(&mut self) -> Result<(), ResetEpochError> { let anchor_info = self.beacon_chain.store.get_anchor_info(); if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { @@ -1129,12 +1158,6 @@ impl BackFillSync { } /// Checks with the beacon chain if backfill sync has completed. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn check_completed(&mut self) -> bool { if self.would_complete(self.current_start) { // Check that the beacon chain agrees @@ -1150,12 +1173,6 @@ impl BackFillSync { } /// Checks if backfill would complete by syncing to `start_epoch`. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn would_complete(&self, start_epoch: Epoch) -> bool { start_epoch <= self @@ -1165,22 +1182,10 @@ impl BackFillSync { } /// Updates the global network state indicating the current state of a backfill sync. - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn set_state(&self, state: BackFillState) { *self.network_globals.backfill_state.write() = state; } - #[instrument(parent = None, - level = "info", - fields(service = "backfill_sync"), - name = "backfill_sync", - skip_all - )] fn state(&self) -> BackFillState { self.network_globals.backfill_state.read().clone() } @@ -1198,8 +1203,8 @@ mod tests { use beacon_chain::test_utils::BeaconChainHarness; use bls::Hash256; use lighthouse_network::{NetworkConfig, SyncInfo, SyncStatus}; - use rand::prelude::StdRng; - use rand::SeedableRng; + use rand_08::SeedableRng; + use rand_08::prelude::StdRng; use types::MinimalEthSpec; #[test] @@ -1243,6 +1248,7 @@ mod tests { head_root: Hash256::random(), finalized_epoch, finalized_root: Hash256::random(), + earliest_available_slot: None, }, }, ); diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/batch.rs similarity index 76% rename from beacon_node/network/src/sync/range_sync/batch.rs rename to beacon_node/network/src/sync/batch.rs index 264f83ee82..8de386f5be 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -1,29 +1,29 @@ use beacon_chain::block_verification_types::RpcBlock; -use lighthouse_network::rpc::methods::BlocksByRangeRequest; -use lighthouse_network::service::api_types::Id; +use educe::Educe; use lighthouse_network::PeerId; +use lighthouse_network::rpc::methods::BlocksByRangeRequest; +use lighthouse_network::rpc::methods::DataColumnsByRangeRequest; +use lighthouse_network::service::api_types::Id; use std::collections::HashSet; -use std::fmt; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; +use std::marker::PhantomData; use std::ops::Sub; -use std::time::{Duration, Instant}; +use std::time::Duration; +use std::time::Instant; use strum::Display; -use types::{Epoch, EthSpec, Slot}; +use types::Slot; +use types::{DataColumnSidecarList, Epoch, EthSpec}; -/// The number of times to retry a batch before it is considered failed. -const MAX_BATCH_DOWNLOAD_ATTEMPTS: u8 = 5; - -/// Invalid batches are attempted to be re-downloaded from other peers. If a batch cannot be processed -/// after `MAX_BATCH_PROCESSING_ATTEMPTS` times, it is considered faulty. -const MAX_BATCH_PROCESSING_ATTEMPTS: u8 = 3; +pub type BatchId = Epoch; /// Type of expected batch. -#[derive(Debug, Copy, Clone, Display)] +#[derive(Debug, Clone, Display)] #[strum(serialize_all = "snake_case")] pub enum ByRangeRequestType { BlocksAndColumns, BlocksAndBlobs, Blocks, + Columns(HashSet), } /// Allows customisation of the above constants used in other sync methods such as BackFillSync. @@ -59,28 +59,10 @@ pub trait BatchConfig { /// Note that simpler hashing functions considered in the past (hash of first block, hash of last /// block, number of received blocks) are not good enough to differentiate attempts. For this /// reason, we hash the complete set of blocks both in RangeSync and BackFillSync. - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64; + fn batch_attempt_hash(data: &D) -> u64; } #[derive(Debug)] -pub struct RangeSyncBatchConfig {} - -impl BatchConfig for RangeSyncBatchConfig { - fn max_batch_download_attempts() -> u8 { - MAX_BATCH_DOWNLOAD_ATTEMPTS - } - fn max_batch_processing_attempts() -> u8 { - MAX_BATCH_PROCESSING_ATTEMPTS - } - fn batch_attempt_hash(blocks: &[RpcBlock]) -> u64 { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - blocks.hash(&mut hasher); - hasher.finish() - } -} - -/// Error type of a batch in a wrong state. -// Such errors should never be encountered. pub struct WrongState(pub(crate) String); /// After batch operations, we use this to communicate whether a batch can continue or not @@ -89,35 +71,40 @@ pub enum BatchOperationOutcome { Failed { blacklist: bool }, } +#[derive(Debug)] pub enum BatchProcessingResult { Success, FaultyFailure, NonFaultyFailure, } -#[derive(Debug)] +#[derive(Educe)] +#[educe(Debug)] /// A segment of a chain. -pub struct BatchInfo { +pub struct BatchInfo { /// Start slot of the batch. start_slot: Slot, /// End slot of the batch. end_slot: Slot, /// The `Attempts` that have been made and failed to send us this batch. - failed_processing_attempts: Vec, + failed_processing_attempts: Vec>, /// Number of processing attempts that have failed but we do not count. non_faulty_processing_attempts: u8, /// The number of download retries this batch has undergone due to a failed request. failed_download_attempts: Vec>, /// State of the batch. - state: BatchState, + state: BatchState, /// Whether this batch contains all blocks or all blocks and blobs. batch_type: ByRangeRequestType, /// Pin the generic - marker: std::marker::PhantomData, + #[educe(Debug(ignore))] + marker: std::marker::PhantomData<(E, B)>, } -impl fmt::Display for BatchInfo { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display + for BatchInfo +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Start Slot: {}, End Slot: {}, State: {}", @@ -128,21 +115,21 @@ impl fmt::Display for BatchInfo { #[derive(Display)] /// Current state of a batch -pub enum BatchState { +pub enum BatchState { /// The batch has failed either downloading or processing, but can be requested again. AwaitingDownload, /// The batch is being downloaded. Downloading(Id), /// The batch has been completely downloaded and is ready for processing. - AwaitingProcessing(PeerId, Vec>, Instant), + AwaitingProcessing(PeerId, D, Instant), /// The batch is being processed. - Processing(Attempt), + Processing(Attempt), /// The batch was successfully processed and is waiting to be validated. /// /// It is not sufficient to process a batch successfully to consider it correct. This is /// because batches could be erroneously empty, or incomplete. Therefore, a batch is considered /// valid, only if the next sequential batch imports at least a block. - AwaitingValidation(Attempt), + AwaitingValidation(Attempt), /// Intermediate state for inner state handling. Poisoned, /// The batch has maxed out the allowed attempts for either downloading or processing. It @@ -150,14 +137,14 @@ pub enum BatchState { Failed, } -impl BatchState { +impl BatchState { /// Helper function for poisoning a state. - pub fn poison(&mut self) -> BatchState { + pub fn poison(&mut self) -> BatchState { std::mem::replace(self, BatchState::Poisoned) } } -impl BatchInfo { +impl BatchInfo { /// Batches are downloaded excluding the first block of the epoch assuming it has already been /// downloaded. /// @@ -174,13 +161,13 @@ impl BatchInfo { pub fn new(start_epoch: &Epoch, num_of_epochs: u64, batch_type: ByRangeRequestType) -> Self { let start_slot = start_epoch.start_slot(E::slots_per_epoch()); let end_slot = start_slot + num_of_epochs * E::slots_per_epoch(); - BatchInfo { + Self { start_slot, end_slot, failed_processing_attempts: Vec::new(), failed_download_attempts: Vec::new(), non_faulty_processing_attempts: 0, - state: BatchState::AwaitingDownload, + state: BatchState::::AwaitingDownload, batch_type, marker: std::marker::PhantomData, } @@ -204,8 +191,8 @@ impl BatchInfo { peers } - /// Verifies if an incoming block belongs to this batch. - pub fn is_expecting_block(&self, request_id: &Id) -> bool { + /// Verifies if an incoming request id to this batch. + pub fn is_expecting_request_id(&self, request_id: &Id) -> bool { if let BatchState::Downloading(expected_id) = &self.state { return expected_id == request_id; } @@ -223,30 +210,6 @@ impl BatchInfo { } } - /// Returns the count of stored pending blocks if in awaiting processing state - pub fn pending_blocks(&self) -> usize { - match &self.state { - BatchState::AwaitingProcessing(_, blocks, _) => blocks.len(), - BatchState::AwaitingDownload - | BatchState::Downloading { .. } - | BatchState::Processing { .. } - | BatchState::AwaitingValidation { .. } - | BatchState::Poisoned - | BatchState::Failed => 0, - } - } - - /// Returns a BlocksByRange request associated with the batch. - pub fn to_blocks_by_range_request(&self) -> (BlocksByRangeRequest, ByRangeRequestType) { - ( - BlocksByRangeRequest::new( - self.start_slot.into(), - self.end_slot.sub(self.start_slot).into(), - ), - self.batch_type, - ) - } - /// After different operations over a batch, this could be in a state that allows it to /// continue, or in failed state. When the batch has failed, we check if it did mainly due to /// processing failures. In this case the batch is considered failed and faulty. @@ -261,27 +224,22 @@ impl BatchInfo { } } - pub fn state(&self) -> &BatchState { + pub fn state(&self) -> &BatchState { &self.state } - pub fn attempts(&self) -> &[Attempt] { + pub fn attempts(&self) -> &[Attempt] { &self.failed_processing_attempts } - /// Marks the batch as ready to be processed if the blocks are in the range. The number of - /// received blocks is returned, or the wrong batch end on failure + /// Marks the batch as ready to be processed if the data columns are in the range. The number of + /// received columns is returned, or the wrong batch end on failure #[must_use = "Batch may have failed"] - pub fn download_completed( - &mut self, - blocks: Vec>, - peer: PeerId, - ) -> Result { + pub fn download_completed(&mut self, data_columns: D, peer: PeerId) -> Result<(), WrongState> { match self.state.poison() { BatchState::Downloading(_) => { - let received = blocks.len(); - self.state = BatchState::AwaitingProcessing(peer, blocks, Instant::now()); - Ok(received) + self.state = BatchState::AwaitingProcessing(peer, data_columns, Instant::now()); + Ok(()) } BatchState::Poisoned => unreachable!("Poisoned batch"), other => { @@ -330,6 +288,31 @@ impl BatchInfo { } } + /// Change the batch state from `Self::Downloading` to `Self::AwaitingDownload` without + /// registering a failed attempt. + /// + /// Note: must use this cautiously with some level of retry protection + /// as not registering a failed attempt could lead to requesting in a loop. + #[must_use = "Batch may have failed"] + pub fn downloading_to_awaiting_download( + &mut self, + ) -> Result { + match self.state.poison() { + BatchState::Downloading(_) => { + self.state = BatchState::AwaitingDownload; + Ok(self.outcome()) + } + BatchState::Poisoned => unreachable!("Poisoned batch"), + other => { + self.state = other; + Err(WrongState(format!( + "Download failed for batch in wrong state {:?}", + self.state + ))) + } + } + } + pub fn start_downloading(&mut self, request_id: Id) -> Result<(), WrongState> { match self.state.poison() { BatchState::AwaitingDownload => { @@ -347,31 +330,30 @@ impl BatchInfo { } } - pub fn start_processing(&mut self) -> Result<(Vec>, Duration), WrongState> { + pub fn start_processing(&mut self) -> Result<(D, Duration), WrongState> { match self.state.poison() { - BatchState::AwaitingProcessing(peer, blocks, start_instant) => { - self.state = BatchState::Processing(Attempt::new::(peer, &blocks)); - Ok((blocks, start_instant.elapsed())) + BatchState::AwaitingProcessing(peer, data_columns, start_instant) => { + self.state = BatchState::Processing(Attempt::new::(peer, &data_columns)); + Ok((data_columns, start_instant.elapsed())) } BatchState::Poisoned => unreachable!("Poisoned batch"), other => { self.state = other; Err(WrongState(format!( - "Starting procesing batch in wrong state {:?}", + "Starting processing batch in wrong state {:?}", self.state ))) } } } - #[must_use = "Batch may have failed"] pub fn processing_completed( &mut self, - procesing_result: BatchProcessingResult, + processing_result: BatchProcessingResult, ) -> Result { match self.state.poison() { BatchState::Processing(attempt) => { - self.state = match procesing_result { + self.state = match processing_result { BatchProcessingResult::Success => BatchState::AwaitingValidation(attempt), BatchProcessingResult::FaultyFailure => { // register the failed attempt @@ -438,39 +420,86 @@ impl BatchInfo { } } -/// Represents a peer's attempt and providing the result for this batch. -/// -/// Invalid attempts will downscore a peer. -#[derive(PartialEq, Debug)] -pub struct Attempt { +// BatchInfo implementations for RangeSync +impl BatchInfo>> { + /// Returns a BlocksByRange request associated with the batch. + pub fn to_blocks_by_range_request(&self) -> (BlocksByRangeRequest, ByRangeRequestType) { + ( + BlocksByRangeRequest::new( + self.start_slot.into(), + self.end_slot.sub(self.start_slot).into(), + ), + self.batch_type.clone(), + ) + } + + /// Returns the count of stored pending blocks if in awaiting processing state + pub fn pending_blocks(&self) -> usize { + match &self.state { + BatchState::AwaitingProcessing(_, blocks, _) => blocks.len(), + BatchState::AwaitingDownload + | BatchState::Downloading { .. } + | BatchState::Processing { .. } + | BatchState::AwaitingValidation { .. } + | BatchState::Poisoned + | BatchState::Failed => 0, + } + } +} + +// BatchInfo implementation for CustodyBackFillSync +impl BatchInfo> { + /// Returns a DataColumnsByRange request associated with the batch. + pub fn to_data_columns_by_range_request( + &self, + ) -> Result { + match &self.batch_type { + ByRangeRequestType::Columns(columns) => Ok(DataColumnsByRangeRequest { + start_slot: self.start_slot.into(), + count: self.end_slot.sub(self.start_slot).into(), + columns: columns.clone().into_iter().collect(), + }), + _ => Err(WrongState( + "Custody backfill sync can only make data columns by range requests.".to_string(), + )), + } + } +} + +#[derive(Debug)] +pub struct Attempt { /// The peer that made the attempt. pub peer_id: PeerId, /// The hash of the blocks of the attempt. pub hash: u64, + /// Pin the generic. + marker: PhantomData, } -impl Attempt { - fn new(peer_id: PeerId, blocks: &[RpcBlock]) -> Self { - let hash = B::batch_attempt_hash(blocks); - Attempt { peer_id, hash } +impl Attempt { + fn new(peer_id: PeerId, data: &D) -> Self { + let hash = B::batch_attempt_hash(data); + Attempt { + peer_id, + hash, + marker: PhantomData, + } } } -impl std::fmt::Debug for BatchState { +impl std::fmt::Debug for BatchState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - BatchState::Processing(Attempt { - ref peer_id, - hash: _, - }) => write!(f, "Processing({})", peer_id), - BatchState::AwaitingValidation(Attempt { - ref peer_id, - hash: _, - }) => write!(f, "AwaitingValidation({})", peer_id), + BatchState::Processing(Attempt { peer_id, .. }) => { + write!(f, "Processing({})", peer_id) + } + BatchState::AwaitingValidation(Attempt { peer_id, .. }) => { + write!(f, "AwaitingValidation({})", peer_id) + } BatchState::AwaitingDownload => f.write_str("AwaitingDownload"), BatchState::Failed => f.write_str("Failed"), - BatchState::AwaitingProcessing(ref peer, ref blocks, _) => { - write!(f, "AwaitingProcessing({}, {} blocks)", peer, blocks.len()) + BatchState::AwaitingProcessing(peer, ..) => { + write!(f, "AwaitingProcessing({})", peer) } BatchState::Downloading(request_id) => { write!(f, "Downloading({})", request_id) @@ -480,7 +509,7 @@ impl std::fmt::Debug for BatchState { } } -impl BatchState { +impl BatchState { /// Creates a character representation/visualization for the batch state to display in logs for quicker and /// easier recognition fn visualize(&self) -> char { diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index 86b6894bac..c6b0519087 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -14,8 +14,8 @@ use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; use types::{DataColumnSidecarList, SignedBeaconBlock}; -use super::single_block_lookup::{ComponentRequests, DownloadResult}; use super::SingleLookupId; +use super::single_block_lookup::{ComponentRequests, DownloadResult}; #[derive(Debug, Copy, Clone)] pub enum ResponseType { diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 8c884f644e..f8ffd298ca 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -20,15 +20,15 @@ //! or consider a lookup complete. These caches are read from the `SyncNetworkContext` and its state //! returned to this module as `LookupRequestResult` variants. -use self::parent_chain::{compute_parent_chains, NodeChain}; +use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; use super::manager::{BlockProcessType, BlockProcessingResult, SLOT_IMPORT_TOLERANCE}; use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; use crate::metrics; +use crate::sync::SyncMessage; use crate::sync::block_lookups::common::ResponseType; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; -use crate::sync::SyncMessage; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_availability_checker::{ AvailabilityCheckError, AvailabilityCheckErrorCategory, @@ -36,7 +36,6 @@ use beacon_chain::data_availability_checker::{ use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; pub use common::RequestState; use fnv::FnvHashMap; -use itertools::Itertools; use lighthouse_network::service::api_types::SingleLookupReqId; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; @@ -45,7 +44,7 @@ use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; -use tracing::{debug, error, instrument, warn}; +use tracing::{debug, error, warn}; use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; pub mod common; @@ -60,7 +59,7 @@ mod single_block_lookup; /// reaches the maximum depth it will force trigger range sync. pub(crate) const PARENT_DEPTH_TOLERANCE: usize = SLOT_IMPORT_TOLERANCE; -const FAILED_CHAINS_CACHE_EXPIRY_SECONDS: u64 = 60; +const IGNORED_CHAINS_CACHE_EXPIRY_SECONDS: u64 = 60; pub const SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS: u8 = 4; /// Maximum time we allow a lookup to exist before assuming it is stuck and will never make @@ -106,13 +105,15 @@ pub type SingleLookupId = u32; enum Action { Retry, ParentUnknown { parent_root: Hash256 }, - Drop, + Drop(/* reason: */ String), Continue, } pub struct BlockLookups { - /// A cache of failed chain lookups to prevent duplicate searches. - failed_chains: LRUTimeCache, + /// A cache of block roots that must be ignored for some time to prevent useless searches. For + /// example if a chain is too long, its lookup chain is dropped, and range sync is expected to + /// eventually sync those blocks + ignored_chains: LRUTimeCache, // TODO: Why not index lookups by block_root? single_block_lookups: FnvHashMap>, @@ -127,45 +128,26 @@ use lighthouse_network::service::api_types::Id; pub(crate) type BlockLookupSummary = (Id, Hash256, Option, Vec); impl BlockLookups { - #[instrument(parent = None,level = "info", fields(service = "lookup_sync"), name = "lookup_sync")] pub fn new() -> Self { Self { - failed_chains: LRUTimeCache::new(Duration::from_secs( - FAILED_CHAINS_CACHE_EXPIRY_SECONDS, + ignored_chains: LRUTimeCache::new(Duration::from_secs( + IGNORED_CHAINS_CACHE_EXPIRY_SECONDS, )), single_block_lookups: Default::default(), } } #[cfg(test)] - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] - pub(crate) fn insert_failed_chain(&mut self, block_root: Hash256) { - self.failed_chains.insert(block_root); + pub(crate) fn insert_ignored_chain(&mut self, block_root: Hash256) { + self.ignored_chains.insert(block_root); } #[cfg(test)] - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] - pub(crate) fn get_failed_chains(&mut self) -> Vec { - self.failed_chains.keys().cloned().collect() + pub(crate) fn get_ignored_chains(&mut self) -> Vec { + self.ignored_chains.keys().cloned().collect() } #[cfg(test)] - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub(crate) fn active_single_lookups(&self) -> Vec { self.single_block_lookups .iter() @@ -174,12 +156,6 @@ impl BlockLookups { } /// Returns a vec of all parent lookup chains by tip, in descending slot order (tip first) - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub(crate) fn active_parent_lookups(&self) -> Vec { compute_parent_chains( &self @@ -194,26 +170,23 @@ impl BlockLookups { /// Creates a parent lookup for the block with the given `block_root` and immediately triggers it. /// If a parent lookup exists or is triggered, a current lookup will be created. - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] + /// + /// Returns true if the lookup is created or already exists + #[must_use = "only reference the new lookup if returns true"] pub fn search_child_and_parent( &mut self, block_root: Hash256, block_component: BlockComponent, peer_id: PeerId, cx: &mut SyncNetworkContext, - ) { + ) -> bool { let parent_root = block_component.parent_root(); let parent_lookup_exists = self.search_parent_of_child(parent_root, block_root, &[peer_id], cx); // Only create the child lookup if the parent exists if parent_lookup_exists { - // `search_parent_of_child` ensures that parent root is not a failed chain + // `search_parent_of_child` ensures that the parent lookup exists so we can safely wait for it self.new_current_lookup( block_root, Some(block_component), @@ -223,25 +196,23 @@ impl BlockLookups { // the lookup with zero peers to house the block components. &[], cx, - ); + ) + } else { + false } } /// Seach a block whose parent root is unknown. + /// /// Returns true if the lookup is created or already exists - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] + #[must_use = "only reference the new lookup if returns true"] pub fn search_unknown_block( &mut self, block_root: Hash256, peer_source: &[PeerId], cx: &mut SyncNetworkContext, - ) { - self.new_current_lookup(block_root, None, None, peer_source, cx); + ) -> bool { + self.new_current_lookup(block_root, None, None, peer_source, cx) } /// A block or blob triggers the search of a parent. @@ -250,12 +221,7 @@ impl BlockLookups { /// - `block_root_to_search` is a failed chain /// /// Returns true if the lookup is created or already exists - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] + #[must_use = "only reference the new lookup if returns true"] pub fn search_parent_of_child( &mut self, block_root_to_search: Hash256, @@ -280,8 +246,8 @@ impl BlockLookups { debug!(block_root = ?block_root_to_search, "Parent lookup chain too long"); // Searching for this parent would extend a parent chain over the max - // Insert the tip only to failed chains - self.failed_chains.insert(parent_chain.tip); + // Insert the tip only to chains to ignore + self.ignored_chains.insert(parent_chain.tip); // Note: Drop only the chain that's too long until it merges with another chain // that's not too long. Consider this attack: there's a chain of valid unknown @@ -357,12 +323,7 @@ impl BlockLookups { /// Searches for a single block hash. If the blocks parent is unknown, a chain of blocks is /// constructed. /// Returns true if the lookup is created or already exists - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] + #[must_use = "only reference the new lookup if returns true"] fn new_current_lookup( &mut self, block_root: Hash256, @@ -371,12 +332,9 @@ impl BlockLookups { peers: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { - // If this block or it's parent is part of a known failed chain, ignore it. - if self.failed_chains.contains(&block_root) { - debug!(?block_root, "Block is from a past failed chain. Dropping"); - for peer_id in peers { - cx.report_peer(*peer_id, PeerAction::MidToleranceError, "failed_chain"); - } + // If this block or it's parent is part of a known ignored chain, ignore it. + if self.ignored_chains.contains(&block_root) { + debug!(?block_root, "Dropping lookup for block marked ignored"); return false; } @@ -405,15 +363,14 @@ impl BlockLookups { } // Ensure that awaiting parent exists, otherwise this lookup won't be able to make progress - if let Some(awaiting_parent) = awaiting_parent { - if !self + if let Some(awaiting_parent) = awaiting_parent + && !self .single_block_lookups .iter() .any(|(_, lookup)| lookup.is_for_block(awaiting_parent)) - { - warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); - return false; - } + { + warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); + return false; } // Lookups contain untrusted data, bound the total count of lookups hold in memory to reduce @@ -426,6 +383,7 @@ impl BlockLookups { // If we know that this lookup has unknown parent (is awaiting a parent lookup to resolve), // signal here to hold processing downloaded data. let mut lookup = SingleBlockLookup::new(block_root, peers, cx.next_id(), awaiting_parent); + let _guard = lookup.span.clone().entered(); // Add block components to the new request if let Some(block_component) = block_component { @@ -465,12 +423,6 @@ impl BlockLookups { /* Lookup responses */ /// Process a block or blob response received from a single lookup request. - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn on_download_response>( &mut self, id: SingleLookupReqId, @@ -556,12 +508,6 @@ impl BlockLookups { /* Error responses */ - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn peer_disconnected(&mut self, peer_id: &PeerId) { for (_, lookup) in self.single_block_lookups.iter_mut() { lookup.remove_peer(peer_id); @@ -570,12 +516,6 @@ impl BlockLookups { /* Processing responses */ - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn on_processing_result( &mut self, process_type: BlockProcessType, @@ -596,12 +536,6 @@ impl BlockLookups { self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); } - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn on_processing_result_inner>( &mut self, lookup_id: SingleLookupId, @@ -656,7 +590,7 @@ impl BlockLookups { // This is unreachable because RPC blocks do not undergo gossip verification, and // this error can *only* come from gossip verification. error!(?block_root, "Single block lookup hit unreachable condition"); - Action::Drop + Action::Drop("DuplicateImportStatusUnknown".to_owned()) } BlockProcessingResult::Ignored => { // Beacon processor signalled to ignore the block processing result. @@ -665,14 +599,14 @@ impl BlockLookups { component = ?R::response_type(), "Lookup component processing ignored, cpu might be overloaded" ); - Action::Drop + Action::Drop("Block processing ignored".to_owned()) } BlockProcessingResult::Err(e) => { match e { BlockError::BeaconChainError(e) => { // Internal error error!(%block_root, error = ?e, "Beacon chain error processing lookup component"); - Action::Drop + Action::Drop(format!("{e:?}")) } BlockError::ParentUnknown { parent_root, .. } => { // Reverts the status of this request to `AwaitingProcessing` holding the @@ -691,7 +625,7 @@ impl BlockLookups { error = ?e, "Single block lookup failed. Execution layer is offline / unsynced / misconfigured" ); - Action::Drop + Action::Drop(format!("{e:?}")) } BlockError::AvailabilityCheck(e) if e.category() == AvailabilityCheckErrorCategory::Internal => @@ -703,7 +637,7 @@ impl BlockLookups { // lookup state transition. This error invalidates both blob and block requests, and we don't know the // state of both requests. Blobs may have already successfullly processed for example. // We opt to drop the lookup instead. - Action::Drop + Action::Drop(format!("{e:?}")) } other => { debug!( @@ -718,15 +652,15 @@ impl BlockLookups { // but future errors may follow the same pattern. Generalize this // pattern with https://github.com/sigp/lighthouse/pull/6321 BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidColumn(errors), - ) => errors - .iter() - // Collect all peers that sent a column that was invalid. Must - // run .unique as a single peer can send multiple invalid - // columns. Penalize once to avoid insta-bans - .flat_map(|(index, _)| peer_group.of_index((*index) as usize)) - .unique() - .collect(), + AvailabilityCheckError::InvalidColumn((index_opt, _)), + ) => { + match index_opt { + Some(index) => peer_group.of_index(index as usize).collect(), + // If no index supplied this is an un-attributable fault. In practice + // this should never happen. + None => vec![], + } + } _ => peer_group.all().collect(), }; for peer in peers_to_penalize { @@ -757,19 +691,32 @@ impl BlockLookups { } Action::ParentUnknown { parent_root } => { let peers = lookup.all_peers(); + // Mark lookup as awaiting **before** creating the parent lookup. At this point the + // lookup maybe inconsistent. lookup.set_awaiting_parent(parent_root); - debug!( - id = lookup.id, - ?block_root, - ?parent_root, - "Marking lookup as awaiting parent" - ); - self.search_parent_of_child(parent_root, block_root, &peers, cx); - Ok(LookupResult::Pending) + let parent_lookup_exists = + self.search_parent_of_child(parent_root, block_root, &peers, cx); + if parent_lookup_exists { + // The parent lookup exist or has been created. It's safe for `lookup` to + // reference the parent as awaiting. + debug!( + id = lookup_id, + ?block_root, + ?parent_root, + "Marking lookup as awaiting parent" + ); + Ok(LookupResult::Pending) + } else { + // The parent lookup is faulty and was not created, we must drop the `lookup` as + // it's in an inconsistent state. We must drop all of its children too. + Err(LookupRequestError::Failed(format!( + "Parent lookup is faulty {parent_root:?}" + ))) + } } - Action::Drop => { + Action::Drop(reason) => { // Drop with noop - Err(LookupRequestError::Failed) + Err(LookupRequestError::Failed(reason)) } Action::Continue => { // Drop this completed lookup only @@ -778,12 +725,6 @@ impl BlockLookups { } } - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn on_external_processing_result( &mut self, block_root: Hash256, @@ -809,12 +750,6 @@ impl BlockLookups { } /// Makes progress on the immediate children of `block_root` - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self @@ -840,12 +775,6 @@ impl BlockLookups { /// Drops `dropped_id` lookup and all its children recursively. Lookups awaiting a parent need /// the parent to make progress to resolve, therefore we must drop them if the parent is /// dropped. - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn drop_lookup_and_children(&mut self, dropped_id: SingleLookupId) { if let Some(dropped_lookup) = self.single_block_lookups.remove(&dropped_id) { debug!( @@ -870,12 +799,6 @@ impl BlockLookups { /// Common handler a lookup request error, drop it and update metrics /// Returns true if the lookup is created or already exists - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] fn on_lookup_result( &mut self, id: SingleLookupId, @@ -913,24 +836,12 @@ impl BlockLookups { /* Helper functions */ /// Drops all the single block requests and returns how many requests were dropped. - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn drop_single_block_requests(&mut self) -> usize { let requests_to_drop = self.single_block_lookups.len(); self.single_block_lookups.clear(); requests_to_drop } - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn update_metrics(&self) { metrics::set_gauge( &metrics::SYNC_SINGLE_BLOCK_LOOKUPS, @@ -939,12 +850,6 @@ impl BlockLookups { } /// Perform some prune operations on lookups on some interval - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] pub fn prune_lookups(&mut self) { self.drop_lookups_without_peers(); self.drop_stuck_lookups(); @@ -968,12 +873,6 @@ impl BlockLookups { /// /// Instead there's no negative for keeping lookups with no peers around for some time. If we /// regularly prune them, it should not be a memory concern (TODO: maybe yes!). - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] fn drop_lookups_without_peers(&mut self) { for (lookup_id, block_root) in self .single_block_lookups @@ -1011,12 +910,6 @@ impl BlockLookups { /// /// - One single clear warn level log per stuck incident /// - If the original bug is sporadic, it reduces the time a node is stuck from forever to 15 min - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] fn drop_stuck_lookups(&mut self) { // While loop to find and drop all disjoint trees of potentially stuck lookups. while let Some(stuck_lookup) = self.single_block_lookups.values().find(|lookup| { @@ -1054,12 +947,6 @@ impl BlockLookups { } /// Recursively find the oldest ancestor lookup of another lookup - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] fn find_oldest_ancestor_lookup<'a>( &'a self, lookup: &'a SingleBlockLookup, @@ -1084,12 +971,6 @@ impl BlockLookups { /// Adds peers to a lookup and its ancestors recursively. /// Note: Takes a `lookup_id` as argument to allow recursion on mutable lookups, without having /// to duplicate the code to add peers to a lookup - #[instrument(parent = None, - level = "info", - fields(service = "lookup_sync"), - name = "lookup_sync", - skip_all - )] fn add_peers_to_lookup_and_ancestors( &mut self, lookup_id: SingleLookupId, diff --git a/beacon_node/network/src/sync/block_lookups/parent_chain.rs b/beacon_node/network/src/sync/block_lookups/parent_chain.rs index 009b5e2ff7..5deea1dd94 100644 --- a/beacon_node/network/src/sync/block_lookups/parent_chain.rs +++ b/beacon_node/network/src/sync/block_lookups/parent_chain.rs @@ -117,8 +117,9 @@ pub(crate) fn find_oldest_fork_ancestor( #[cfg(test)] mod tests { - use super::{compute_parent_chains, find_oldest_fork_ancestor, Node}; - use types::{FixedBytesExtended, Hash256}; + use super::{Node, compute_parent_chains, find_oldest_fork_ancestor}; + use fixed_bytes::FixedBytesExtended; + use types::Hash256; fn h(n: u64) -> Hash256 { Hash256::from_low_u64_be(n) diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 3789dbe91e..46897b2283 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -5,8 +5,9 @@ use crate::sync::network_context::{ SyncNetworkContext, }; use beacon_chain::{BeaconChainTypes, BlockProcessStatus}; -use derivative::Derivative; +use educe::Educe; use lighthouse_network::service::api_types::Id; +use lighthouse_tracing::SPAN_SINGLE_BLOCK_LOOKUP; use parking_lot::RwLock; use std::collections::HashSet; use std::fmt::Debug; @@ -14,6 +15,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use store::Hash256; use strum::IntoStaticStr; +use tracing::{Span, debug_span}; use types::blob_sidecar::FixedBlobSidecarList; use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; @@ -40,7 +42,7 @@ pub enum LookupRequestError { /// Inconsistent lookup request state BadState(String), /// Lookup failed for some other reason and should be dropped - Failed, + Failed(/* reason: */ String), /// Received MissingComponents when all components have been processed. This should never /// happen, and indicates some internal bug MissingComponentsAfterAllProcessed, @@ -55,8 +57,8 @@ pub enum LookupRequestError { }, } -#[derive(Derivative)] -#[derivative(Debug(bound = "T: BeaconChainTypes"))] +#[derive(Educe)] +#[educe(Debug(bound(T: BeaconChainTypes)))] pub struct SingleBlockLookup { pub id: Id, pub block_request_state: BlockRequestState, @@ -65,11 +67,12 @@ pub struct SingleBlockLookup { /// the custody request to have an updated view of the peers that claim to have imported the /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster /// than the lifetime of a custody request. - #[derivative(Debug(format_with = "fmt_peer_set_as_len"))] + #[educe(Debug(method(fmt_peer_set_as_len)))] peers: Arc>>, block_root: Hash256, awaiting_parent: Option, created: Instant, + pub(crate) span: Span, } #[derive(Debug)] @@ -89,6 +92,12 @@ impl SingleBlockLookup { id: Id, awaiting_parent: Option, ) -> Self { + let lookup_span = debug_span!( + SPAN_SINGLE_BLOCK_LOOKUP, + block_root = %requested_block_root, + id = id, + ); + Self { id, block_request_state: BlockRequestState::new(requested_block_root), @@ -97,6 +106,7 @@ impl SingleBlockLookup { block_root: requested_block_root, awaiting_parent, created: Instant::now(), + span: lookup_span, } } @@ -192,6 +202,7 @@ impl SingleBlockLookup { &mut self, cx: &mut SyncNetworkContext, ) -> Result { + let _guard = self.span.clone().entered(); // TODO: Check what's necessary to download, specially for blobs self.continue_request::>(cx, 0)?; @@ -208,7 +219,7 @@ impl SingleBlockLookup { // can assert that this is the correct value of `blob_kzg_commitments_count`. match cx.chain.get_block_process_status(&self.block_root) { BlockProcessStatus::Unknown => None, - BlockProcessStatus::NotValidated(block) + BlockProcessStatus::NotValidated(block, _) | BlockProcessStatus::ExecutionValidated(block) => Some(block.clone()), } }) { @@ -257,6 +268,7 @@ impl SingleBlockLookup { // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. if self.all_components_processed() { + self.span = Span::none(); Ok(LookupResult::Completed) } else { Ok(LookupResult::Pending) @@ -357,10 +369,10 @@ impl SingleBlockLookup { } /// The state of the blob request component of a `SingleBlockLookup`. -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Educe)] +#[educe(Debug)] pub struct BlobRequestState { - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] pub block_root: Hash256, pub state: SingleLookupRequestState>, } @@ -375,10 +387,10 @@ impl BlobRequestState { } /// The state of the custody request component of a `SingleBlockLookup`. -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Educe)] +#[educe(Debug)] pub struct CustodyRequestState { - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] pub block_root: Hash256, pub state: SingleLookupRequestState>, } @@ -393,10 +405,10 @@ impl CustodyRequestState { } /// The state of the block request component of a `SingleBlockLookup`. -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Educe)] +#[educe(Debug)] pub struct BlockRequestState { - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] pub requested_block_root: Hash256, pub state: SingleLookupRequestState>>, } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 99428b0c80..ed9a11a03d 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,23 +1,43 @@ use beacon_chain::{ block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, get_block_root, }; -use lighthouse_network::service::api_types::{ - BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, +use lighthouse_network::{ + PeerId, + service::api_types::{ + BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, + }, }; +use ssz_types::RuntimeVariableList; use std::{collections::HashMap, sync::Arc}; +use tracing::{Span, debug}; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, RuntimeVariableList, SignedBeaconBlock, + Hash256, SignedBeaconBlock, }; +use crate::sync::network_context::MAX_COLUMN_RETRIES; + +/// Accumulates and couples beacon blocks with their associated data (blobs or data columns) +/// from range sync network responses. +/// +/// This struct acts as temporary storage while multiple network responses arrive: +/// - Blocks themselves (always required) +/// - Blob sidecars (pre-Fulu fork) +/// - Data columns (Fulu fork and later) +/// +/// It accumulates responses until all expected components are received, then couples +/// them together and returns complete `RpcBlock`s ready for processing. Handles validation +/// and peer failure detection during the coupling process. pub struct RangeBlockComponentsRequest { /// Blocks we have received awaiting for their corresponding sidecar. blocks_request: ByRangeRequest>>>, /// Sidecars we have received awaiting for their corresponding block. block_data_request: RangeBlockDataRequest, + /// Span to track the range request and all children range requests. + pub(crate) request_span: Span, } -enum ByRangeRequest { +pub enum ByRangeRequest { Active(I), Complete(T), } @@ -30,25 +50,54 @@ enum RangeBlockDataRequest { DataColumnsByRangeRequestId, ByRangeRequest>, >, + /// The column indices corresponding to the request + column_peers: HashMap>, expected_custody_columns: Vec, + attempt: usize, }, } +#[derive(Debug)] +pub(crate) enum CouplingError { + InternalError(String), + /// The peer we requested the columns from was faulty/malicious + DataColumnPeerFailure { + error: String, + faulty_peers: Vec<(ColumnIndex, PeerId)>, + exceeded_retries: bool, + }, + BlobPeerFailure(String), +} + impl RangeBlockComponentsRequest { + /// Creates a new range request for blocks and their associated data (blobs or data columns). + /// + /// # Arguments + /// * `blocks_req_id` - Request ID for the blocks + /// * `blobs_req_id` - Optional request ID for blobs (pre-Fulu fork) + /// * `data_columns` - Optional tuple of (request_id->column_indices pairs, expected_custody_columns) for Fulu fork + #[allow(clippy::type_complexity)] pub fn new( blocks_req_id: BlocksByRangeRequestId, blobs_req_id: Option, - data_columns: Option<(Vec, Vec)>, + data_columns: Option<( + Vec<(DataColumnsByRangeRequestId, Vec)>, + Vec, + )>, + request_span: Span, ) -> Self { let block_data_request = if let Some(blobs_req_id) = blobs_req_id { RangeBlockDataRequest::Blobs(ByRangeRequest::Active(blobs_req_id)) } else if let Some((requests, expected_custody_columns)) = data_columns { + let column_peers: HashMap<_, _> = requests.into_iter().collect(); RangeBlockDataRequest::DataColumns { - requests: requests - .into_iter() - .map(|id| (id, ByRangeRequest::Active(id))) + requests: column_peers + .keys() + .map(|id| (*id, ByRangeRequest::Active(*id))) .collect(), + column_peers, expected_custody_columns, + attempt: 0, } } else { RangeBlockDataRequest::NoData @@ -57,9 +106,36 @@ impl RangeBlockComponentsRequest { Self { blocks_request: ByRangeRequest::Active(blocks_req_id), block_data_request, + request_span, } } + /// Modifies `self` by inserting a new `DataColumnsByRangeRequestId` for a formerly failed + /// request for some columns. + pub fn reinsert_failed_column_requests( + &mut self, + failed_column_requests: Vec<(DataColumnsByRangeRequestId, Vec)>, + ) -> Result<(), String> { + match &mut self.block_data_request { + RangeBlockDataRequest::DataColumns { + requests, + expected_custody_columns: _, + column_peers, + attempt: _, + } => { + for (request, columns) in failed_column_requests.into_iter() { + requests.insert(request, ByRangeRequest::Active(request)); + column_peers.insert(request, columns); + } + Ok(()) + } + _ => Err("not a column request".to_string()), + } + } + + /// Adds received blocks to the request. + /// + /// Returns an error if the request ID doesn't match the expected blocks request. pub fn add_blocks( &mut self, req_id: BlocksByRangeRequestId, @@ -68,6 +144,10 @@ impl RangeBlockComponentsRequest { self.blocks_request.finish(req_id, blocks) } + /// Adds received blobs to the request. + /// + /// Returns an error if this request expects data columns instead of blobs, + /// or if the request ID doesn't match. pub fn add_blobs( &mut self, req_id: BlobsByRangeRequestId, @@ -75,13 +155,17 @@ impl RangeBlockComponentsRequest { ) -> Result<(), String> { match &mut self.block_data_request { RangeBlockDataRequest::NoData => Err("received blobs but expected no data".to_owned()), - RangeBlockDataRequest::Blobs(ref mut req) => req.finish(req_id, blobs), + RangeBlockDataRequest::Blobs(req) => req.finish(req_id, blobs), RangeBlockDataRequest::DataColumns { .. } => { Err("received blobs but expected data columns".to_owned()) } } } + /// Adds received custody columns to the request. + /// + /// Returns an error if this request expects blobs instead of data columns, + /// or if the request ID is unknown. pub fn add_custody_columns( &mut self, req_id: DataColumnsByRangeRequestId, @@ -94,9 +178,7 @@ impl RangeBlockComponentsRequest { RangeBlockDataRequest::Blobs(_) => { Err("received data columns but expected blobs".to_owned()) } - RangeBlockDataRequest::DataColumns { - ref mut requests, .. - } => { + RangeBlockDataRequest::DataColumns { requests, .. } => { let req = requests .get_mut(&req_id) .ok_or(format!("unknown data columns by range req_id {req_id}"))?; @@ -105,12 +187,21 @@ impl RangeBlockComponentsRequest { } } - pub fn responses(&self, spec: &ChainSpec) -> Option>, String>> { + /// Attempts to construct RPC blocks from all received components. + /// + /// Returns `None` if not all expected requests have completed. + /// Returns `Some(Ok(_))` with valid RPC blocks if all data is present and valid. + /// Returns `Some(Err(_))` if there are issues coupling blocks with their data. + pub fn responses( + &mut self, + spec: &ChainSpec, + ) -> Option>, CouplingError>> { let Some(blocks) = self.blocks_request.to_finished() else { return None; }; - match &self.block_data_request { + // Increment the attempt once this function returns the response or errors + match &mut self.block_data_request { RangeBlockDataRequest::NoData => { Some(Self::responses_with_blobs(blocks.to_vec(), vec![], spec)) } @@ -127,8 +218,11 @@ impl RangeBlockComponentsRequest { RangeBlockDataRequest::DataColumns { requests, expected_custody_columns, + column_peers, + attempt, } => { let mut data_columns = vec![]; + let mut column_to_peer_id: HashMap = HashMap::new(); for req in requests.values() { let Some(data) = req.to_finished() else { return None; @@ -136,12 +230,41 @@ impl RangeBlockComponentsRequest { data_columns.extend(data.clone()) } - Some(Self::responses_with_custody_columns( + // An "attempt" is complete here after we have received a response for all the + // requests we made. i.e. `req.to_finished()` returns Some for all requests. + *attempt += 1; + + // Note: this assumes that only 1 peer is responsible for a column + // with a batch. + for (id, columns) in column_peers { + for column in columns { + column_to_peer_id.insert(*column, id.peer); + } + } + + let resp = Self::responses_with_custody_columns( blocks.to_vec(), data_columns, + column_to_peer_id, expected_custody_columns, - spec, - )) + *attempt, + ); + + if let Err(CouplingError::DataColumnPeerFailure { + error: _, + faulty_peers, + exceeded_retries: _, + }) = &resp + { + for (_, peer) in faulty_peers.iter() { + // find the req id associated with the peer and + // delete it from the entries as we are going to make + // a separate attempt for those components. + requests.retain(|&k, _| k.peer != *peer); + } + } + + Some(resp) } } } @@ -150,7 +273,7 @@ impl RangeBlockComponentsRequest { blocks: Vec>>, blobs: Vec>>, spec: &ChainSpec, - ) -> Result>, String> { + ) -> Result>, CouplingError> { // There can't be more more blobs than blocks. i.e. sending any blob (empty // included) for a skipped slot is not permitted. let mut responses = Vec::with_capacity(blocks.len()); @@ -159,23 +282,28 @@ impl RangeBlockComponentsRequest { let max_blobs_per_block = spec.max_blobs_per_block(block.epoch()) as usize; let mut blob_list = Vec::with_capacity(max_blobs_per_block); while { - let pair_next_blob = blob_iter + blob_iter .peek() .map(|sidecar| sidecar.slot() == block.slot()) - .unwrap_or(false); - pair_next_blob + .unwrap_or(false) } { - blob_list.push(blob_iter.next().ok_or("Missing next blob".to_string())?); + blob_list.push(blob_iter.next().ok_or_else(|| { + CouplingError::BlobPeerFailure("Missing next blob".to_string()) + })?); } let mut blobs_buffer = vec![None; max_blobs_per_block]; for blob in blob_list { let blob_index = blob.index as usize; let Some(blob_opt) = blobs_buffer.get_mut(blob_index) else { - return Err("Invalid blob index".to_string()); + return Err(CouplingError::BlobPeerFailure( + "Invalid blob index".to_string(), + )); }; if blob_opt.is_some() { - return Err("Repeat blob index".to_string()); + return Err(CouplingError::BlobPeerFailure( + "Repeat blob index".to_string(), + )); } else { *blob_opt = Some(blob); } @@ -184,13 +312,22 @@ impl RangeBlockComponentsRequest { blobs_buffer.into_iter().flatten().collect::>(), max_blobs_per_block, ) - .map_err(|_| "Blobs returned exceeds max length".to_string())?; - responses.push(RpcBlock::new(None, block, Some(blobs)).map_err(|e| format!("{e:?}"))?) + .map_err(|_| { + CouplingError::BlobPeerFailure("Blobs returned exceeds max length".to_string()) + })?; + responses.push( + RpcBlock::new(None, block, Some(blobs)) + .map_err(|e| CouplingError::BlobPeerFailure(format!("{e:?}")))?, + ) } - // if accumulated sidecars is not empty, throw an error. + // if accumulated sidecars is not empty, log an error but return the responses + // as we can still make progress. if blob_iter.next().is_some() { - return Err("Received sidecars that don't pair well".to_string()); + let remaining_blobs = blob_iter + .map(|b| (b.index, b.block_root())) + .collect::>(); + debug!(?remaining_blobs, "Received sidecars that don't pair well",); } Ok(responses) @@ -199,9 +336,10 @@ impl RangeBlockComponentsRequest { fn responses_with_custody_columns( blocks: Vec>>, data_columns: DataColumnSidecarList, + column_to_peer: HashMap, expects_custody_columns: &[ColumnIndex], - spec: &ChainSpec, - ) -> Result>, String> { + attempt: usize, + ) -> Result>, CouplingError> { // Group data columns by block_root and index let mut data_columns_by_block = HashMap::>>>::new(); @@ -215,9 +353,12 @@ impl RangeBlockComponentsRequest { .insert(index, column) .is_some() { - return Err(format!( - "Repeated column block_root {block_root:?} index {index}" - )); + // `DataColumnsByRangeRequestItems` ensures that we do not request any duplicated indices across all peers + // we request the data from. + // If there are duplicated indices, its likely a peer sending us the same index multiple times. + // However we can still proceed even if there are extra columns, just log an error. + tracing::debug!(?block_root, ?index, "Repeated column for block_root"); + continue; } } @@ -225,56 +366,69 @@ impl RangeBlockComponentsRequest { // plus we have columns for our custody requirements let mut rpc_blocks = Vec::with_capacity(blocks.len()); + let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; for block in blocks { let block_root = get_block_root(&block); rpc_blocks.push(if block.num_expected_blobs() > 0 { let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) else { - // This PR ignores the fix from https://github.com/sigp/lighthouse/pull/5675 - // which allows blobs to not match blocks. - // TODO(das): on the initial version of PeerDAS the beacon chain does not check - // rpc custody requirements and dropping this check can allow the block to have - // an inconsistent DB. - return Err(format!("No columns for block {block_root:?} with data")); + let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); + return Err(CouplingError::DataColumnPeerFailure { + error: format!("No columns for block {block_root:?} with data"), + faulty_peers: responsible_peers, + exceeded_retries, + + }); }; let mut custody_columns = vec![]; + let mut naughty_peers = vec![]; for index in expects_custody_columns { - let Some(data_column) = data_columns_by_index.remove(index) else { - return Err(format!("No column for block {block_root:?} index {index}")); - }; // Safe to convert to `CustodyDataColumn`: we have asserted that the index of // this column is in the set of `expects_custody_columns` and with the expected // block root, so for the expected epoch of this batch. - custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); + if let Some(data_column) = data_columns_by_index.remove(index) { + custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); + } else { + let Some(responsible_peer) = column_to_peer.get(index) else { + return Err(CouplingError::InternalError(format!("Internal error, no request made for column {}", index))); + }; + naughty_peers.push((*index, *responsible_peer)); + } + } + if !naughty_peers.is_empty() { + return Err(CouplingError::DataColumnPeerFailure { + error: format!("Peers did not return column for block_root {block_root:?} {naughty_peers:?}"), + faulty_peers: naughty_peers, + exceeded_retries + }); } // Assert that there are no columns left if !data_columns_by_index.is_empty() { let remaining_indices = data_columns_by_index.keys().collect::>(); - return Err(format!( - "Not all columns consumed for block {block_root:?}: {remaining_indices:?}" - )); + // log the error but don't return an error, we can still progress with extra columns. + tracing::debug!( + ?block_root, + ?remaining_indices, + "Not all columns consumed for block" + ); } - RpcBlock::new_with_custody_columns( - Some(block_root), - block, - custody_columns, - expects_custody_columns.len(), - spec, - ) - .map_err(|e| format!("{e:?}"))? + RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns) + .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? } else { // Block has no data, expects zero columns - RpcBlock::new_without_blobs(Some(block_root), block, 0) + RpcBlock::new_without_blobs(Some(block_root), block) }); } // Assert that there are no columns left for other blocks if !data_columns_by_block.is_empty() { let remaining_roots = data_columns_by_block.keys().collect::>(); - return Err(format!("Not all columns consumed: {remaining_roots:?}")); + // log the error but don't return an error, we can still progress with responses. + // this is most likely an internal error with overrequesting or a client bug. + tracing::debug!(?remaining_roots, "Not all columns consumed for block"); } Ok(rpc_blocks) @@ -282,7 +436,7 @@ impl RangeBlockComponentsRequest { } impl ByRangeRequest { - fn finish(&mut self, id: I, data: T) -> Result<(), String> { + pub fn finish(&mut self, id: I, data: T) -> Result<(), String> { match self { Self::Active(expected_id) => { if expected_id != &id { @@ -295,7 +449,7 @@ impl ByRangeRequest { } } - fn to_finished(&self) -> Option<&T> { + pub fn to_finished(&self) -> Option<&T> { match self { Self::Active(_) => None, Self::Complete(data) => Some(data), @@ -306,16 +460,21 @@ impl ByRangeRequest { #[cfg(test)] mod tests { use super::RangeBlockComponentsRequest; + use crate::sync::network_context::MAX_COLUMN_RETRIES; use beacon_chain::test_utils::{ - generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, NumBlobs, + NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, }; - use lighthouse_network::service::api_types::{ - BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, - DataColumnsByRangeRequestId, Id, RangeRequestId, + use lighthouse_network::{ + PeerId, + service::api_types::{ + BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, RangeRequestId, + }, }; use rand::SeedableRng; use std::sync::Arc; - use types::{test_utils::XorShiftRng, Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + use tracing::Span; + use types::{Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock, test_utils::XorShiftRng}; fn components_id() -> ComponentsByRangeRequestId { ComponentsByRangeRequestId { @@ -343,33 +502,34 @@ mod tests { fn columns_id( id: Id, - parent_request_id: ComponentsByRangeRequestId, + parent_request_id: DataColumnsByRangeRequester, ) -> DataColumnsByRangeRequestId { DataColumnsByRangeRequestId { id, parent_request_id, + peer: PeerId::random(), } } - fn is_finished(info: &RangeBlockComponentsRequest) -> bool { + fn is_finished(info: &mut RangeBlockComponentsRequest) -> bool { let spec = test_spec::(); info.responses(&spec).is_some() } #[test] fn no_blobs_into_responses() { - let spec = test_spec::(); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { - generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng, &spec) + generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng) .0 .into() }) .collect::>>>(); let blocks_req_id = blocks_id(components_id()); - let mut info = RangeBlockComponentsRequest::::new(blocks_req_id, None, None); + let mut info = + RangeBlockComponentsRequest::::new(blocks_req_id, None, None, Span::none()); // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); @@ -380,27 +540,25 @@ mod tests { #[test] fn empty_blobs_into_responses() { - let spec = test_spec::(); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { // Always generate some blobs. - generate_rand_block_and_blobs::( - ForkName::Deneb, - NumBlobs::Number(3), - &mut rng, - &spec, - ) - .0 - .into() + generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Number(3), &mut rng) + .0 + .into() }) .collect::>>>(); let components_id = components_id(); let blocks_req_id = blocks_id(components_id); let blobs_req_id = blobs_id(components_id); - let mut info = - RangeBlockComponentsRequest::::new(blocks_req_id, Some(blobs_req_id), None); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + Some(blobs_req_id), + None, + Span::none(), + ); // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); @@ -434,12 +592,21 @@ mod tests { let columns_req_id = expects_custody_columns .iter() .enumerate() - .map(|(i, _)| columns_id(i as Id, components_id)) + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) .collect::>(); let mut info = RangeBlockComponentsRequest::::new( blocks_req_id, None, Some((columns_req_id.clone(), expects_custody_columns.clone())), + Span::none(), ); // Send blocks and complete terminate response info.add_blocks( @@ -448,12 +615,13 @@ mod tests { ) .unwrap(); // Assert response is not finished - assert!(!is_finished(&info)); + assert!(!is_finished(&mut info)); // Send data columns for (i, &column_index) in expects_custody_columns.iter().enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); info.add_custody_columns( - columns_req_id.get(i).copied().unwrap(), + *req, blocks .iter() .flat_map(|b| b.1.iter().filter(|d| d.index == column_index).cloned()) @@ -463,7 +631,7 @@ mod tests { if i < expects_custody_columns.len() - 1 { assert!( - !is_finished(&info), + !is_finished(&mut info), "requested should not be finished at loop {i}" ); } @@ -491,13 +659,22 @@ mod tests { let columns_req_id = batched_column_requests .iter() .enumerate() - .map(|(i, _)| columns_id(i as Id, components_id)) + .map(|(i, columns)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + columns.clone(), + ) + }) .collect::>(); let mut info = RangeBlockComponentsRequest::::new( blocks_req_id, None, Some((columns_req_id.clone(), expects_custody_columns.clone())), + Span::none(), ); let mut rng = XorShiftRng::from_seed([42; 16]); @@ -519,12 +696,13 @@ mod tests { ) .unwrap(); // Assert response is not finished - assert!(!is_finished(&info)); + assert!(!is_finished(&mut info)); for (i, column_indices) in batched_column_requests.iter().enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); // Send the set of columns in the same batch request info.add_custody_columns( - columns_req_id.get(i).copied().unwrap(), + *req, blocks .iter() .flat_map(|b| { @@ -538,7 +716,7 @@ mod tests { if i < num_of_data_column_requests - 1 { assert!( - !is_finished(&info), + !is_finished(&mut info), "requested should not be finished at loop {i}" ); } @@ -547,4 +725,277 @@ mod tests { // All completed construct response info.responses(&spec).unwrap().unwrap(); } + + #[test] + fn missing_custody_columns_from_faulty_peers() { + // GIVEN: A request expecting custody columns from multiple peers + let spec = test_spec::(); + let expected_custody_columns = vec![1, 2, 3, 4]; + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..2) + .map(|_| { + generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ) + }) + .collect::>(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Span::none(), + ); + + // AND: All blocks are received successfully + info.add_blocks( + blocks_req_id, + blocks.iter().map(|b| b.0.clone().into()).collect(), + ) + .unwrap(); + + // AND: Only some custody columns are received (columns 1 and 2) + for (i, &column_index) in expected_custody_columns.iter().take(2).enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); + info.add_custody_columns( + *req, + blocks + .iter() + .flat_map(|b| b.1.iter().filter(|d| d.index == column_index).cloned()) + .collect(), + ) + .unwrap(); + } + + // AND: Remaining column requests are completed with empty data (simulating faulty peers) + for i in 2..4 { + let (req, _columns) = columns_req_id.get(i).unwrap(); + info.add_custody_columns(*req, vec![]).unwrap(); + } + + // WHEN: Attempting to construct RPC blocks + let result = info.responses(&spec).unwrap(); + + // THEN: Should fail with PeerFailure identifying the faulty peers + assert!(result.is_err()); + if let Err(super::CouplingError::DataColumnPeerFailure { + error, + faulty_peers, + exceeded_retries, + }) = result + { + assert!(error.contains("Peers did not return column")); + assert_eq!(faulty_peers.len(), 2); // columns 3 and 4 missing + assert_eq!(faulty_peers[0].0, 3); // column index 3 + assert_eq!(faulty_peers[1].0, 4); // column index 4 + assert!(!exceeded_retries); // First attempt, should be false + } else { + panic!("Expected PeerFailure error"); + } + } + + #[test] + fn retry_logic_after_peer_failures() { + // GIVEN: A request expecting custody columns where some peers initially fail + let spec = test_spec::(); + let expected_custody_columns = vec![1, 2]; + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..2) + .map(|_| { + generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ) + }) + .collect::>(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Span::none(), + ); + + // AND: All blocks are received + info.add_blocks( + blocks_req_id, + blocks.iter().map(|b| b.0.clone().into()).collect(), + ) + .unwrap(); + + // AND: Only partial custody columns are received (column 1 but not 2) + let (req1, _) = columns_req_id.first().unwrap(); + info.add_custody_columns( + *req1, + blocks + .iter() + .flat_map(|b| b.1.iter().filter(|d| d.index == 1).cloned()) + .collect(), + ) + .unwrap(); + + // AND: The missing column request is completed with empty data (peer failure) + let (req2, _) = columns_req_id.get(1).unwrap(); + info.add_custody_columns(*req2, vec![]).unwrap(); + + // WHEN: First attempt to get responses fails + let result = info.responses(&spec).unwrap(); + assert!(result.is_err()); + + // AND: We retry with a new peer for the failed column + let new_columns_req_id = columns_id( + 10 as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ); + let failed_column_requests = vec![(new_columns_req_id, vec![2])]; + info.reinsert_failed_column_requests(failed_column_requests) + .unwrap(); + + // AND: The new peer provides the missing column data + info.add_custody_columns( + new_columns_req_id, + blocks + .iter() + .flat_map(|b| b.1.iter().filter(|d| d.index == 2).cloned()) + .collect(), + ) + .unwrap(); + + // WHEN: Attempting to get responses again + let result = info.responses(&spec).unwrap(); + + // THEN: Should succeed with complete RPC blocks + assert!(result.is_ok()); + let rpc_blocks = result.unwrap(); + assert_eq!(rpc_blocks.len(), 2); + } + + #[test] + fn max_retries_exceeded_behavior() { + // GIVEN: A request where peers consistently fail to provide required columns + let spec = test_spec::(); + let expected_custody_columns = vec![1, 2]; + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..1) + .map(|_| { + generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ) + }) + .collect::>(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Span::none(), + ); + + // AND: All blocks are received + info.add_blocks( + blocks_req_id, + blocks.iter().map(|b| b.0.clone().into()).collect(), + ) + .unwrap(); + + // AND: Only partial custody columns are provided (column 1 but not 2) + let (req1, _) = columns_req_id.first().unwrap(); + info.add_custody_columns( + *req1, + blocks + .iter() + .flat_map(|b| b.1.iter().filter(|d| d.index == 1).cloned()) + .collect(), + ) + .unwrap(); + + // AND: Column 2 request completes with empty data (persistent peer failure) + let (req2, _) = columns_req_id.get(1).unwrap(); + info.add_custody_columns(*req2, vec![]).unwrap(); + + // WHEN: Multiple retry attempts are made (up to max retries) + for _ in 0..MAX_COLUMN_RETRIES { + let result = info.responses(&spec).unwrap(); + assert!(result.is_err()); + + if let Err(super::CouplingError::DataColumnPeerFailure { + exceeded_retries, .. + }) = &result + && *exceeded_retries + { + break; + } + } + + // AND: One final attempt after exceeding max retries + let result = info.responses(&spec).unwrap(); + + // THEN: Should fail with exceeded_retries = true + assert!(result.is_err()); + if let Err(super::CouplingError::DataColumnPeerFailure { + error: _, + faulty_peers, + exceeded_retries, + }) = result + { + assert_eq!(faulty_peers.len(), 1); // column 2 missing + assert_eq!(faulty_peers[0].0, 2); // column index 2 + assert!(exceeded_retries); // Should be true after max retries + } else { + panic!("Expected PeerFailure error with exceeded_retries=true"); + } + } } diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs new file mode 100644 index 0000000000..bb2c6799f1 --- /dev/null +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -0,0 +1,1126 @@ +use std::{ + collections::{BTreeMap, HashSet, btree_map::Entry}, + marker::PhantomData, + sync::Arc, +}; + +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use lighthouse_network::{ + NetworkGlobals, PeerAction, PeerId, + service::api_types::{CustodyBackFillBatchRequestId, CustodyBackfillBatchId}, + types::CustodyBackFillState, +}; +use lighthouse_tracing::SPAN_CUSTODY_BACKFILL_SYNC_BATCH_REQUEST; +use logging::crit; +use std::hash::{DefaultHasher, Hash, Hasher}; +use tracing::{debug, error, info, info_span, warn}; +use types::{DataColumnSidecarList, Epoch, EthSpec}; + +use crate::sync::{ + backfill_sync::{BACKFILL_EPOCHS_PER_BATCH, ProcessResult, SyncStart}, + batch::{ + BatchConfig, BatchId, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, + ByRangeRequestType, + }, + block_sidecar_coupling::CouplingError, + manager::CustodyBatchProcessResult, + network_context::{RpcResponseError, SyncNetworkContext}, +}; + +/// The maximum number of batches to queue before requesting more. +const BACKFILL_BATCH_BUFFER_SIZE: u8 = 5; + +/// Columns are downloaded in batches from peers. This constant specifies how many epochs worth of +/// columns per batch are requested _at most_. A batch may request less columns to account for +/// already requested columns. There is a timeout for each batch request. If this value is too high, +/// we will negatively report peers with poor bandwidth. This can be set arbitrarily high, in which +/// case the responder will fill the response up to the max request size, assuming they have the +/// bandwidth to do so. +pub const CUSTODY_BACKFILL_EPOCHS_PER_BATCH: u64 = 1; + +type CustodyBackFillBatchInfo = + BatchInfo, DataColumnSidecarList>; +type CustodyBackFillBatches = BTreeMap>; + +#[derive(Debug)] +pub struct CustodyBackFillBatchConfig { + marker: PhantomData, +} + +impl BatchConfig for CustodyBackFillBatchConfig { + fn max_batch_download_attempts() -> u8 { + 5 + } + fn max_batch_processing_attempts() -> u8 { + 5 + } + fn batch_attempt_hash(data: &D) -> u64 { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() + } +} + +/// The ways a custody backfill sync can fail. +// The info in the enum variants is displayed in logging, clippy thinks it's dead code. +#[derive(Debug)] +pub enum CustodyBackfillError { + /// A batch failed to be downloaded. + BatchDownloadFailed(#[allow(dead_code)] BatchId), + /// A batch could not be processed. + BatchProcessingFailed(#[allow(dead_code)] BatchId), + /// A batch entered an invalid state. + BatchInvalidState(#[allow(dead_code)] BatchId, #[allow(dead_code)] String), + /// The sync algorithm entered an invalid state. + InvalidSyncState(#[allow(dead_code)] String), + /// The chain became paused. + Paused, +} + +pub struct CustodyBackFillSync { + /// Keeps track of the current progress of the custody backfill. + /// This only gets refreshed from the beacon chain if we enter a failed state. + current_start: BatchId, + + /// Starting epoch of the batch that needs to be processed next. + /// This is incremented as the chain advances. + processing_target: BatchId, + + /// The custody group count we are trying to fulfill up to the DA window. + /// This is used as an indicator to restart custody backfill sync if the cgc + /// was changed in the middle of a currently active sync. + cgc: u64, + + /// Run ID of this backfill process. Increments if sync restarts. Used to differentiate batch + /// results from different runs. + run_id: u64, + + /// Starting epoch of the next batch that needs to be downloaded. + to_be_downloaded: BatchId, + + /// Keeps track if we have requested the final batch. + last_batch_downloaded: bool, + + /// Sorted map of batches undergoing some kind of processing. + batches: CustodyBackFillBatches, + + /// The current processing batch, if any. + current_processing_batch: Option, + + /// Batches validated. + validated_batches: u64, + + /// These are batches that we've skipped because we have no columns to fetch for the epoch. + skipped_batches: HashSet, + + /// When a custody backfill sync fails, we keep track of whether a new fully synced peer has joined. + /// This signifies that we are able to attempt to restart a failed chain. + restart_failed_sync: bool, + + /// Reference to the beacon chain to obtain initial starting points for custody backfill sync. + beacon_chain: Arc>, + + /// Reference to the network globals in order to obtain valid peers to backfill columns from + /// (i.e synced peers). + network_globals: Arc>, +} + +impl CustodyBackFillSync { + pub fn new( + beacon_chain: Arc>, + network_globals: Arc>, + ) -> Self { + Self { + current_start: Epoch::new(0), + processing_target: Epoch::new(0), + cgc: 0, + run_id: 0, + to_be_downloaded: Epoch::new(0), + last_batch_downloaded: false, + batches: BTreeMap::new(), + skipped_batches: HashSet::new(), + current_processing_batch: None, + validated_batches: 0, + restart_failed_sync: false, + beacon_chain, + network_globals, + } + } + + /// Pauses the custody sync if it's currently syncing. + pub fn pause(&mut self, reason: String) { + if let CustodyBackFillState::Syncing = self.state() { + debug!(processed_epochs = %self.validated_batches, to_be_processed = %self.current_start,"Custody backfill sync paused"); + self.set_state(CustodyBackFillState::Pending(reason)); + } + } + + /// Checks if custody backfill sync should start and sets the missing columns + /// custody backfill sync will attempt to fetch. + /// The criteria to start custody sync is: + /// - The earliest data column epoch's custodied columns != previous epoch's custodied columns + /// - The earliest data column epoch is a finalied epoch + pub fn should_start_custody_backfill_sync(&mut self) -> bool { + let Some(da_boundary_epoch) = self.beacon_chain.get_column_da_boundary() else { + return false; + }; + + // This is the epoch in which we have met our current custody requirements + let Some(earliest_data_column_epoch) = + self.beacon_chain.earliest_custodied_data_column_epoch() + else { + return false; + }; + + // Check if we have missing columns between the da boundary and `earliest_data_column_epoch` + let missing_columns = self + .beacon_chain + .get_missing_columns_for_epoch(da_boundary_epoch); + + if !missing_columns.is_empty() { + let latest_finalized_epoch = self + .beacon_chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch; + + // Check that the earliest data column epoch is a finalized epoch. + return earliest_data_column_epoch <= latest_finalized_epoch; + } + + false + } + + fn restart_sync(&mut self) { + // Set state to paused + self.set_state(CustodyBackFillState::Pending( + "CGC count has changed and custody backfill sync needs to restart".to_string(), + )); + + // Remove all batches and active requests. + self.batches.clear(); + self.skipped_batches.clear(); + self.restart_failed_sync = false; + + // Reset all downloading and processing targets + // NOTE: Lets keep validated_batches for posterity + self.processing_target = Epoch::new(0); + self.to_be_downloaded = Epoch::new(0); + self.last_batch_downloaded = false; + self.current_processing_batch = None; + self.validated_batches = 0; + self.run_id += 1; + + self.set_start_epoch(); + self.set_cgc(); + } + + fn restart_if_required(&mut self) -> bool { + let cgc_at_head = self + .beacon_chain + .data_availability_checker + .custody_context() + .custody_group_count_at_head(&self.beacon_chain.spec); + + if cgc_at_head != self.cgc { + self.restart_sync(); + return true; + } + + false + } + + /// Starts syncing. + #[must_use = "A failure here indicates custody backfill sync has failed and the global sync state should be updated"] + pub fn start( + &mut self, + network: &mut SyncNetworkContext, + ) -> Result { + match self.state() { + CustodyBackFillState::Syncing => { + if self.restart_if_required() { + return Ok(SyncStart::NotSyncing); + } + + if self.check_completed() { + self.set_state(CustodyBackFillState::Completed); + return Ok(SyncStart::NotSyncing); + } + } + CustodyBackFillState::Pending(_) | CustodyBackFillState::Completed => { + if self.check_completed() { + self.set_state(CustodyBackFillState::Completed); + return Ok(SyncStart::NotSyncing); + } + self.set_cgc(); + + if !self.should_start_custody_backfill_sync() { + return Ok(SyncStart::NotSyncing); + } + self.set_start_epoch(); + if self + .network_globals + .peers + .read() + .synced_peers() + .next() + .is_some() + { + debug!( + run_id = self.run_id, + current_start = %self.current_start, + processing_target = %self.processing_target, + to_be_downloaded = %self.to_be_downloaded, + "Starting custody backfill sync" + ); + // If there are peers to resume with, begin the resume. + self.set_state(CustodyBackFillState::Syncing); + // Resume any previously failed batches. + self.resume_batches(network)?; + // begin requesting blocks from the peer pool, until all peers are exhausted. + self.request_batches(network)?; + + // start processing batches if needed + self.process_completed_batches(network)?; + } else { + return Ok(SyncStart::NotSyncing); + } + } + } + + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return Ok(SyncStart::NotSyncing); + }; + + Ok(SyncStart::Syncing { + completed: (self.validated_batches + * CUSTODY_BACKFILL_EPOCHS_PER_BATCH + * T::EthSpec::slots_per_epoch()) as usize, + remaining: self + .current_start + .end_slot(T::EthSpec::slots_per_epoch()) + .saturating_sub(column_da_boundary.start_slot(T::EthSpec::slots_per_epoch())) + .as_usize(), + }) + } + + fn set_cgc(&mut self) { + self.cgc = self + .beacon_chain + .data_availability_checker + .custody_context() + .custody_group_count_at_head(&self.beacon_chain.spec); + } + + fn set_start_epoch(&mut self) { + let earliest_data_column_epoch = self + .beacon_chain + .earliest_custodied_data_column_epoch() + .unwrap_or(Epoch::new(0)); + + self.current_start = earliest_data_column_epoch + 1; + self.processing_target = self.current_start; + self.to_be_downloaded = self.current_start; + } + + /// Attempts to request the next required batches from the peer pool. It will exhaust the peer + /// pool and left over batches until the batch buffer is reached or all peers are exhausted. + fn request_batches( + &mut self, + network: &mut SyncNetworkContext, + ) -> Result<(), CustodyBackfillError> { + if !matches!(self.state(), CustodyBackFillState::Syncing) { + return Ok(()); + } + + // find the next pending batch and request it from the peer + // Note: for this function to not infinite loop we must: + // - If `include_next_batch` returns Some we MUST increase the count of batches that are + // accounted in the `BACKFILL_BATCH_BUFFER_SIZE` limit in the `matches!` statement of + // that function. + while let Some(batch_id) = self.include_next_batch() { + // send the batch + self.send_batch(network, batch_id)?; + } + + // No more batches, simply stop + Ok(()) + } + + /// When resuming a chain, this function searches for batches that need to be re-downloaded and + /// transitions their state to redownload the batch. + fn resume_batches( + &mut self, + network: &mut SyncNetworkContext, + ) -> Result<(), CustodyBackfillError> { + let batch_ids_to_retry = self + .batches + .iter() + .filter_map(|(batch_id, batch)| { + // In principle there should only ever be on of these, and we could terminate the + // loop early, however the processing is negligible and we continue the search + // for robustness to handle potential future modification + if matches!(batch.state(), BatchState::AwaitingDownload) { + Some(*batch_id) + } else { + None + } + }) + .collect::>(); + + for batch_id in batch_ids_to_retry { + self.send_batch(network, batch_id)?; + } + Ok(()) + } + + /// Creates the next required batch from the chain. If there are no more batches required, + /// `None` is returned. + fn include_next_batch(&mut self) -> Option { + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return None; + }; + + // Skip all batches (Epochs) that don't have missing columns. + for epoch in Epoch::range_inclusive_rev(self.to_be_downloaded, column_da_boundary) { + let missing_columns = self.beacon_chain.get_missing_columns_for_epoch(epoch); + + if !missing_columns.is_empty() { + self.to_be_downloaded = epoch; + break; + } + + // This batch is being skipped, insert it into the skipped batches mapping. + self.skipped_batches.insert(epoch); + + if epoch == column_da_boundary { + return None; + } + } + + // Don't request batches before the column da boundary + if self.to_be_downloaded < column_da_boundary { + return None; + } + + // Don't request batches beyond the DA window + if self.last_batch_downloaded { + return None; + } + + // Only request batches up to the buffer size limit + // NOTE: we don't count batches in the AwaitingValidation state, to prevent stalling sync + // if the current processing window is contained in a long range of skip slots. + let in_buffer = |batch: &CustodyBackFillBatchInfo| { + matches!( + batch.state(), + BatchState::Downloading(..) | BatchState::AwaitingProcessing(..) + ) + }; + if self + .batches + .iter() + .filter(|&(_epoch, batch)| in_buffer(batch)) + .count() + > BACKFILL_BATCH_BUFFER_SIZE as usize + { + return None; + } + + let batch_id = self.to_be_downloaded; + + match self.batches.entry(batch_id) { + Entry::Occupied(_) => { + // this batch doesn't need downloading, let this same function decide the next batch + if self.would_complete(batch_id) { + self.last_batch_downloaded = true; + } + + self.to_be_downloaded = self + .to_be_downloaded + .saturating_sub(CUSTODY_BACKFILL_EPOCHS_PER_BATCH); + self.include_next_batch() + } + Entry::Vacant(entry) => { + let missing_columns = self.beacon_chain.get_missing_columns_for_epoch(batch_id); + entry.insert(BatchInfo::new( + &batch_id, + CUSTODY_BACKFILL_EPOCHS_PER_BATCH, + ByRangeRequestType::Columns(missing_columns), + )); + if self.would_complete(batch_id) { + self.last_batch_downloaded = true; + } + self.to_be_downloaded = self + .to_be_downloaded + .saturating_sub(CUSTODY_BACKFILL_EPOCHS_PER_BATCH); + Some(batch_id) + } + } + } + + /// Processes the batch with the given id. + /// The batch must exist and be ready for processing + fn process_batch( + &mut self, + network: &mut SyncNetworkContext, + batch_id: BatchId, + ) -> Result { + // Check if we need to restart custody backfill sync due to a recent cgc change + if self.restart_if_required() { + return Ok(ProcessResult::Successful); + } + + if self.state() != CustodyBackFillState::Syncing || self.current_processing_batch.is_some() + { + return Ok(ProcessResult::Successful); + } + + let Some(batch) = self.batches.get_mut(&batch_id) else { + return self + .fail_sync(CustodyBackfillError::InvalidSyncState(format!( + "Trying to process a batch that does not exist: {}", + batch_id + ))) + .map(|_| ProcessResult::Successful); + }; + + let (data_columns, _) = match batch.start_processing() { + Err(e) => { + return self + .fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0)) + .map(|_| ProcessResult::Successful); + } + Ok(v) => v, + }; + + self.current_processing_batch = Some(batch_id); + + if let Err(e) = network.beacon_processor().send_historic_data_columns( + CustodyBackfillBatchId { + epoch: batch_id, + run_id: self.run_id, + }, + data_columns, + self.cgc, + ) { + crit!( + msg = "process_batch", + error = %e, + batch = ?self.processing_target, + "Failed to send data columns to processor." + ); + // This is unlikely to happen but it would stall syncing since the batch now has no + // data columns to continue, and the chain is expecting a processing result that won't + // arrive. To mitigate this, (fake) fail this processing so that the batch is + // re-downloaded. + self.on_batch_process_result( + network, + CustodyBackfillBatchId { + epoch: batch_id, + run_id: self.run_id, + }, + &CustodyBatchProcessResult::Error { peer_action: None }, + ) + } else { + Ok(ProcessResult::Successful) + } + } + + /// A data column has been received for a batch. + /// If the column correctly completes the batch it will be processed if possible. + /// If this returns an error, custody sync has failed and will be restarted once new peers + /// join the system. + /// The sync manager should update the global sync state on failure. + #[must_use = "A failure here indicates custody backfill sync has failed and the global sync state should be updated"] + pub fn on_data_column_response( + &mut self, + network: &mut SyncNetworkContext, + req_id: CustodyBackFillBatchRequestId, + peer_id: &PeerId, + resp: Result, RpcResponseError>, + ) -> Result { + if req_id.batch_id.run_id != self.run_id { + debug!(%req_id, "Ignoring custody backfill download response from different run_id"); + return Ok(ProcessResult::Successful); + } + + let batch_id = req_id.batch_id.epoch; + // check if we have this batch + let Some(batch) = self.batches.get_mut(&batch_id) else { + if !matches!(self.state(), CustodyBackFillState::Pending(_)) { + // A batch might get removed when custody sync advances, so this is non fatal. + debug!(epoch = %batch_id, "Received a column for unknown batch"); + } + return Ok(ProcessResult::Successful); + }; + + // A batch could be retried without the peer failing the request (disconnecting/ + // sending an error /timeout) if the peer is removed for other + // reasons. Check that this column belongs to the expected peer, and that the + // request_id matches + if !batch.is_expecting_request_id(&req_id.id) { + return Ok(ProcessResult::Successful); + } + + match resp { + Ok(data_columns) => { + let received = data_columns.len(); + + match batch.download_completed(data_columns, *peer_id) { + Ok(_) => { + let awaiting_batches = self.processing_target.saturating_sub(batch_id) + / CUSTODY_BACKFILL_EPOCHS_PER_BATCH; + debug!( + %req_id, + blocks = received, + %awaiting_batches, + "Completed batch received" + ); + + // pre-emptively request more columns from peers whilst we process current columns. + self.request_batches(network)?; + self.process_completed_batches(network) + } + Err(e) => { + self.fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0))?; + Ok(ProcessResult::Successful) + } + } + } + Err(err) => { + debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); + + // If there are any coupling errors, penalize the appropriate peers + if let RpcResponseError::BlockComponentCouplingError(coupling_error) = err + && let CouplingError::DataColumnPeerFailure { + error, + faulty_peers, + exceeded_retries: _, + } = coupling_error + { + for (column_index, faulty_peer) in faulty_peers { + debug!( + ?error, + ?column_index, + ?faulty_peer, + "Custody backfill sync penalizing peer" + ); + network.report_peer( + faulty_peer, + PeerAction::LowToleranceError, + "Peer failed to serve column", + ); + } + } + + match batch.download_failed(Some(*peer_id)) { + Err(e) => { + self.fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0))?; + } + Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { + self.fail_sync(CustodyBackfillError::BatchDownloadFailed(batch_id))?; + } + Ok(BatchOperationOutcome::Continue) => { + self.send_batch(network, batch_id)?; + } + } + Ok(ProcessResult::Successful) + } + } + } + + /// The beacon processor has completed processing a batch. This function handles the result + /// of the batch processor. + /// If an error is returned custody backfill sync has failed. + #[must_use = "A failure here indicates custody backfill sync has failed and the global sync state should be updated"] + pub fn on_batch_process_result( + &mut self, + network: &mut SyncNetworkContext, + custody_batch_id: CustodyBackfillBatchId, + result: &CustodyBatchProcessResult, + ) -> Result { + let batch_id = custody_batch_id.epoch; + if custody_batch_id.run_id != self.run_id { + debug!(batch = %custody_batch_id, "Ignoring custody backfill error from different run_id"); + return Ok(ProcessResult::Successful); + } + + // The first two cases are possible in regular sync, should not occur in custody backfill, but we + // keep this logic for handling potential processing race conditions. + // result + let batch = match &self.current_processing_batch { + Some(processing_id) if *processing_id != batch_id => { + debug!( + batch_epoch = %batch_id, + expected_batch_epoch = processing_id.as_u64(), + "Unexpected batch result" + ); + return Ok(ProcessResult::Successful); + } + None => { + debug!(%batch_id, "Chain was not expecting a batch result"); + return Ok(ProcessResult::Successful); + } + _ => { + // batch_id matches, continue + self.current_processing_batch = None; + + match self.batches.get_mut(&batch_id) { + Some(batch) => batch, + None => { + // This is an error. Fail the sync algorithm. + return self + .fail_sync(CustodyBackfillError::InvalidSyncState(format!( + "Current processing batch not found: {}", + batch_id + ))) + .map(|_| ProcessResult::Successful); + } + } + } + }; + + let Some(peer) = batch.processing_peer() else { + self.fail_sync(CustodyBackfillError::BatchInvalidState( + batch_id, + String::from("Peer does not exist"), + ))?; + return Ok(ProcessResult::Successful); + }; + + debug!( + ?result, + batch_id = %custody_batch_id, + %peer, + client = %network.client_type(peer), + "Custody backfill batch processed" + ); + + match result { + CustodyBatchProcessResult::Success { + imported_columns, .. + } => { + if let Err(e) = batch.processing_completed(BatchProcessingResult::Success) { + self.fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0))?; + } + + debug!(imported_count=?imported_columns, "Succesfully imported historical data columns"); + + self.advance_custody_backfill_sync(batch_id); + + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return Err(CustodyBackfillError::InvalidSyncState( + "Can't calculate column data availability boundary".to_string(), + )); + }; + + if batch_id == self.processing_target { + // Advance processing target to the previous epoch + // If the current processing target is above the column DA boundary + if self.processing_target > column_da_boundary { + self.processing_target = self + .processing_target + .saturating_sub(CUSTODY_BACKFILL_EPOCHS_PER_BATCH); + } + } + + // check if custody sync has completed syncing up to the DA window + if self.check_completed() { + info!( + validated_epochs = ?self.validated_batches, + run_id = self.run_id, + "Custody backfill sync completed" + ); + self.batches.clear(); + self.restart_failed_sync = false; + self.processing_target = self.current_start; + self.to_be_downloaded = self.current_start; + self.last_batch_downloaded = false; + self.current_processing_batch = None; + self.validated_batches = 0; + self.skipped_batches.clear(); + self.set_state(CustodyBackFillState::Completed); + self.beacon_chain.update_data_column_custody_info(None); + Ok(ProcessResult::SyncCompleted) + } else { + // custody sync is not completed + // attempt to request more batches + self.request_batches(network)?; + // attempt to process more batches + self.process_completed_batches(network) + } + } + CustodyBatchProcessResult::Error { peer_action } => { + match peer_action { + // Faulty failure + Some(peer_action) => { + match batch.processing_completed(BatchProcessingResult::FaultyFailure) { + Err(e) => { + // Batch was in the wrong state + self.fail_sync(CustodyBackfillError::BatchInvalidState( + batch_id, e.0, + )) + .map(|_| ProcessResult::Successful) + } + Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { + warn!( + score_adjustment = ?peer_action, + batch_epoch = %batch_id, + "Custody backfill batch failed to download. Penalizing peers" + ); + self.fail_sync(CustodyBackfillError::BatchProcessingFailed( + batch_id, + )) + .map(|_| ProcessResult::Successful) + } + + Ok(BatchOperationOutcome::Continue) => { + self.advance_custody_backfill_sync(batch_id); + // Handle this invalid batch, that is within the re-process retries limit. + self.handle_invalid_batch(network, batch_id) + .map(|_| ProcessResult::Successful) + } + } + } + // Non faulty failure + None => { + if let Err(e) = + batch.processing_completed(BatchProcessingResult::NonFaultyFailure) + { + self.fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0))?; + } + self.send_batch(network, batch_id)?; + Ok(ProcessResult::Successful) + } + } + } + } + } + + /// Processes the next ready batch. + fn process_completed_batches( + &mut self, + network: &mut SyncNetworkContext, + ) -> Result { + // Only process batches if custody backfill is syncing and only process one batch at a time + if self.state() != CustodyBackFillState::Syncing || self.current_processing_batch.is_some() + { + return Ok(ProcessResult::Successful); + } + + // Don't try to process batches before the Fulu fork epoch since data columns don't exist + if let Some(fulu_fork_epoch) = self.beacon_chain.spec.fulu_fork_epoch + && self.processing_target < fulu_fork_epoch + { + return Ok(ProcessResult::Successful); + } + + // Check if we need to restart custody backfill sync due to a cgc change. + if self.restart_if_required() { + return Ok(ProcessResult::Successful); + } + + while self.skipped_batches.contains(&self.processing_target) { + self.skipped_batches.remove(&self.processing_target); + // Update data column custody info with the skipped batch + if let Err(e) = self + .beacon_chain + .safely_backfill_data_column_custody_info(self.processing_target) + { + // I can't see a scenario where this could happen, but if we don't + // handle this edge case custody backfill sync could be stuck indefinitely. + error!( + error=?e, + "Unable to update data column custody info, restarting sync" + ); + self.restart_sync(); + }; + self.processing_target -= BACKFILL_EPOCHS_PER_BATCH; + } + + // Find the id of the batch we are going to process. + if let Some(batch) = self.batches.get(&self.processing_target) { + let state = batch.state(); + match state { + BatchState::AwaitingProcessing(..) => { + return self.process_batch(network, self.processing_target); + } + BatchState::Downloading(..) => { + // Batch is not ready, nothing to process + } + // Batches can be in `AwaitingDownload` state if there weren't good data column subnet + // peers to send the request to. + BatchState::AwaitingDownload => return Ok(ProcessResult::Successful), + BatchState::AwaitingValidation(..) => { + // The batch is validated + } + BatchState::Poisoned => unreachable!("Poisoned batch"), + BatchState::Failed | BatchState::Processing(_) => { + // these are all inconsistent states: + // - Failed -> non recoverable batch. Columns should have been removed + // - AwaitingDownload -> A recoverable failed batch should have been + // re-requested. + // - Processing -> `self.current_processing_batch` is None + self.fail_sync(CustodyBackfillError::InvalidSyncState(String::from( + "Invalid expected batch state", + )))?; + return Ok(ProcessResult::Successful); + } + } + } else { + self.fail_sync(CustodyBackfillError::InvalidSyncState(format!( + "Batch not found for current processing target {}", + self.processing_target + )))?; + return Ok(ProcessResult::Successful); + } + Ok(ProcessResult::Successful) + } + + /// Removes any batches previous to the given `validating_epoch` and advance custody backfill sync + /// to `validating_epoch`. + /// + /// The `validating_epoch` must align with batch boundaries. + fn advance_custody_backfill_sync(&mut self, validating_epoch: Epoch) { + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return; + }; + // make sure this epoch produces an advancement, unless its at the column DA boundary + if validating_epoch >= self.current_start && validating_epoch > column_da_boundary { + return; + } + + // We can now validate higher batches than the current batch. Here we remove all + // batches that are higher than the current batch. We add on an extra + // `BACKFILL_EPOCHS_PER_BATCH` as `split_off` is inclusive. + let removed_batches = self + .batches + .split_off(&(validating_epoch + CUSTODY_BACKFILL_EPOCHS_PER_BATCH)); + + for (id, batch) in removed_batches.into_iter() { + self.validated_batches = self.validated_batches.saturating_add(1); + match batch.state() { + BatchState::Downloading(..) | BatchState::AwaitingValidation(..) => {} + BatchState::Failed | BatchState::Poisoned | BatchState::AwaitingDownload => { + crit!("Batch indicates inconsistent data columns while advancing custody sync") + } + BatchState::AwaitingProcessing(..) => {} + BatchState::Processing(_) => { + debug!(batch = %id, %batch, "Advancing custody sync while processing a batch"); + if let Some(processing_id) = self.current_processing_batch + && id >= processing_id + { + self.current_processing_batch = None; + } + } + } + } + + self.processing_target = self.processing_target.min(validating_epoch); + self.current_start = self.current_start.min(validating_epoch); + self.to_be_downloaded = self.to_be_downloaded.min(validating_epoch); + + if self.batches.contains_key(&self.to_be_downloaded) { + // if custody backfill sync is advanced by Range beyond the previous `self.to_be_downloaded`, we + // won't have this batch, so we need to request it. + self.to_be_downloaded -= CUSTODY_BACKFILL_EPOCHS_PER_BATCH; + } + debug!(?validating_epoch, processing_target = ?self.processing_target, "Custody backfill advanced"); + } + + /// An invalid batch has been received that could not be processed, but that can be retried. + /// + /// These events occur when a peer has successfully responded with columns, but the columns + /// received are incorrect or invalid. This indicates the peer has not performed as + /// intended and can result in down voting a peer. + fn handle_invalid_batch( + &mut self, + network: &mut SyncNetworkContext, + batch_id: BatchId, + ) -> Result<(), CustodyBackfillError> { + // The current batch could not be processed, indicating either the current or previous + // batches are invalid. + + // The previous batch could be incomplete due to the columns being too large to fit in + // a single RPC request or there could be consecutive empty batches which are not supposed + // to be there + + // The current (sub-optimal) strategy is to simply re-request all batches that could + // potentially be faulty. If a batch returns a different result than the original and + // results in successful processing, we downvote the original peer that sent us the batch. + + // this is our robust `processing_target`. All previous batches must be awaiting + // validation + let mut redownload_queue = Vec::new(); + + for (id, _) in self.batches.iter_mut().filter(|&(&id, _)| id > batch_id) { + redownload_queue.push(*id); + } + + // no batch maxed out it process attempts, so now the chain's volatile progress must be + // reset + self.processing_target = self.current_start; + + for id in redownload_queue { + self.send_batch(network, id)?; + } + // finally, re-request the failed batch. + self.send_batch(network, batch_id) + } + + /// Checks with the beacon chain if custody sync has completed. + fn check_completed(&mut self) -> bool { + if self.would_complete(self.current_start) { + // Check that the data column custody info `earliest_available_slot` + // is in an epoch that is less than or equal to the current DA boundary + let Some(earliest_data_column_epoch) = + self.beacon_chain.earliest_custodied_data_column_epoch() + else { + return false; + }; + + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return false; + }; + + return earliest_data_column_epoch <= column_da_boundary; + } + false + } + + /// Checks if custody backfill would complete by syncing to `start_epoch`. + fn would_complete(&self, start_epoch: Epoch) -> bool { + let Some(column_da_boundary) = self.beacon_chain.get_column_da_boundary() else { + return false; + }; + start_epoch <= column_da_boundary + } + + /// Requests the batch assigned to the given id from a given peer. + fn send_batch( + &mut self, + network: &mut SyncNetworkContext, + batch_id: BatchId, + ) -> Result<(), CustodyBackfillError> { + let span = info_span!(SPAN_CUSTODY_BACKFILL_SYNC_BATCH_REQUEST); + let _enter = span.enter(); + + if let Some(batch) = self.batches.get_mut(&batch_id) { + let synced_peers = self + .network_globals + .peers + .read() + .synced_peers_for_epoch(batch_id) + .cloned() + .collect::>(); + + let request = batch.to_data_columns_by_range_request().map_err(|_| { + CustodyBackfillError::InvalidSyncState( + "Can't convert to data column by range request".to_string(), + ) + })?; + let failed_peers = batch.failed_peers(); + + match network.custody_backfill_data_columns_batch_request( + request, + CustodyBackfillBatchId { + epoch: batch_id, + run_id: self.run_id, + }, + &synced_peers, + &failed_peers, + ) { + Ok(request_id) => { + // inform the batch about the new request + if let Err(e) = batch.start_downloading(request_id.id) { + return self + .fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0)); + } + debug!(epoch = %batch_id, %batch, "Requesting batch"); + + return Ok(()); + } + Err(e) => match e { + crate::sync::network_context::RpcRequestSendError::NoPeer(no_peer) => { + // If we are here we have no more synced peers + debug!( + "reason" = format!("insufficient_synced_peers({no_peer:?})"), + "Custody sync paused" + ); + self.pause("Insufficient peers".to_string()); + return Err(CustodyBackfillError::Paused); + } + crate::sync::network_context::RpcRequestSendError::InternalError(e) => { + // NOTE: under normal conditions this shouldn't happen but we handle it anyway + warn!(%batch_id, error = ?e, %batch,"Could not send batch request"); + // register the failed download and check if the batch can be retried + if let Err(e) = batch.start_downloading(1) { + return self + .fail_sync(CustodyBackfillError::BatchInvalidState(batch_id, e.0)); + } + + match batch.download_failed(None) { + Err(e) => self.fail_sync(CustodyBackfillError::BatchInvalidState( + batch_id, e.0, + ))?, + Ok(BatchOperationOutcome::Failed { blacklist: _ }) => { + self.fail_sync(CustodyBackfillError::BatchDownloadFailed(batch_id))? + } + Ok(BatchOperationOutcome::Continue) => { + return self.send_batch(network, batch_id); + } + } + } + }, + } + } + + Ok(()) + } + + /// The syncing process has failed. + /// + /// This resets past variables, to allow for a fresh start when resuming. + fn fail_sync(&mut self, error: CustodyBackfillError) -> Result<(), CustodyBackfillError> { + // Some errors shouldn't cause failure. + if matches!(error, CustodyBackfillError::Paused) { + return Ok(()); + } + + // Set the state + self.pause("Sync has failed".to_string()); + // Remove all batches and active requests. + self.batches.clear(); + self.restart_failed_sync = false; + + // Reset all downloading and processing targets + // NOTE: Lets keep validated_batches for posterity + self.processing_target = self.current_start; + self.to_be_downloaded = self.current_start; + self.last_batch_downloaded = false; + self.current_processing_batch = None; + self.restart_sync(); + + Err(error) + } + + pub fn state(&self) -> CustodyBackFillState { + self.network_globals.custody_sync_state.read().clone() + } + + /// Updates the global network state indicating the current state of a backfill sync. + pub fn set_state(&self, state: CustodyBackFillState) { + *self.network_globals.custody_sync_state.write() = state; + } + + /// A fully synced peer has joined us. + /// If we are in a failed state, update a local variable to indicate we are able to restart + /// the failed sync on the next attempt. + pub fn fully_synced_peer_joined(&mut self) { + if matches!(self.state(), CustodyBackFillState::Pending(_)) { + self.restart_failed_sync = true; + } + } +} diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 473881f182..338f21ce98 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -38,45 +38,44 @@ use super::block_lookups::BlockLookups; use super::network_context::{ CustodyByRootResult, RangeBlockComponent, RangeRequestId, RpcEvent, SyncNetworkContext, }; -use super::peer_sampling::{Sampling, SamplingConfig, SamplingResult}; -use super::peer_sync_info::{remote_sync_type, PeerSyncType}; -use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; +use super::peer_sync_info::{PeerSyncType, remote_sync_type}; +use super::range_sync::{EPOCHS_PER_BATCH, RangeSync, RangeSyncType}; use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; -use crate::sync::network_context::PeerGroup; +use crate::sync::custody_backfill_sync::CustodyBackFillSync; +use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, }; use futures::StreamExt; +use lighthouse_network::SyncInfo; use lighthouse_network::rpc::RPCError; use lighthouse_network::service::api_types::{ - BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyRequester, - DataColumnsByRangeRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, - SamplingId, SamplingRequester, SingleLookupReqId, SyncRequestId, + BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, + CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyRequester, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, + DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; -use lighthouse_network::SyncInfo; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; use lru_cache::LRUTimeCache; +use slot_clock::SlotClock; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use tracing::{debug, error, info, info_span, trace, warn, Instrument}; +use tracing::{debug, error, info, trace}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, }; -#[cfg(test)] -use types::ColumnIndex; - /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync /// from a peer. If a peer is within this tolerance (forwards or backwards), it is treated as a /// fully sync'd peer. @@ -146,10 +145,6 @@ pub enum SyncMessage { /// manager to attempt to find the block matching the unknown hash. UnknownBlockHashFromAttestation(PeerId, Hash256), - /// Request to start sampling a block. Caller should ensure that block has data before sending - /// the request. - SampleBlock(Hash256, Slot), - /// A peer has disconnected. Disconnect(PeerId), @@ -166,18 +161,18 @@ pub enum SyncMessage { result: BatchProcessResult, }, + /// A custody batch has been processed by the processor thread. + CustodyBatchProcessed { + batch_id: CustodyBackfillBatchId, + result: CustodyBatchProcessResult, + }, + /// Block processed BlockComponentProcessed { process_type: BlockProcessType, result: BlockProcessingResult, }, - /// Sample data column verified - SampleVerified { - id: SamplingId, - result: Result<(), String>, - }, - /// A block from gossip has completed processing, GossipBlockProcessResult { block_root: Hash256, imported: bool }, } @@ -223,6 +218,19 @@ pub enum BatchProcessResult { NonFaultyFailure, } +/// The result of processing multiple data columns. +#[derive(Debug)] +pub enum CustodyBatchProcessResult { + /// The custody batch was completed successfully. It carries whether the sent batch contained data columns. + Success { + #[allow(dead_code)] + sent_columns: usize, + imported_columns: usize, + }, + /// The custody batch processing failed. + Error { peer_action: Option }, +} + /// The primary object for handling and driving all the current syncing logic. It maintains the /// current state of the syncing process, the number of useful peers, downloaded blocks and /// controls the logic behind both the long-range (batch) sync and the on-going potential parent @@ -243,13 +251,14 @@ pub struct SyncManager { /// Backfill syncing. backfill_sync: BackFillSync, + /// Custody syncing. + custody_backfill_sync: CustodyBackFillSync, + block_lookups: BlockLookups, /// debounce duplicated `UnknownBlockHashFromAttestation` for the same root peer tuple. A peer /// may forward us thousands of a attestations, each one triggering an individual event. Only /// one event is useful, the rest generating log noise and wasted cycles notified_unknown_roots: LRUTimeCache<(PeerId, Hash256)>, - - sampling: Sampling, } /// Spawns a new `SyncManager` thread which has a weak reference to underlying beacon @@ -264,7 +273,10 @@ pub fn spawn( fork_context: Arc, ) { assert!( - beacon_chain.spec.max_request_blocks(fork_context.current_fork()) as u64 >= T::EthSpec::slots_per_epoch() * EPOCHS_PER_BATCH, + beacon_chain + .spec + .max_request_blocks(fork_context.current_fork_name()) as u64 + >= T::EthSpec::slots_per_epoch() * EPOCHS_PER_BATCH, "Max blocks that can be requested in a single batch greater than max allowed blocks in a single request" ); @@ -274,20 +286,12 @@ pub fn spawn( network_send, beacon_processor, sync_recv, - SamplingConfig::Default, fork_context, ); // spawn the sync manager thread debug!("Sync Manager started"); - executor.spawn( - async move { - Box::pin(sync_manager.main()) - .instrument(info_span!("", service = "sync")) - .await - }, - "sync", - ); + executor.spawn(async move { Box::pin(sync_manager.main()).await }, "sync"); } impl SyncManager { @@ -296,7 +300,6 @@ impl SyncManager { network_send: mpsc::UnboundedSender>, beacon_processor: Arc>, sync_recv: mpsc::UnboundedReceiver>, - sampling_config: SamplingConfig, fork_context: Arc, ) -> Self { let network_globals = beacon_processor.network_globals.clone(); @@ -310,12 +313,12 @@ impl SyncManager { fork_context.clone(), ), range_sync: RangeSync::new(beacon_chain.clone()), - backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals), + backfill_sync: BackFillSync::new(beacon_chain.clone(), network_globals.clone()), + custody_backfill_sync: CustodyBackFillSync::new(beacon_chain.clone(), network_globals), block_lookups: BlockLookups::new(), notified_unknown_roots: LRUTimeCache::new(Duration::from_secs( NOTIFIED_UNKNOWN_ROOT_EXPIRY_SECONDS, )), - sampling: Sampling::new(sampling_config), } } @@ -351,27 +354,13 @@ impl SyncManager { } #[cfg(test)] - pub(crate) fn get_failed_chains(&mut self) -> Vec { - self.block_lookups.get_failed_chains() + pub(crate) fn get_ignored_chains(&mut self) -> Vec { + self.block_lookups.get_ignored_chains() } #[cfg(test)] - pub(crate) fn insert_failed_chain(&mut self, block_root: Hash256) { - self.block_lookups.insert_failed_chain(block_root); - } - - #[cfg(test)] - pub(crate) fn active_sampling_requests(&self) -> Vec { - self.sampling.active_sampling_requests() - } - - #[cfg(test)] - pub(crate) fn get_sampling_request_status( - &self, - block_root: Hash256, - index: &ColumnIndex, - ) -> Option { - self.sampling.get_request_status(block_root, index) + pub(crate) fn insert_ignored_chain(&mut self, block_root: Hash256) { + self.block_lookups.insert_ignored_chain(block_root); } #[cfg(test)] @@ -398,10 +387,11 @@ impl SyncManager { // ensure the beacon chain still exists let status = self.chain.status_message(); let local = SyncInfo { - head_slot: status.head_slot, - head_root: status.head_root, - finalized_epoch: status.finalized_epoch, - finalized_root: status.finalized_root, + head_slot: *status.head_slot(), + head_root: *status.head_root(), + finalized_epoch: *status.finalized_epoch(), + finalized_root: *status.finalized_root(), + earliest_available_slot: status.earliest_available_slot().ok().cloned(), }; let sync_type = remote_sync_type(&local, &remote, &self.chain); @@ -450,10 +440,11 @@ impl SyncManager { ) { let status = self.chain.status_message(); let local = SyncInfo { - head_slot: status.head_slot, - head_root: status.head_root, - finalized_epoch: status.finalized_epoch, - finalized_root: status.finalized_root, + head_slot: *status.head_slot(), + head_root: *status.head_root(), + finalized_epoch: *status.finalized_epoch(), + finalized_root: *status.finalized_root(), + earliest_available_slot: status.earliest_available_slot().ok().cloned(), }; let head_slot = head_slot.unwrap_or_else(|| { @@ -471,6 +462,7 @@ impl SyncManager { // Set finalized to same as local to trigger Head sync finalized_epoch: local.finalized_epoch, finalized_root: local.finalized_root, + earliest_available_slot: local.earliest_available_slot, }; for peer_id in peers { @@ -583,6 +575,7 @@ impl SyncManager { // inform the backfill sync that a new synced peer has joined us. if new_state.is_synced() { self.backfill_sync.fully_synced_peer_joined(); + self.custody_backfill_sync.fully_synced_peer_joined(); } } is_connected @@ -592,17 +585,18 @@ impl SyncManager { } } - /// Updates the global sync state, optionally instigating or pausing a backfill sync as well as + /// Updates the global sync state, optionally instigating or pausing a backfill or custody sync as well as /// logging any changes. /// /// The logic for which sync should be running is as follows: - /// - If there is a range-sync running (or required) pause any backfill and let range-sync + /// - If there is a range-sync running (or required) pause any backfill/custody sync and let range-sync /// complete. /// - If there is no current range sync, check for any requirement to backfill and either /// start/resume a backfill sync if required. The global state will be BackFillSync if a /// backfill sync is running. /// - If there is no range sync and no required backfill and we have synced up to the currently /// known peers, we consider ourselves synced. + /// - If there is no range sync and no required backfill we check if we need to execute a custody sync. fn update_sync_state(&mut self) { let new_state: SyncState = match self.range_sync.state() { Err(e) => { @@ -658,15 +652,51 @@ impl SyncManager { error!(error = ?e, "Backfill sync failed to start"); } } + + // If backfill is complete, check if we have a pending custody backfill to complete + let anchor_info = self.chain.store.get_anchor_info(); + if anchor_info.block_backfill_complete(self.chain.genesis_backfill_slot) { + match self.custody_backfill_sync.start(&mut self.network) { + Ok(SyncStart::Syncing { + completed, + remaining, + }) => { + sync_state = SyncState::CustodyBackFillSyncing { + completed, + remaining, + }; + } + Ok(SyncStart::NotSyncing) => {} // Ignore updating the state if custody sync state didn't start. + Err(e) => { + use crate::sync::custody_backfill_sync::CustodyBackfillError; + + match &e { + CustodyBackfillError::BatchDownloadFailed(_) + | CustodyBackfillError::BatchProcessingFailed(_) => { + debug!(error=?e, "Custody backfill batch processing or downloading failed"); + } + CustodyBackfillError::BatchInvalidState(_, reason) => { + error!(error=?e, reason, "Custody backfill sync failed due to invalid batch state") + } + CustodyBackfillError::InvalidSyncState(reason) => { + error!(error=?e, reason, "Custody backfill sync failed due to invalid sync state") + } + CustodyBackfillError::Paused => {} + } + } + } + } } // Return the sync state if backfilling is not required. sync_state } Some((RangeSyncType::Finalized, start_slot, target_slot)) => { - // If there is a backfill sync in progress pause it. + // Range sync is in progress. If there is a backfill or custody sync in progress pause it. #[cfg(not(feature = "disable-backfill"))] self.backfill_sync.pause(); + self.custody_backfill_sync + .pause("Range sync in progress".to_string()); SyncState::SyncingFinalized { start_slot, @@ -674,9 +704,12 @@ impl SyncManager { } } Some((RangeSyncType::Head, start_slot, target_slot)) => { - // If there is a backfill sync in progress pause it. + // Range sync is in progress. If there is a backfill or custody backfill sync + // in progress pause it. #[cfg(not(feature = "disable-backfill"))] self.backfill_sync.pause(); + self.custody_backfill_sync + .pause("Range sync in progress".to_string()); SyncState::SyncingHead { start_slot, @@ -696,7 +729,9 @@ impl SyncManager { if new_state.is_synced() && !matches!( old_state, - SyncState::Synced | SyncState::BackFillSyncing { .. } + SyncState::Synced + | SyncState::BackFillSyncing { .. } + | SyncState::CustodyBackFillSyncing { .. } ) { self.network.subscribe_core_topics(); @@ -727,6 +762,11 @@ impl SyncManager { let mut register_metrics_interval = tokio::time::interval(Duration::from_secs(5)); + // Trigger a sync state update every epoch. This helps check if we need to trigger a custody backfill sync. + let epoch_duration = + self.chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); + let mut epoch_interval = tokio::time::interval(Duration::from_secs(epoch_duration)); + // process any inbound messages loop { tokio::select! { @@ -745,6 +785,9 @@ impl SyncManager { _ = register_metrics_interval.tick() => { self.network.register_metrics(); } + _ = epoch_interval.tick() => { + self.update_sync_state(); + } } } } @@ -850,15 +893,6 @@ impl SyncManager { self.handle_unknown_block_root(peer_id, block_root); } } - SyncMessage::SampleBlock(block_root, block_slot) => { - debug!(%block_root, slot = %block_slot, "Received SampleBlock message"); - if let Some((requester, result)) = self - .sampling - .on_new_sample_request(block_root, &mut self.network) - { - self.on_sampling_result(requester, result) - } - } SyncMessage::Disconnect(peer_id) => { debug!(%peer_id, "Received disconnected message"); self.peer_disconnect(&peer_id); @@ -908,12 +942,19 @@ impl SyncManager { } } }, - SyncMessage::SampleVerified { id, result } => { - if let Some((requester, result)) = - self.sampling - .on_sample_verified(id, result, &mut self.network) - { - self.on_sampling_result(requester, result) + SyncMessage::CustodyBatchProcessed { result, batch_id } => { + match self.custody_backfill_sync.on_batch_process_result( + &mut self.network, + batch_id, + &result, + ) { + Ok(ProcessResult::Successful) => {} + Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), + Err(error) => { + error!(error = ?error, "Custody sync failed"); + // Update the global status + self.update_sync_state(); + } } } } @@ -929,12 +970,20 @@ impl SyncManager { ) { match self.should_search_for_block(Some(slot), &peer_id) { Ok(_) => { - self.block_lookups.search_child_and_parent( + if self.block_lookups.search_child_and_parent( block_root, block_component, peer_id, &mut self.network, - ); + ) { + // Lookup created. No need to log here it's logged in `new_current_lookup` + } else { + debug!( + ?block_root, + ?parent_root, + "No lookup created for child and parent" + ); + } } Err(reason) => { debug!(%block_root, %parent_root, reason, "Ignoring unknown parent request"); @@ -945,8 +994,15 @@ impl SyncManager { fn handle_unknown_block_root(&mut self, peer_id: PeerId, block_root: Hash256) { match self.should_search_for_block(None, &peer_id) { Ok(_) => { - self.block_lookups - .search_unknown_block(block_root, &[peer_id], &mut self.network); + if self.block_lookups.search_unknown_block( + block_root, + &[peer_id], + &mut self.network, + ) { + // Lookup created. No need to log here it's logged in `new_current_lookup` + } else { + debug!(?block_root, "No lookup created for unknown block"); + } } Err(reason) => { debug!(%block_root, reason, "Ignoring unknown block request"); @@ -1117,11 +1173,13 @@ impl SyncManager { RpcEvent::from_chunk(data_column, seen_timestamp), ); } - SyncRequestId::DataColumnsByRange(id) => self.on_data_columns_by_range_response( - id, - peer_id, - RpcEvent::from_chunk(data_column, seen_timestamp), - ), + SyncRequestId::DataColumnsByRange(req_id) => { + self.on_data_columns_by_range_response( + req_id, + peer_id, + RpcEvent::from_chunk(data_column, seen_timestamp), + ); + } _ => { crit!(%peer_id, "bad request id for data_column"); } @@ -1157,14 +1215,6 @@ impl SyncManager { .on_data_columns_by_root_response(req_id, peer_id, data_column) { match req_id.requester { - DataColumnsByRootRequester::Sampling(id) => { - if let Some((requester, result)) = - self.sampling - .on_sample_downloaded(id, peer_id, resp, &mut self.network) - { - self.on_sampling_result(requester, result) - } - } DataColumnsByRootRequester::Custody(custody_id) => { if let Some(result) = self .network @@ -1217,11 +1267,22 @@ impl SyncManager { .network .on_data_columns_by_range_response(id, peer_id, data_column) { - self.on_range_components_response( - id.parent_request_id, - peer_id, - RangeBlockComponent::CustodyColumns(id, resp), - ); + match id.parent_request_id { + DataColumnsByRangeRequester::ComponentsByRange(components_by_range_req_id) => { + self.on_range_components_response( + components_by_range_req_id, + peer_id, + RangeBlockComponent::CustodyColumns(id, resp), + ); + } + DataColumnsByRangeRequester::CustodyBackfillSync(custody_backfill_req_id) => self + .on_custody_backfill_columns_response( + custody_backfill_req_id, + id, + peer_id, + resp, + ), + } } } @@ -1238,31 +1299,6 @@ impl SyncManager { ); } - fn on_sampling_result(&mut self, requester: SamplingRequester, result: SamplingResult) { - match requester { - SamplingRequester::ImportedBlock(block_root) => { - debug!(%block_root, ?result, "Sampling result"); - - match result { - Ok(_) => { - // Notify the fork-choice of a successful sampling result to mark the block - // branch as safe. - if let Err(e) = self - .network - .beacon_processor() - .send_sampling_completed(block_root) - { - warn!(?block_root, reason = ?e, "Error sending sampling result"); - } - } - Err(e) => { - warn!(?block_root, reason = ?e, "Sampling failed"); - } - } - } - } - } - /// Handles receiving a response for a range sync request that should have both blocks and /// blobs. fn on_range_components_response( @@ -1336,6 +1372,36 @@ impl SyncManager { } } } + + /// Handles receiving a response for a custody range sync request that has columns. + fn on_custody_backfill_columns_response( + &mut self, + custody_sync_request_id: CustodyBackFillBatchRequestId, + req_id: DataColumnsByRangeRequestId, + peer_id: PeerId, + data_columns: RpcResponseResult>>>, + ) { + if let Some(resp) = self.network.custody_backfill_data_columns_response( + custody_sync_request_id, + req_id, + data_columns, + ) { + match self.custody_backfill_sync.on_data_column_response( + &mut self.network, + custody_sync_request_id, + &peer_id, + resp, + ) { + Ok(ProcessResult::SyncCompleted) => self.update_sync_state(), + Ok(ProcessResult::Successful) => {} + Err(_e) => { + // The custody sync has failed, errors are reported + // within. + self.update_sync_state(); + } + } + } + } } impl From> for BlockProcessingResult { diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 0f5fd6fb9f..054bab654c 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -2,16 +2,17 @@ //! //! Stores the various syncing methods for the beacon chain. mod backfill_sync; +mod batch; mod block_lookups; mod block_sidecar_coupling; +mod custody_backfill_sync; pub mod manager; mod network_context; -mod peer_sampling; mod peer_sync_info; +mod range_data_column_batch_request; mod range_sync; #[cfg(test)] mod tests; -pub use lighthouse_network::service::api_types::SamplingId; pub use manager::{BatchProcessResult, SyncMessage}; -pub use range_sync::{BatchOperationOutcome, ChainId}; +pub use range_sync::ChainId; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 58641f8606..2e0c56db23 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -3,18 +3,20 @@ use self::custody::{ActiveCustodyRequest, Error as CustodyRequestError}; pub use self::requests::{BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest}; +use super::SyncMessage; use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::BlockProcessType; -use super::range_sync::ByRangeRequestType; -use super::SyncMessage; use crate::metrics; use crate::network_beacon_processor::NetworkBeaconProcessor; #[cfg(test)] use crate::network_beacon_processor::TestBeaconChainType; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; +use crate::sync::batch::ByRangeRequestType; use crate::sync::block_lookups::SingleLookupId; +use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; +use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; @@ -24,10 +26,12 @@ use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, Req pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, - CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRootRequestId, + CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyId, CustodyRequester, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; +use lighthouse_tracing::{SPAN_OUTGOING_BLOCK_BY_ROOT_REQUEST, SPAN_OUTGOING_RANGE_REQUEST}; use parking_lot::RwLock; pub use requests::LookupVerifyError; use requests::{ @@ -44,16 +48,31 @@ use std::time::Duration; #[cfg(test)] use task_executor::TaskExecutor; use tokio::sync::mpsc; -use tracing::{debug, error, span, warn, Level}; +use tracing::{Span, debug, debug_span, error, warn}; use types::blob_sidecar::FixedBlobSidecarList; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, - Hash256, SignedBeaconBlock, Slot, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + ForkContext, Hash256, SignedBeaconBlock, Slot, }; pub mod custody; mod requests; +macro_rules! new_range_request_span { + ($self:expr, $name:literal, $parent:expr, $peer_id:expr) => {{ + let client = $self.client_type(&$peer_id).kind; + debug_span!( + parent: $parent, + $name, + peer_id = %$peer_id, + client = %client + ) + }}; +} + +/// Max retries for block components after which we fail the batch. +pub const MAX_COLUMN_RETRIES: usize = 3; + #[derive(Debug)] pub enum RpcEvent { StreamTermination, @@ -81,7 +100,7 @@ pub enum RpcResponseError { RpcError(#[allow(dead_code)] RPCError), VerifyError(LookupVerifyError), CustodyRequestError(#[allow(dead_code)] CustodyRequestError), - BlockComponentCouplingError(#[allow(dead_code)] String), + BlockComponentCouplingError(CouplingError), } #[derive(Debug, PartialEq, Eq)] @@ -194,7 +213,6 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRange requests data_columns_by_range_requests: ActiveRequests>, - /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -202,6 +220,10 @@ pub struct SyncNetworkContext { components_by_range_requests: FnvHashMap>, + /// A batch of data columns by range request for custody sync + custody_backfill_data_column_batch_requests: + FnvHashMap>, + /// Whether the ee is online. If it's not, we don't allow access to the /// `beacon_processor_send`. execution_engine_state: EngineState, @@ -266,12 +288,6 @@ impl SyncNetworkContext { chain: Arc>, fork_context: Arc, ) -> Self { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); SyncNetworkContext { network_send, execution_engine_state: EngineState::Online, // always assume `Online` at the start @@ -284,6 +300,7 @@ impl SyncNetworkContext { data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), custody_by_root_requests: <_>::default(), components_by_range_requests: FnvHashMap::default(), + custody_backfill_data_column_batch_requests: FnvHashMap::default(), network_beacon_processor, chain, fork_context, @@ -313,6 +330,7 @@ impl SyncNetworkContext { custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests components_by_range_requests: _, + custody_backfill_data_column_batch_requests: _, execution_engine_state: _, network_beacon_processor: _, chain: _, @@ -343,7 +361,6 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); - blocks_by_root_ids .chain(blobs_by_root_ids) .chain(data_column_by_root_ids) @@ -373,22 +390,16 @@ impl SyncNetworkContext { } pub fn status_peers(&self, chain: &C, peers: impl Iterator) { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let status_message = chain.status_message(); for peer_id in peers { debug!( peer = %peer_id, - fork_digest = ?status_message.fork_digest, - finalized_root = ?status_message.finalized_root, - finalized_epoch = ?status_message.finalized_epoch, - head_root = %status_message.head_root, - head_slot = %status_message.head_slot, + fork_digest = ?status_message.fork_digest(), + finalized_root = ?status_message.finalized_root(), + finalized_epoch = ?status_message.finalized_epoch(), + head_root = %status_message.head_root(), + head_slot = %status_message.head_slot(), + earliest_available_slot = ?status_message.earliest_available_slot(), "Sending Status Request" ); @@ -416,6 +427,7 @@ impl SyncNetworkContext { custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests components_by_range_requests: _, + custody_backfill_data_column_batch_requests: _, execution_engine_state: _, network_beacon_processor: _, chain: _, @@ -440,18 +452,109 @@ impl SyncNetworkContext { active_request_count_by_peer } + /// Retries only the specified failed columns by requesting them again. + /// + /// Note: This function doesn't retry the whole batch, but retries specific requests within + /// the batch. + pub fn retry_columns_by_range( + &mut self, + id: Id, + peers: &HashSet, + peers_to_deprioritize: &HashSet, + request: BlocksByRangeRequest, + failed_columns: &HashSet, + ) -> Result<(), String> { + let Some((requester, parent_request_span)) = self + .components_by_range_requests + .iter() + .find_map(|(key, value)| { + if key.id == id { + Some((key.requester, value.request_span.clone())) + } else { + None + } + }) + else { + return Err("request id not present".to_string()); + }; + + let active_request_count_by_peer = self.active_request_count_by_peer(); + + debug!( + ?failed_columns, + ?id, + ?requester, + "Retrying only failed column requests from other peers" + ); + + // Attempt to find all required custody peers to request the failed columns from + let columns_by_range_peers_to_request = self + .select_columns_by_range_peers_to_request( + failed_columns, + peers, + active_request_count_by_peer, + peers_to_deprioritize, + ) + .map_err(|e| format!("{:?}", e))?; + + // Reuse the id for the request that received partially correct responses + let id = ComponentsByRangeRequestId { id, requester }; + + let data_column_requests = columns_by_range_peers_to_request + .into_iter() + .map(|(peer_id, columns)| { + self.send_data_columns_by_range_request( + peer_id, + DataColumnsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + columns, + }, + DataColumnsByRangeRequester::ComponentsByRange(id), + new_range_request_span!( + self, + "outgoing_columns_by_range_retry", + parent_request_span.clone(), + peer_id + ), + ) + }) + .collect::, _>>() + .map_err(|e| format!("{:?}", e))?; + + // instead of creating a new `RangeBlockComponentsRequest`, we reinsert + // the new requests created for the failed requests + let Some(range_request) = self.components_by_range_requests.get_mut(&id) else { + return Err( + "retrying custody request for range request that does not exist".to_string(), + ); + }; + + range_request.reinsert_failed_column_requests(data_column_requests)?; + Ok(()) + } + /// A blocks by range request sent by the range sync algorithm pub fn block_components_by_range_request( &mut self, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, requester: RangeRequestId, - peers: &HashSet, + block_peers: &HashSet, + column_peers: &HashSet, peers_to_deprioritize: &HashSet, ) -> Result { + let range_request_span = debug_span!( + parent: None, + SPAN_OUTGOING_RANGE_REQUEST, + range_req_id = %requester, + block_peers = block_peers.len(), + column_peers = column_peers.len() + ); + let _guard = range_request_span.clone().entered(); let active_request_count_by_peer = self.active_request_count_by_peer(); - let Some(block_peer) = peers + let Some(block_peer) = block_peers .iter() .map(|peer| { ( @@ -476,10 +579,16 @@ impl SyncNetworkContext { // Attempt to find all required custody peers before sending any request or creating an ID let columns_by_range_peers_to_request = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let column_indexes = self.network_globals().sampling_columns.clone(); + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); + let column_indexes = self + .chain + .sampling_columns_for_epoch(epoch) + .iter() + .cloned() + .collect(); Some(self.select_columns_by_range_peers_to_request( &column_indexes, - peers, + column_peers, active_request_count_by_peer, peers_to_deprioritize, )?) @@ -493,7 +602,17 @@ impl SyncNetworkContext { requester, }; - let blocks_req_id = self.send_blocks_by_range_request(block_peer, request.clone(), id)?; + let blocks_req_id = self.send_blocks_by_range_request( + block_peer, + request.clone(), + id, + new_range_request_span!( + self, + "outgoing_blocks_by_range", + range_request_span.clone(), + block_peer + ), + )?; let blobs_req_id = if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { Some(self.send_blobs_by_range_request( @@ -503,6 +622,12 @@ impl SyncNetworkContext { count: *request.count(), }, id, + new_range_request_span!( + self, + "outgoing_blobs_by_range", + range_request_span.clone(), + block_peer + ), )?) } else { None @@ -520,27 +645,30 @@ impl SyncNetworkContext { count: *request.count(), columns, }, - id, + DataColumnsByRangeRequester::ComponentsByRange(id), + new_range_request_span!( + self, + "outgoing_columns_by_range", + range_request_span.clone(), + peer_id + ), ) }) .collect::, _>>() }) .transpose()?; + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); let info = RangeBlockComponentsRequest::new( blocks_req_id, blobs_req_id, data_column_requests.map(|data_column_requests| { ( data_column_requests, - self.network_globals() - .sampling_columns - .clone() - .iter() - .copied() - .collect(), + self.chain.sampling_columns_for_epoch(epoch).to_vec(), ) }), + range_request_span, ); self.components_by_range_requests.insert(id, info); @@ -618,20 +746,28 @@ impl SyncNetworkContext { let request = entry.get_mut(); match range_block_component { RangeBlockComponent::Block(req_id, resp) => resp.and_then(|(blocks, _)| { - request - .add_blocks(req_id, blocks) - .map_err(RpcResponseError::BlockComponentCouplingError) + request.add_blocks(req_id, blocks).map_err(|e| { + RpcResponseError::BlockComponentCouplingError(CouplingError::InternalError( + e, + )) + }) }), RangeBlockComponent::Blob(req_id, resp) => resp.and_then(|(blobs, _)| { - request - .add_blobs(req_id, blobs) - .map_err(RpcResponseError::BlockComponentCouplingError) + request.add_blobs(req_id, blobs).map_err(|e| { + RpcResponseError::BlockComponentCouplingError(CouplingError::InternalError( + e, + )) + }) }), RangeBlockComponent::CustodyColumns(req_id, resp) => { resp.and_then(|(custody_columns, _)| { request .add_custody_columns(req_id, custody_columns) - .map_err(RpcResponseError::BlockComponentCouplingError) + .map_err(|e| { + RpcResponseError::BlockComponentCouplingError( + CouplingError::InternalError(e), + ) + }) }) } } @@ -640,8 +776,28 @@ impl SyncNetworkContext { return Some(Err(e)); } - if let Some(blocks_result) = entry.get().responses(&self.chain.spec) { - entry.remove(); + let range_req = entry.get_mut(); + if let Some(blocks_result) = range_req.responses(&self.chain.spec) { + if let Err(CouplingError::DataColumnPeerFailure { + error, + faulty_peers: _, + exceeded_retries, + }) = &blocks_result + { + // Remove the entry if it's a peer failure **and** retry counter is exceeded + if *exceeded_retries { + debug!( + entry=?entry.key(), + msg = error, + "Request exceeded max retries, failing batch" + ); + entry.remove(); + }; + } else { + // also remove the entry only if it coupled successfully + // or if it isn't a column peer failure. + entry.remove(); + } // If the request is finished, dequeue everything Some(blocks_result.map_err(RpcResponseError::BlockComponentCouplingError)) } else { @@ -684,30 +840,35 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::Pending("no peers")); }; - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - match self.chain.get_block_process_status(&block_root) { // Unknown block, continue request to download BlockProcessStatus::Unknown => {} - // Block is known are currently processing, expect a future event with the result of - // processing. - BlockProcessStatus::NotValidated { .. } => { - // Lookup sync event safety: If the block is currently in the processing cache, we - // are guaranteed to receive a `SyncMessage::GossipBlockProcessResult` that will - // make progress on this lookup - return Ok(LookupRequestResult::Pending("block in processing cache")); - } + // Block is known and currently processing. Imports from gossip and HTTP API insert the + // block in the da_cache. However, HTTP API is unable to notify sync when it completes + // block import. Returning `Pending` here will result in stuck lookups if the block is + // importing from sync. + BlockProcessStatus::NotValidated(_, source) => match source { + BlockImportSource::Gossip => { + // Lookup sync event safety: If the block is currently in the processing cache, we + // are guaranteed to receive a `SyncMessage::GossipBlockProcessResult` that will + // make progress on this lookup + return Ok(LookupRequestResult::Pending("block in processing cache")); + } + BlockImportSource::Lookup + | BlockImportSource::RangeSync + | BlockImportSource::HttpApi => { + // Lookup, RangeSync or HttpApi block import don't emit the GossipBlockProcessResult + // event. If a lookup happens to be created during block import from one of + // those sources just import the block twice. Otherwise the lookup will get + // stuck. Double imports are fine, they just waste resources. + } + }, // Block is fully validated. If it's not yet imported it's waiting for missing block // components. Consider this request completed and do nothing. BlockProcessStatus::ExecutionValidated { .. } => { return Ok(LookupRequestResult::NoRequestNeeded( "block execution validated", - )) + )); } } @@ -724,10 +885,15 @@ impl SyncNetworkContext { // - RPCError(request_id): handled by `Self::on_single_block_response` // - Disconnect(peer_id) handled by `Self::peer_disconnected``which converts it to a // ` RPCError(request_id)`event handled by the above method + let network_request = RequestType::BlocksByRoot( + request + .into_request(&self.fork_context) + .map_err(RpcRequestSendError::InternalError)?, + ); self.network_send .send(NetworkMessage::SendRequest { peer_id, - request: RequestType::BlocksByRoot(request.into_request(&self.fork_context)), + request: network_request, app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), }) .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; @@ -740,6 +906,11 @@ impl SyncNetworkContext { "Sync RPC request sent" ); + let request_span = debug_span!( + parent: Span::current(), + SPAN_OUTGOING_BLOCK_BY_ROOT_REQUEST, + %block_root, + ); self.blocks_by_root_requests.insert( id, peer_id, @@ -747,6 +918,7 @@ impl SyncNetworkContext { // block and the peer must have it. true, BlocksByRootRequestItems::new(request), + request_span, ); Ok(LookupRequestResult::RequestSent(id.req_id)) @@ -790,13 +962,6 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::Pending("no peers")); }; - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let imported_blob_indexes = self .chain .data_availability_checker @@ -823,10 +988,16 @@ impl SyncNetworkContext { }; // Lookup sync event safety: Refer to `Self::block_lookup_request` `network_send.send` call + let network_request = RequestType::BlobsByRoot( + request + .clone() + .into_request(&self.fork_context) + .map_err(RpcRequestSendError::InternalError)?, + ); self.network_send .send(NetworkMessage::SendRequest { peer_id, - request: RequestType::BlobsByRoot(request.clone().into_request(&self.fork_context)), + request: network_request, app_request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), }) .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; @@ -848,6 +1019,8 @@ impl SyncNetworkContext { // have imported the block+blobs. true, BlobsByRootRequestItems::new(request), + // Not implemented + Span::none(), ); Ok(LookupRequestResult::RequestSent(id.req_id)) @@ -861,13 +1034,6 @@ impl SyncNetworkContext { request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, ) -> Result, &'static str> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let id = DataColumnsByRootRequestId { id: self.next_id(), requester, @@ -876,9 +1042,10 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: RequestType::DataColumnsByRoot( - request - .clone() - .try_into_request(self.fork_context.current_fork(), &self.chain.spec)?, + request.clone().try_into_request::( + self.fork_context.current_fork_name(), + &self.chain.spec, + )?, ), app_request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), })?; @@ -897,6 +1064,9 @@ impl SyncNetworkContext { peer_id, expect_max_responses, DataColumnsByRootRequestItems::new(request), + // Span is tracked in `self.custody_columns_by_root_requests` in the + // `ActiveCustodyRequest` struct. + Span::none(), ); Ok(LookupRequestResult::RequestSent(id)) @@ -912,25 +1082,22 @@ impl SyncNetworkContext { block_root: Hash256, lookup_peers: Arc>>, ) -> Result { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let custody_indexes_imported = self .chain .data_availability_checker .cached_data_column_indexes(&block_root) .unwrap_or_default(); + let current_epoch = self.chain.epoch().map_err(|e| { + RpcRequestSendError::InternalError(format!("Unable to read slot clock {:?}", e)) + })?; + // Include only the blob indexes not yet imported (received through gossip) let custody_indexes_to_fetch = self - .network_globals() - .sampling_columns - .clone() - .into_iter() + .chain + .sampling_columns_for_epoch(current_epoch) + .iter() + .copied() .filter(|index| !custody_indexes_imported.contains(index)) .collect::>(); @@ -995,6 +1162,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: BlocksByRangeRequest, parent_request_id: ComponentsByRangeRequestId, + request_span: Span, ) -> Result { let id = BlocksByRangeRequestId { id: self.next_id(), @@ -1024,6 +1192,7 @@ impl SyncNetworkContext { // know if there are missed blocks. false, BlocksByRangeRequestItems::new(request), + request_span, ); Ok(id) } @@ -1033,6 +1202,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: BlobsByRangeRequest, parent_request_id: ComponentsByRangeRequestId, + request_span: Span, ) -> Result { let id = BlobsByRangeRequestId { id: self.next_id(), @@ -1066,6 +1236,7 @@ impl SyncNetworkContext { // know if there are missed blocks. false, BlobsByRangeRequestItems::new(request, max_blobs_per_block), + request_span, ); Ok(id) } @@ -1074,11 +1245,14 @@ impl SyncNetworkContext { &mut self, peer_id: PeerId, request: DataColumnsByRangeRequest, - parent_request_id: ComponentsByRangeRequestId, - ) -> Result { + parent_request_id: DataColumnsByRangeRequester, + request_span: Span, + ) -> Result<(DataColumnsByRangeRequestId, Vec), RpcRequestSendError> { + let requested_columns = request.columns.clone(); let id = DataColumnsByRangeRequestId { id: self.next_id(), parent_request_id, + peer: peer_id, }; self.send_network_msg(NetworkMessage::SendRequest { @@ -1105,8 +1279,9 @@ impl SyncNetworkContext { // know if there are missed blocks. false, DataColumnsByRangeRequestItems::new(request), + request_span, ); - Ok(id) + Ok((id, requested_columns)) } pub fn is_execution_engine_online(&self) -> bool { @@ -1114,26 +1289,12 @@ impl SyncNetworkContext { } pub fn update_execution_engine_state(&mut self, engine_state: EngineState) { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - debug!(past_state = ?self.execution_engine_state, new_state = ?engine_state, "Sync's view on execution engine state updated"); self.execution_engine_state = engine_state; } /// Terminates the connection with the peer and bans them. pub fn goodbye_peer(&mut self, peer_id: PeerId, reason: GoodbyeReason) { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - self.network_send .send(NetworkMessage::GoodbyePeer { peer_id, @@ -1147,13 +1308,6 @@ impl SyncNetworkContext { /// Reports to the scoring algorithm the behaviour of a peer. pub fn report_peer(&self, peer_id: PeerId, action: PeerAction, msg: &'static str) { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - debug!(%peer_id, %action, %msg, "Sync reporting peer"); self.network_send .send(NetworkMessage::ReportPeer { @@ -1169,13 +1323,6 @@ impl SyncNetworkContext { /// Subscribes to core topics. pub fn subscribe_core_topics(&self) { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - self.network_send .send(NetworkMessage::SubscribeCoreTopics) .unwrap_or_else(|e| { @@ -1185,13 +1332,6 @@ impl SyncNetworkContext { /// Sends an arbitrary network message. fn send_network_msg(&self, msg: NetworkMessage) -> Result<(), &'static str> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - self.network_send.send(msg).map_err(|_| { debug!("Could not send message to the network service"); "Network channel send Failed" @@ -1416,13 +1556,6 @@ impl SyncNetworkContext { peer_id: PeerId, resp: RpcResponseResult>>>, ) -> Option> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - // Note: need to remove the request to borrow self again below. Otherwise we can't // do nested requests let Some(mut request) = self.custody_by_root_requests.remove(&id.requester) else { @@ -1442,13 +1575,6 @@ impl SyncNetworkContext { request: ActiveCustodyRequest, result: CustodyRequestResult, ) -> Option> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let result = result .map_err(RpcResponseError::CustodyRequestError) .transpose(); @@ -1476,22 +1602,11 @@ impl SyncNetworkContext { block: Arc>, seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - let block = RpcBlock::new_without_blobs( - Some(block_root), - block, - self.network_globals().custody_columns_count() as usize, - ); + let block = RpcBlock::new_without_blobs(Some(block_root), block); debug!(block = ?block_root, id, "Sending block for processing"); // Lookup sync event safety: If `beacon_processor.send_rpc_beacon_block` returns Ok() sync @@ -1519,13 +1634,6 @@ impl SyncNetworkContext { blobs: FixedBlobSidecarList, seen_timestamp: Duration, ) -> Result<(), SendErrorProcessor> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; @@ -1557,13 +1665,6 @@ impl SyncNetworkContext { seen_timestamp: Duration, process_type: BlockProcessType, ) -> Result<(), SendErrorProcessor> { - let span = span!( - Level::INFO, - "SyncNetworkContext", - service = "network_context" - ); - let _enter = span.enter(); - let beacon_processor = self .beacon_processor_if_enabled() .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; @@ -1585,6 +1686,111 @@ impl SyncNetworkContext { }) } + /// data column by range requests sent by the custody sync algorithm + pub fn custody_backfill_data_columns_batch_request( + &mut self, + request: DataColumnsByRangeRequest, + batch_id: CustodyBackfillBatchId, + peers: &HashSet, + peers_to_deprioritize: &HashSet, + ) -> Result { + let active_request_count_by_peer = self.active_request_count_by_peer(); + // Attempt to find all required custody peers before sending any request or creating an ID + let columns_by_range_peers_to_request = { + let column_indexes = self + .chain + .sampling_columns_for_epoch(batch_id.epoch) + .iter() + .cloned() + .collect(); + + self.select_columns_by_range_peers_to_request( + &column_indexes, + peers, + active_request_count_by_peer, + peers_to_deprioritize, + )? + }; + + // Create the overall `custody_by_range` request id + let id = CustodyBackFillBatchRequestId { + id: self.next_id(), + batch_id, + }; + + let result = columns_by_range_peers_to_request + .iter() + .filter_map(|(peer_id, _)| { + self.send_data_columns_by_range_request( + *peer_id, + request.clone(), + DataColumnsByRangeRequester::CustodyBackfillSync(id), + Span::none(), + ) + .ok() + }) + .collect::>(); + + let range_data_column_batch_request = + RangeDataColumnBatchRequest::new(result, self.chain.clone(), batch_id.epoch); + + self.custody_backfill_data_column_batch_requests + .insert(id, range_data_column_batch_request); + + Ok(id) + } + + /// Received a data columns by range response from a custody sync request which batches them. + pub fn custody_backfill_data_columns_response( + &mut self, + // Identifies the custody backfill request for all data columns on this epoch + custody_sync_request_id: CustodyBackFillBatchRequestId, + // Identifies a specific data_columns_by_range request for *some* columns in this epoch. We + // pass them separately as DataColumnsByRangeRequestId parent is an enum and would require + // matching again. + req_id: DataColumnsByRangeRequestId, + data_columns: RpcResponseResult>, + ) -> Option, RpcResponseError>> { + let Entry::Occupied(mut entry) = self + .custody_backfill_data_column_batch_requests + .entry(custody_sync_request_id) + else { + metrics::inc_counter_vec( + &metrics::SYNC_UNKNOWN_NETWORK_REQUESTS, + &["range_data_columns"], + ); + return None; + }; + + if let Err(e) = { + let request = entry.get_mut(); + data_columns.and_then(|(data_columns, _)| { + request + .add_custody_columns(req_id, data_columns.clone()) + .map_err(|e| { + RpcResponseError::BlockComponentCouplingError(CouplingError::InternalError( + e, + )) + }) + }) + } { + entry.remove(); + return Some(Err(e)); + } + + if let Some(data_column_result) = entry.get_mut().responses() { + if data_column_result.is_ok() { + // remove the entry only if it coupled successfully with + // no errors + entry.remove(); + } + // If the request is finished, dequeue everything + Some(data_column_result.map_err(RpcResponseError::BlockComponentCouplingError)) + } else { + None + } + } + pub(crate) fn register_metrics(&self) { for (id, count) in [ ("blocks_by_root", self.blocks_by_root_requests.len()), diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index f4d010b881..71e002cc42 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,28 +1,25 @@ use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; -use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::BeaconChainTypes; +use beacon_chain::validator_monitor::timestamp_now; use fnv::FnvHashMap; -use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; use lighthouse_network::PeerId; -use lru_cache::LRUTimeCache; +use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; +use lighthouse_tracing::SPAN_OUTGOING_CUSTODY_REQUEST; use parking_lot::RwLock; -use rand::Rng; use std::collections::HashSet; +use std::hash::{BuildHasher, RandomState}; use std::time::{Duration, Instant}; use std::{collections::HashMap, marker::PhantomData, sync::Arc}; -use tracing::{debug, warn}; -use types::EthSpec; -use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Hash256}; +use tracing::{Span, debug, debug_span, warn}; +use types::{DataColumnSidecar, Hash256, data_column_sidecar::ColumnIndex}; +use types::{DataColumnSidecarList, EthSpec}; use super::{LookupRequestResult, PeerGroup, RpcResponseResult, SyncNetworkContext}; -const FAILED_PEERS_CACHE_EXPIRY_SECONDS: u64 = 5; const MAX_STALE_NO_PEERS_DURATION: Duration = Duration::from_secs(30); -type DataColumnSidecarList = Vec>>; - pub struct ActiveCustodyRequest { block_root: Hash256, custody_id: CustodyId, @@ -31,12 +28,11 @@ pub struct ActiveCustodyRequest { /// Active requests for 1 or more columns each active_batch_columns_requests: FnvHashMap, - /// Peers that have recently failed to successfully respond to a columns by root request. - /// Having a LRUTimeCache allows this request to not have to track disconnecting peers. - failed_peers: LRUTimeCache, + peer_attempts: HashMap, /// Set of peers that claim to have imported this block and their custody columns lookup_peers: Arc>>, - + /// Span for tracing the lifetime of this request. + span: Span, _phantom: PhantomData, } @@ -57,6 +53,8 @@ pub enum Error { struct ActiveBatchColumnsRequest { indices: Vec, + /// Span for tracing the lifetime of this request. + span: Span, } pub type CustodyRequestResult = @@ -69,6 +67,11 @@ impl ActiveCustodyRequest { column_indices: &[ColumnIndex], lookup_peers: Arc>>, ) -> Self { + let span = debug_span!( + parent: Span::current(), + SPAN_OUTGOING_CUSTODY_REQUEST, + %block_root, + ); Self { block_root, custody_id, @@ -78,8 +81,9 @@ impl ActiveCustodyRequest { .map(|index| (*index, ColumnRequest::new())), ), active_batch_columns_requests: <_>::default(), - failed_peers: LRUTimeCache::new(Duration::from_secs(FAILED_PEERS_CACHE_EXPIRY_SECONDS)), + peer_attempts: HashMap::new(), lookup_peers, + span, _phantom: PhantomData, } } @@ -108,6 +112,8 @@ impl ActiveCustodyRequest { return Ok(None); }; + let _guard = batch_request.span.clone().entered(); + match resp { Ok((data_columns, seen_timestamp)) => { debug!( @@ -161,12 +167,9 @@ impl ActiveCustodyRequest { block_root = ?self.block_root, %req_id, %peer_id, - // TODO(das): this property can become very noisy, being the full range 0..128 ?missing_column_indexes, "Custody column peer claims to not have some data" ); - - self.failed_peers.insert(peer_id); } } Err(err) => { @@ -185,8 +188,6 @@ impl ActiveCustodyRequest { .ok_or(Error::BadState("unknown column_index".to_owned()))? .on_download_error_and_mark_failure(req_id)?; } - - self.failed_peers.insert(peer_id); } }; @@ -197,6 +198,7 @@ impl ActiveCustodyRequest { &mut self, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { + let _guard = self.span.clone().entered(); if self.column_requests.values().all(|r| r.is_downloaded()) { // All requests have completed successfully. let mut peers = HashMap::>::new(); @@ -222,52 +224,29 @@ impl ActiveCustodyRequest { let active_request_count_by_peer = cx.active_request_count_by_peer(); let mut columns_to_request_by_peer = HashMap::>::new(); let lookup_peers = self.lookup_peers.read(); + // Create deterministic hasher per request to ensure consistent peer ordering within + // this request (avoiding fragmentation) while varying selection across different requests + let random_state = RandomState::new(); - // Need to: - // - track how many active requests a peer has for load balancing - // - which peers have failures to attempt others - // - which peer returned what to have PeerGroup attributability - - for (column_index, request) in self.column_requests.iter_mut() { + for (column_index, request) in self.column_requests.iter() { if let Some(wait_duration) = request.is_awaiting_download() { + // Note: an empty response is considered a successful response, so we may end up + // retrying many more times than `MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS`. if request.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { return Err(Error::TooManyFailures); } - // TODO(das): When is a fork and only a subset of your peers know about a block, we should - // only query the peers on that fork. Should this case be handled? How to handle it? - let custodial_peers = cx.get_custodial_peers(*column_index); + let peer_to_request = self.select_column_peer( + cx, + &active_request_count_by_peer, + &lookup_peers, + *column_index, + &random_state, + ); - // We draw from the total set of peers, but prioritize those peers who we have - // received an attestation / status / block message claiming to have imported the - // lookup. The frequency of those messages is low, so drawing only from lookup_peers - // could cause many lookups to take much longer or fail as they don't have enough - // custody peers on a given column - let mut priorized_peers = custodial_peers - .iter() - .map(|peer| { - ( - // Prioritize peers that claim to know have imported this block - if lookup_peers.contains(peer) { 0 } else { 1 }, - // De-prioritize peers that have failed to successfully respond to - // requests recently - self.failed_peers.contains(peer), - // Prefer peers with fewer requests to load balance across peers. - // We batch requests to the same peer, so count existence in the - // `columns_to_request_by_peer` as a single 1 request. - active_request_count_by_peer.get(peer).copied().unwrap_or(0) - + columns_to_request_by_peer.get(peer).map(|_| 1).unwrap_or(0), - // Random factor to break ties, otherwise the PeerID breaks ties - rand::thread_rng().gen::(), - *peer, - ) - }) - .collect::>(); - priorized_peers.sort_unstable(); - - if let Some((_, _, _, _, peer_id)) = priorized_peers.first() { + if let Some(peer_id) = peer_to_request { columns_to_request_by_peer - .entry(*peer_id) + .entry(peer_id) .or_default() .push(*column_index); } else if wait_duration > MAX_STALE_NO_PEERS_DURATION { @@ -282,6 +261,23 @@ impl ActiveCustodyRequest { } } + let peer_requests = columns_to_request_by_peer.len(); + if peer_requests > 0 { + let columns_requested_count = columns_to_request_by_peer + .values() + .map(|v| v.len()) + .sum::(); + debug!( + lookup_peers = lookup_peers.len(), + "Requesting {} columns from {} peers", columns_requested_count, peer_requests, + ); + } else { + debug!( + lookup_peers = lookup_peers.len(), + "No column peers found for look up", + ); + } + for (peer_id, indices) in columns_to_request_by_peer.into_iter() { let request_result = cx .data_column_lookup_request( @@ -301,6 +297,15 @@ impl ActiveCustodyRequest { match request_result { LookupRequestResult::RequestSent(req_id) => { + *self.peer_attempts.entry(peer_id).or_insert(0) += 1; + + let client = cx.network_globals().client(&peer_id).kind; + let batch_columns_req_span = debug_span!( + "batch_columns_req", + %peer_id, + %client, + ); + let _guard = batch_columns_req_span.clone().entered(); for column_index in &indices { let column_request = self .column_requests @@ -311,8 +316,13 @@ impl ActiveCustodyRequest { column_request.on_download_start(req_id)?; } - self.active_batch_columns_requests - .insert(req_id, ActiveBatchColumnsRequest { indices }); + self.active_batch_columns_requests.insert( + req_id, + ActiveBatchColumnsRequest { + indices, + span: batch_columns_req_span, + }, + ); } LookupRequestResult::NoRequestNeeded(_) => unreachable!(), LookupRequestResult::Pending(_) => unreachable!(), @@ -321,11 +331,54 @@ impl ActiveCustodyRequest { Ok(None) } + + fn select_column_peer( + &self, + cx: &mut SyncNetworkContext, + active_request_count_by_peer: &HashMap, + lookup_peers: &HashSet, + column_index: ColumnIndex, + random_state: &RandomState, + ) -> Option { + // We draw from the total set of peers, but prioritize those peers who we have + // received an attestation or a block from (`lookup_peers`), as the `lookup_peers` may take + // time to build up and we are likely to not find any column peers initially. + let custodial_peers = cx.get_custodial_peers(column_index); + let mut prioritized_peers = custodial_peers + .iter() + .filter(|peer| { + // Exclude peers that we have already made too many attempts to. + self.peer_attempts.get(peer).copied().unwrap_or(0) <= MAX_CUSTODY_PEER_ATTEMPTS + }) + .map(|peer| { + ( + // Prioritize peers that claim to know have imported this block + if lookup_peers.contains(peer) { 0 } else { 1 }, + // De-prioritize peers that we have already attempted to download from + self.peer_attempts.get(peer).copied().unwrap_or(0), + // Prefer peers with fewer requests to load balance across peers. + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + // The hash ensures consistent peer ordering within this request + // to avoid fragmentation while varying selection across different requests. + random_state.hash_one(peer), + *peer, + ) + }) + .collect::>(); + prioritized_peers.sort_unstable(); + + prioritized_peers + .first() + .map(|(_, _, _, _, peer_id)| *peer_id) + } } /// TODO(das): this attempt count is nested into the existing lookup request count. const MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS: usize = 3; +/// Max number of attempts to request custody columns from a single peer. +const MAX_CUSTODY_PEER_ATTEMPTS: usize = 3; + struct ColumnRequest { status: Status, download_failures: usize, diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 963b633ed6..3183c06d76 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -4,6 +4,7 @@ use beacon_chain::validator_monitor::timestamp_now; use fnv::FnvHashMap; use lighthouse_network::PeerId; use strum::IntoStaticStr; +use tracing::Span; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; @@ -50,6 +51,7 @@ struct ActiveRequest { peer_id: PeerId, // Error if the request terminates before receiving max expected responses expect_max_responses: bool, + span: Span, } enum State { @@ -66,13 +68,22 @@ impl ActiveRequests { } } - pub fn insert(&mut self, id: K, peer_id: PeerId, expect_max_responses: bool, items: T) { + pub fn insert( + &mut self, + id: K, + peer_id: PeerId, + expect_max_responses: bool, + items: T, + span: Span, + ) { + let _guard = span.clone().entered(); self.requests.insert( id, ActiveRequest { state: State::Active(items), peer_id, expect_max_responses, + span, }, ); } @@ -86,6 +97,11 @@ impl ActiveRequests { /// `add_item` may convert ReqResp success chunks into errors. This function handles the /// multiple errors / stream termination internally ensuring that a single `Some` is /// returned. + /// + /// ## Returns + /// - `Some` if the request has either completed or errored, and needs to be actioned by the + /// caller. + /// - `None` if no further action is currently needed. pub fn on_response( &mut self, id: K, @@ -101,6 +117,7 @@ impl ActiveRequests { // `ActiveRequestItems` validates the item before appending to its internal state. RpcEvent::Response(item, seen_timestamp) => { let request = &mut entry.get_mut(); + let _guard = request.span.clone().entered(); match &mut request.state { State::Active(items) => { match items.add(item) { @@ -136,6 +153,7 @@ impl ActiveRequests { // After stream termination we must forget about this request, there will be no more // messages coming from the network let request = entry.remove(); + let _guard = request.span.clone().entered(); match request.state { // Received a stream termination in a valid sequence, consume items State::Active(mut items) => { @@ -157,7 +175,9 @@ impl ActiveRequests { RpcEvent::RPCError(e) => { // After an Error event from the network we must forget about this request as this // may be the last message for this request. - match entry.remove().state { + let request = entry.remove(); + let _guard = request.span.clone().entered(); + match request.state { // Received error while request is still active, propagate error. State::Active(_) => Some(Err(e.into())), // Received error after completing the request, ignore the error. This is okay diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs index 547c51198e..39886d814e 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs @@ -1,6 +1,6 @@ use lighthouse_network::rpc::methods::BlobsByRootRequest; use std::sync::Arc; -use types::{blob_sidecar::BlobIdentifier, BlobSidecar, EthSpec, ForkContext, Hash256}; +use types::{BlobSidecar, EthSpec, ForkContext, Hash256, blob_sidecar::BlobIdentifier}; use super::{ActiveRequestItems, LookupVerifyError}; @@ -11,7 +11,7 @@ pub struct BlobsByRootSingleBlockRequest { } impl BlobsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ForkContext) -> BlobsByRootRequest { + pub fn into_request(self, spec: &ForkContext) -> Result { BlobsByRootRequest::new( self.indices .into_iter() diff --git a/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs index 6d7eabf909..8cb7f53ac5 100644 --- a/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs @@ -9,7 +9,8 @@ use super::{ActiveRequestItems, LookupVerifyError}; pub struct BlocksByRootSingleRequest(pub Hash256); impl BlocksByRootSingleRequest { - pub fn into_request(self, fork_context: &ForkContext) -> BlocksByRootRequest { + pub fn into_request(self, fork_context: &ForkContext) -> Result { + // This should always succeed (single block root), but we return a `Result` for safety. BlocksByRootRequest::new(vec![self.0], fork_context) } } diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs index 09d7f4b3b7..34df801eaa 100644 --- a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs @@ -1,8 +1,8 @@ use lighthouse_network::rpc::methods::DataColumnsByRootRequest; +use ssz_types::VariableList; use std::sync::Arc; use types::{ ChainSpec, DataColumnSidecar, DataColumnsByRootIdentifier, EthSpec, ForkName, Hash256, - RuntimeVariableList, }; use super::{ActiveRequestItems, LookupVerifyError}; @@ -14,21 +14,20 @@ pub struct DataColumnsByRootSingleBlockRequest { } impl DataColumnsByRootSingleBlockRequest { - pub fn try_into_request( + pub fn try_into_request( self, fork_name: ForkName, spec: &ChainSpec, - ) -> Result { - let number_of_columns = spec.number_of_columns as usize; - let columns = RuntimeVariableList::new(self.indices, number_of_columns) + ) -> Result, &'static str> { + let columns = VariableList::new(self.indices) .map_err(|_| "Number of indices exceeds total number of columns")?; - Ok(DataColumnsByRootRequest::new( + DataColumnsByRootRequest::new( vec![DataColumnsByRootIdentifier { block_root: self.block_root, columns, }], spec.max_request_blocks(fork_name), - )) + ) } } diff --git a/beacon_node/network/src/sync/peer_sampling.rs b/beacon_node/network/src/sync/peer_sampling.rs deleted file mode 100644 index 59b751787e..0000000000 --- a/beacon_node/network/src/sync/peer_sampling.rs +++ /dev/null @@ -1,741 +0,0 @@ -use self::request::ActiveColumnSampleRequest; -#[cfg(test)] -pub(crate) use self::request::Status; -use super::network_context::{ - DataColumnsByRootSingleBlockRequest, RpcResponseError, SyncNetworkContext, -}; -use crate::metrics; -use beacon_chain::BeaconChainTypes; -use fnv::FnvHashMap; -use lighthouse_network::service::api_types::{ - DataColumnsByRootRequester, SamplingId, SamplingRequestId, SamplingRequester, -}; -use lighthouse_network::{PeerAction, PeerId}; -use rand::{seq::SliceRandom, thread_rng}; -use std::{ - collections::hash_map::Entry, collections::HashMap, marker::PhantomData, sync::Arc, - time::Duration, -}; -use tracing::{debug, error, instrument, warn}; -use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash256}; - -pub type SamplingResult = Result<(), SamplingError>; - -type DataColumnSidecarList = Vec>>; - -pub struct Sampling { - requests: HashMap>, - sampling_config: SamplingConfig, -} - -impl Sampling { - #[instrument(parent = None,level = "info", fields(service = "sampling"), name = "sampling")] - pub fn new(sampling_config: SamplingConfig) -> Self { - Self { - requests: <_>::default(), - sampling_config, - } - } - - #[cfg(test)] - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - pub fn active_sampling_requests(&self) -> Vec { - self.requests.values().map(|r| r.block_root).collect() - } - - #[cfg(test)] - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - pub fn get_request_status( - &self, - block_root: Hash256, - index: &ColumnIndex, - ) -> Option { - let requester = SamplingRequester::ImportedBlock(block_root); - self.requests - .get(&requester) - .and_then(|req| req.get_request_status(index)) - } - - /// Create a new sampling request for a known block - /// - /// ### Returns - /// - /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. - /// - `None`: Request still active, requester should do no action - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - pub fn on_new_sample_request( - &mut self, - block_root: Hash256, - cx: &mut SyncNetworkContext, - ) -> Option<(SamplingRequester, SamplingResult)> { - let id = SamplingRequester::ImportedBlock(block_root); - - let request = match self.requests.entry(id) { - Entry::Vacant(e) => e.insert(ActiveSamplingRequest::new( - block_root, - id, - &self.sampling_config, - &cx.chain.spec, - )), - Entry::Occupied(_) => { - // Sampling is triggered from multiple sources, duplicate sampling requests are - // likely (gossip block + gossip data column) - // TODO(das): Should track failed sampling request for some time? Otherwise there's - // a risk of a loop with multiple triggers creating the request, then failing, - // and repeat. - debug!(?id, "Ignoring duplicate sampling request"); - return None; - } - }; - - debug!( - ?id, - column_selection = ?request.column_selection(), - "Created new sample request" - ); - - // TOOD(das): If a node has very little peers, continue_sampling() will attempt to find enough - // to sample here, immediately failing the sampling request. There should be some grace - // period to allow the peer manager to find custody peers. - let result = request.continue_sampling(cx); - self.handle_sampling_result(result, &id) - } - - /// Insert a downloaded column into an active sampling request. Then make progress on the - /// entire request. - /// - /// ### Returns - /// - /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. - /// - `None`: Request still active, requester should do no action - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - pub fn on_sample_downloaded( - &mut self, - id: SamplingId, - peer_id: PeerId, - resp: Result<(DataColumnSidecarList, Duration), RpcResponseError>, - cx: &mut SyncNetworkContext, - ) -> Option<(SamplingRequester, SamplingResult)> { - let Some(request) = self.requests.get_mut(&id.id) else { - // TOOD(das): This log can happen if the request is error'ed early and dropped - debug!(?id, "Sample downloaded event for unknown request"); - return None; - }; - - let result = request.on_sample_downloaded(peer_id, id.sampling_request_id, resp, cx); - self.handle_sampling_result(result, &id.id) - } - - /// Insert a downloaded column into an active sampling request. Then make progress on the - /// entire request. - /// - /// ### Returns - /// - /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. - /// - `None`: Request still active, requester should do no action - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - pub fn on_sample_verified( - &mut self, - id: SamplingId, - result: Result<(), String>, - cx: &mut SyncNetworkContext, - ) -> Option<(SamplingRequester, SamplingResult)> { - let Some(request) = self.requests.get_mut(&id.id) else { - // TOOD(das): This log can happen if the request is error'ed early and dropped - debug!(?id, "Sample verified event for unknown request"); - return None; - }; - - let result = request.on_sample_verified(id.sampling_request_id, result, cx); - self.handle_sampling_result(result, &id.id) - } - - /// Converts a result from the internal format of `ActiveSamplingRequest` (error first to use ? - /// conveniently), to an Option first format to use an `if let Some() { act on result }` pattern - /// in the sync manager. - #[instrument(parent = None, - level = "info", - fields(service = "sampling"), - name = "sampling", - skip_all - )] - fn handle_sampling_result( - &mut self, - result: Result, SamplingError>, - id: &SamplingRequester, - ) -> Option<(SamplingRequester, SamplingResult)> { - let result = result.transpose(); - if let Some(result) = result { - debug!(?id, ?result, "Sampling request completed, removing"); - metrics::inc_counter_vec( - &metrics::SAMPLING_REQUEST_RESULT, - &[metrics::from_result(&result)], - ); - self.requests.remove(id); - Some((*id, result)) - } else { - None - } - } -} - -pub struct ActiveSamplingRequest { - block_root: Hash256, - requester_id: SamplingRequester, - column_requests: FnvHashMap, - /// Mapping of column indexes for a sampling request. - column_indexes_by_sampling_request: FnvHashMap>, - /// Sequential ID for sampling requests. - current_sampling_request_id: SamplingRequestId, - column_shuffle: Vec, - required_successes: Vec, - _phantom: PhantomData, -} - -#[derive(Debug)] -pub enum SamplingError { - SendFailed(#[allow(dead_code)] &'static str), - ProcessorUnavailable, - TooManyFailures, - BadState(#[allow(dead_code)] String), - ColumnIndexOutOfBounds, -} - -/// Required success index by current failures, with p_target=5.00E-06 -/// Ref: https://colab.research.google.com/drive/18uUgT2i-m3CbzQ5TyP9XFKqTn1DImUJD#scrollTo=E82ITcgB5ATh -const REQUIRED_SUCCESSES: [usize; 11] = [16, 20, 23, 26, 29, 32, 34, 37, 39, 42, 44]; - -#[derive(Debug, Clone)] -pub enum SamplingConfig { - Default, - #[allow(dead_code)] - Custom { - required_successes: Vec, - }, -} - -impl ActiveSamplingRequest { - fn new( - block_root: Hash256, - requester_id: SamplingRequester, - sampling_config: &SamplingConfig, - spec: &ChainSpec, - ) -> Self { - // Select ahead of time the full list of to-sample columns - let mut column_shuffle = - (0..spec.number_of_columns as ColumnIndex).collect::>(); - let mut rng = thread_rng(); - column_shuffle.shuffle(&mut rng); - - Self { - block_root, - requester_id, - column_requests: <_>::default(), - column_indexes_by_sampling_request: <_>::default(), - current_sampling_request_id: SamplingRequestId(0), - column_shuffle, - required_successes: match sampling_config { - SamplingConfig::Default => REQUIRED_SUCCESSES.to_vec(), - SamplingConfig::Custom { required_successes } => required_successes.clone(), - }, - _phantom: PhantomData, - } - } - - #[cfg(test)] - pub fn get_request_status(&self, index: &ColumnIndex) -> Option { - self.column_requests.get(index).map(|req| req.status()) - } - - /// Return the current ordered list of columns that this requests has to sample to succeed - pub(crate) fn column_selection(&self) -> Vec { - self.column_shuffle - .iter() - .take(REQUIRED_SUCCESSES[0]) - .copied() - .collect() - } - - /// Insert a downloaded column into an active sampling request. Then make progress on the - /// entire request. - /// - /// ### Returns - /// - /// - `Err`: Sampling request has failed and will be dropped - /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped - /// - `Ok(None)`: Sampling request still active - pub(crate) fn on_sample_downloaded( - &mut self, - _peer_id: PeerId, - sampling_request_id: SamplingRequestId, - resp: Result<(DataColumnSidecarList, Duration), RpcResponseError>, - cx: &mut SyncNetworkContext, - ) -> Result, SamplingError> { - // Select columns to sample - // Create individual request per column - // Progress requests - // If request fails retry or expand search - // If all good return - let Some(column_indexes) = self - .column_indexes_by_sampling_request - .get(&sampling_request_id) - else { - error!( - ?sampling_request_id, - "Column indexes for the sampling request ID not found" - ); - return Ok(None); - }; - - match resp { - Ok((mut resp_data_columns, seen_timestamp)) => { - let resp_column_indexes = resp_data_columns - .iter() - .map(|r| r.index) - .collect::>(); - debug!( - block_root = %self.block_root, - column_indexes = ?resp_column_indexes, - count = resp_data_columns.len(), - "Sample download success" - ); - metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::SUCCESS]); - - // Filter the data received in the response using the requested column indexes. - let mut data_columns = vec![]; - for column_index in column_indexes { - let Some(request) = self.column_requests.get_mut(column_index) else { - warn!( - block_root = %self.block_root, - column_index, - "Active column sample request not found" - ); - continue; - }; - - let Some(data_pos) = resp_data_columns - .iter() - .position(|data| &data.index == column_index) - else { - // Peer does not have the requested data, mark peer as "dont have" and try - // again with a different peer. - debug!( - block_root = %self.block_root, - column_index, - "Sampling peer claims to not have the data" - ); - request.on_sampling_error()?; - continue; - }; - - data_columns.push(resp_data_columns.swap_remove(data_pos)); - } - - if !resp_data_columns.is_empty() { - let resp_column_indexes = resp_data_columns - .iter() - .map(|d| d.index) - .collect::>(); - debug!( - block_root = %self.block_root, - column_indexes = ?resp_column_indexes, - "Received data that was not requested" - ); - } - - // Handle the downloaded data columns. - if data_columns.is_empty() { - debug!(block_root = %self.block_root, "Received empty response"); - self.column_indexes_by_sampling_request - .remove(&sampling_request_id); - } else { - // Overwrite `column_indexes` with the column indexes received in the response. - let column_indexes = data_columns.iter().map(|d| d.index).collect::>(); - self.column_indexes_by_sampling_request - .insert(sampling_request_id, column_indexes.clone()); - // Peer has data column, send to verify - let Some(beacon_processor) = cx.beacon_processor_if_enabled() else { - // If processor is not available, error the entire sampling - debug!( - block = %self.block_root, - reason = "beacon processor unavailable", - "Dropping sampling" - ); - return Err(SamplingError::ProcessorUnavailable); - }; - debug!( - block = ?self.block_root, - ?column_indexes, - "Sending data_column for verification" - ); - if let Err(e) = beacon_processor.send_rpc_validate_data_columns( - self.block_root, - data_columns, - seen_timestamp, - SamplingId { - id: self.requester_id, - sampling_request_id, - }, - ) { - // Beacon processor is overloaded, drop sampling attempt. Failing to sample - // is not a permanent state so we should recover once the node has capacity - // and receives a descendant block. - error!( - block = %self.block_root, - reason = e.to_string(), - "Dropping sampling" - ); - return Err(SamplingError::SendFailed("beacon processor send failure")); - } - } - } - Err(err) => { - debug!( - block_root = %self.block_root, - ?column_indexes, - error = ?err, - "Sample download error" - ); - metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::FAILURE]); - - // Error downloading, malicious network errors are already penalized before - // reaching this function. Mark the peer as failed and try again with another. - for column_index in column_indexes { - let Some(request) = self.column_requests.get_mut(column_index) else { - warn!( - block_root = %self.block_root, - column_index, - "Active column sample request not found" - ); - continue; - }; - request.on_sampling_error()?; - } - } - }; - - self.continue_sampling(cx) - } - - /// Insert a column verification result into an active sampling request. Then make progress - /// on the entire request. - /// - /// ### Returns - /// - /// - `Err`: Sampling request has failed and will be dropped - /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped - /// - `Ok(None)`: Sampling request still active - pub(crate) fn on_sample_verified( - &mut self, - sampling_request_id: SamplingRequestId, - result: Result<(), String>, - cx: &mut SyncNetworkContext, - ) -> Result, SamplingError> { - let Some(column_indexes) = self - .column_indexes_by_sampling_request - .get(&sampling_request_id) - else { - error!( - ?sampling_request_id, - "Column indexes for the sampling request ID not found" - ); - return Ok(None); - }; - - match result { - Ok(_) => { - debug!(block_root = %self.block_root,?column_indexes, "Sample verification success"); - metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::SUCCESS]); - - // Valid, continue_sampling will maybe consider sampling succees - for column_index in column_indexes { - let Some(request) = self.column_requests.get_mut(column_index) else { - warn!( - block_root = %self.block_root, column_index, - "Active column sample request not found" - ); - continue; - }; - request.on_sampling_success()?; - } - } - Err(err) => { - debug!(block_root = %self.block_root, ?column_indexes, reason = ?err, "Sample verification failure"); - metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::FAILURE]); - - // Peer sent invalid data, penalize and try again from different peer - // TODO(das): Count individual failures - for column_index in column_indexes { - let Some(request) = self.column_requests.get_mut(column_index) else { - warn!( - block_root = %self.block_root, - column_index, - "Active column sample request not found" - ); - continue; - }; - let peer_id = request.on_sampling_error()?; - cx.report_peer( - peer_id, - PeerAction::LowToleranceError, - "invalid data column", - ); - } - } - } - - self.continue_sampling(cx) - } - - pub(crate) fn continue_sampling( - &mut self, - cx: &mut SyncNetworkContext, - ) -> Result, SamplingError> { - // First check if sampling is completed, by computing `required_successes` - let mut successes = 0; - let mut failures = 0; - let mut ongoings = 0; - - for request in self.column_requests.values() { - if request.is_completed() { - successes += 1; - } - if request.is_failed() { - failures += 1; - } - if request.is_ongoing() { - ongoings += 1; - } - } - - // If there are too many failures, consider the sampling failed - let Some(required_successes) = self.required_successes.get(failures) else { - return Err(SamplingError::TooManyFailures); - }; - - // If there are enough successes, consider the sampling complete - if successes >= *required_successes { - return Ok(Some(())); - } - - // First, attempt to progress sampling by requesting more columns, so that request failures - // are accounted for below. - - // Group the requested column indexes by the destination peer to batch sampling requests. - let mut column_indexes_to_request = FnvHashMap::default(); - for idx in 0..*required_successes { - // Re-request columns. Note: out of bounds error should never happen, inputs are hardcoded - let column_index = *self - .column_shuffle - .get(idx) - .ok_or(SamplingError::ColumnIndexOutOfBounds)?; - let request = self - .column_requests - .entry(column_index) - .or_insert(ActiveColumnSampleRequest::new(column_index)); - - if request.is_ready_to_request() { - if let Some(peer_id) = request.choose_peer(cx) { - let indexes = column_indexes_to_request.entry(peer_id).or_insert(vec![]); - indexes.push(column_index); - } - } - } - - // Send requests. - let mut sent_request = false; - for (peer_id, column_indexes) in column_indexes_to_request { - cx.data_column_lookup_request( - DataColumnsByRootRequester::Sampling(SamplingId { - id: self.requester_id, - sampling_request_id: self.current_sampling_request_id, - }), - peer_id, - DataColumnsByRootSingleBlockRequest { - block_root: self.block_root, - indices: column_indexes.clone(), - }, - // false = We issue request to custodians who may or may not have received the - // samples yet. We don't any signal (like an attestation or status messages that the - // custodian has received data). - false, - ) - .map_err(SamplingError::SendFailed)?; - self.column_indexes_by_sampling_request - .insert(self.current_sampling_request_id, column_indexes.clone()); - self.current_sampling_request_id.0 += 1; - sent_request = true; - - // Update request status. - for column_index in column_indexes { - let Some(request) = self.column_requests.get_mut(&column_index) else { - continue; - }; - request.on_start_sampling(peer_id)?; - } - } - - // Make sure that sampling doesn't stall, by ensuring that this sampling request will - // receive a new event of some type. If there are no ongoing requests, and no new - // request was sent, loop to increase the required_successes until the sampling fails if - // there are no peers. - if ongoings == 0 && !sent_request { - debug!(block_root = %self.block_root, "Sampling request stalled"); - } - - Ok(None) - } -} - -mod request { - use super::SamplingError; - use crate::sync::network_context::SyncNetworkContext; - use beacon_chain::BeaconChainTypes; - use lighthouse_network::PeerId; - use rand::seq::SliceRandom; - use rand::thread_rng; - use std::collections::HashSet; - use types::data_column_sidecar::ColumnIndex; - - pub(crate) struct ActiveColumnSampleRequest { - column_index: ColumnIndex, - status: Status, - // TODO(das): Should downscore peers that claim to not have the sample? - peers_dont_have: HashSet, - } - - // Exposed only for testing assertions in lookup tests - #[derive(Debug, Clone)] - pub(crate) enum Status { - NoPeers, - NotStarted, - Sampling(PeerId), - Verified, - } - - impl ActiveColumnSampleRequest { - pub(crate) fn new(column_index: ColumnIndex) -> Self { - Self { - column_index, - status: Status::NotStarted, - peers_dont_have: <_>::default(), - } - } - - pub(crate) fn is_completed(&self) -> bool { - match self.status { - Status::NoPeers | Status::NotStarted | Status::Sampling(_) => false, - Status::Verified => true, - } - } - - pub(crate) fn is_failed(&self) -> bool { - match self.status { - Status::NotStarted | Status::Sampling(_) | Status::Verified => false, - Status::NoPeers => true, - } - } - - pub(crate) fn is_ongoing(&self) -> bool { - match self.status { - Status::NotStarted | Status::NoPeers | Status::Verified => false, - Status::Sampling(_) => true, - } - } - - pub(crate) fn is_ready_to_request(&self) -> bool { - match self.status { - Status::NoPeers | Status::NotStarted => true, - Status::Sampling(_) | Status::Verified => false, - } - } - - #[cfg(test)] - pub(crate) fn status(&self) -> Status { - self.status.clone() - } - - pub(crate) fn choose_peer( - &mut self, - cx: &SyncNetworkContext, - ) -> Option { - // TODO: When is a fork and only a subset of your peers know about a block, sampling should only - // be queried on the peers on that fork. Should this case be handled? How to handle it? - let mut peer_ids = cx.get_custodial_peers(self.column_index); - - peer_ids.retain(|peer_id| !self.peers_dont_have.contains(peer_id)); - - if let Some(peer_id) = peer_ids.choose(&mut thread_rng()) { - Some(*peer_id) - } else { - self.status = Status::NoPeers; - None - } - } - - pub(crate) fn on_start_sampling(&mut self, peer_id: PeerId) -> Result<(), SamplingError> { - match self.status.clone() { - Status::NoPeers | Status::NotStarted => { - self.status = Status::Sampling(peer_id); - Ok(()) - } - other => Err(SamplingError::BadState(format!( - "bad state on_start_sampling expected NoPeers|NotStarted got {other:?}. column_index:{}", - self.column_index - ))), - } - } - - pub(crate) fn on_sampling_error(&mut self) -> Result { - match self.status.clone() { - Status::Sampling(peer_id) => { - self.peers_dont_have.insert(peer_id); - self.status = Status::NotStarted; - Ok(peer_id) - } - other => Err(SamplingError::BadState(format!( - "bad state on_sampling_error expected Sampling got {other:?}. column_index:{}", - self.column_index - ))), - } - } - - pub(crate) fn on_sampling_success(&mut self) -> Result<(), SamplingError> { - match &self.status { - Status::Sampling(_) => { - self.status = Status::Verified; - Ok(()) - } - other => Err(SamplingError::BadState(format!( - "bad state on_sampling_success expected Sampling got {other:?}. column_index:{}", - self.column_index - ))), - } - } - } -} diff --git a/beacon_node/network/src/sync/range_data_column_batch_request.rs b/beacon_node/network/src/sync/range_data_column_batch_request.rs new file mode 100644 index 0000000000..b912a6badc --- /dev/null +++ b/beacon_node/network/src/sync/range_data_column_batch_request.rs @@ -0,0 +1,298 @@ +use std::collections::{HashMap, HashSet}; + +use crate::sync::block_sidecar_coupling::{ByRangeRequest, CouplingError}; +use crate::sync::network_context::MAX_COLUMN_RETRIES; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use itertools::Itertools; +use lighthouse_network::PeerId; +use lighthouse_network::service::api_types::DataColumnsByRangeRequestId; +use std::sync::Arc; +use types::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Slot}; + +pub struct RangeDataColumnBatchRequest { + requests: HashMap< + DataColumnsByRangeRequestId, + ByRangeRequest>, + >, + /// The column indices corresponding to the request + column_peers: HashMap>, + expected_custody_columns: HashSet, + attempt: usize, + beacon_chain: Arc>, + epoch: Epoch, +} + +impl RangeDataColumnBatchRequest { + pub fn new( + by_range_requests: Vec<(DataColumnsByRangeRequestId, Vec)>, + beacon_chain: Arc>, + epoch: Epoch, + ) -> Self { + let requests = by_range_requests + .clone() + .into_iter() + .map(|(req, _)| (req, ByRangeRequest::Active(req))) + .collect::>(); + + let column_peers = by_range_requests.clone().into_iter().collect(); + + let expected_custody_columns = by_range_requests + .into_iter() + .flat_map(|(_, column_indices)| column_indices) + .collect(); + + Self { + requests, + column_peers, + expected_custody_columns, + beacon_chain, + epoch, + attempt: 0, + } + } + + pub fn add_custody_columns( + &mut self, + req_id: DataColumnsByRangeRequestId, + columns: Vec>>, + ) -> Result<(), String> { + let req = self + .requests + .get_mut(&req_id) + .ok_or(format!("unknown data columns by range req_id {req_id}"))?; + req.finish(req_id, columns) + } + + pub fn responses( + &mut self, + ) -> Option, CouplingError>> { + let mut received_columns_for_slot: HashMap> = + HashMap::new(); + let mut column_to_peer_id: HashMap = HashMap::new(); + + for req in self.requests.values() { + let Some(columns) = req.to_finished() else { + return None; + }; + + for column in columns { + received_columns_for_slot + .entry(column.slot()) + .or_default() + .push(column.clone()); + } + } + + // Note: this assumes that only 1 peer is responsible for a column + // with a batch. + for (id, columns) in self.column_peers.iter() { + for column in columns { + column_to_peer_id.insert(*column, id.peer); + } + } + + // An "attempt" is complete here after we have received a response for all the + // requests we made. i.e. `req.to_finished()` returns Some for all requests. + self.attempt += 1; + + let resp = self.responses_with_custody_columns( + received_columns_for_slot, + column_to_peer_id, + &self.expected_custody_columns, + self.attempt, + ); + + if let Err(CouplingError::DataColumnPeerFailure { + error: _, + faulty_peers, + exceeded_retries: _, + }) = &resp + { + for (_, peer) in faulty_peers.iter() { + // find the req id associated with the peer and + // delete it from the entries as we are going to make + // a separate attempt for those components. + self.requests.retain(|&k, _| k.peer != *peer); + } + } + Some(resp) + } + + fn responses_with_custody_columns( + &self, + mut received_columns_for_slot: HashMap>, + column_to_peer: HashMap, + expected_custody_columns: &HashSet, + attempt: usize, + ) -> Result, CouplingError> { + let mut naughty_peers = vec![]; + let mut result: DataColumnSidecarList = vec![]; + + let forward_blocks_iter = self + .beacon_chain + .forwards_iter_block_roots_until( + self.epoch.start_slot(T::EthSpec::slots_per_epoch()), + self.epoch.end_slot(T::EthSpec::slots_per_epoch()), + ) + .map_err(|_| { + CouplingError::InternalError("Failed to fetch block root iterator".to_string()) + })?; + + for block_iter_result in forward_blocks_iter { + let (block_root, slot) = block_iter_result.map_err(|_| { + CouplingError::InternalError("Failed to iterate block roots".to_string()) + })?; + + let Some(block) = self + .beacon_chain + .get_blinded_block(&block_root) + .ok() + .flatten() + else { + // The block root we are fetching is from the forwards block root iterator. This doesn't seem like a possible scenario. + return Err(CouplingError::InternalError( + "Block root from forwards block iterator not found in db".to_string(), + )); + }; + + let Some(columns) = received_columns_for_slot.remove(&slot) else { + // If at least one blob is expected for this slot but none have been served, penalize all peers + // The slot check ensures we arent checking a skipped slot. + if block.num_expected_blobs() != 0 && block.slot() == slot { + for column in expected_custody_columns { + if let Some(naughty_peer) = column_to_peer.get(column) { + naughty_peers.push((*column, *naughty_peer)); + } + } + } + continue; + }; + + // This is a skipped slot, skip to the next slot after we verify that peers + // didn't serve us columns for a skipped slot + if block.slot() != slot { + // If we received columns for a skipped slot, punish the peer + if !columns.is_empty() { + for column in expected_custody_columns { + if let Some(naughty_peer) = column_to_peer.get(column) { + naughty_peers.push((*column, *naughty_peer)); + } + } + } + + continue; + } + + let column_block_roots = columns + .iter() + .map(|column| column.block_root()) + .unique() + .collect::>(); + + let column_block_signatures = columns + .iter() + .map(|column| column.signed_block_header.signature.clone()) + .unique() + .collect::>(); + + let column_block_root = match column_block_roots.as_slice() { + // We expect a single unique block root + [column_block_root] => *column_block_root, + // If there are no block roots, penalize all peers + [] => { + for column in &columns { + if let Some(naughty_peer) = column_to_peer.get(&column.index) { + naughty_peers.push((column.index, *naughty_peer)); + } + } + continue; + } + // If theres more than one unique block root penalize the peers serving the bad block roots. + column_block_roots => { + for column in columns { + if column_block_roots.contains(&column.block_root()) + && block_root != column.block_root() + && let Some(naughty_peer) = column_to_peer.get(&column.index) + { + naughty_peers.push((column.index, *naughty_peer)); + } + } + continue; + } + }; + + let column_block_signature = match column_block_signatures.as_slice() { + // We expect a single unique block signature + [block_signature] => block_signature, + // If there are no block signatures, penalize all peers + [] => { + for column in &columns { + if let Some(naughty_peer) = column_to_peer.get(&column.index) { + naughty_peers.push((column.index, *naughty_peer)); + } + } + continue; + } + // If theres more than one unique block signature, penalize the peers serving the + // invalid block signatures. + column_block_signatures => { + for column in columns { + if column_block_signatures.contains(&column.signed_block_header.signature) + && block.signature() != &column.signed_block_header.signature + && let Some(naughty_peer) = column_to_peer.get(&column.index) + { + naughty_peers.push((column.index, *naughty_peer)); + } + } + continue; + } + }; + + // if the block root doesn't match the columns block root, penalize the peers + if block_root != column_block_root { + for column in &columns { + if let Some(naughty_peer) = column_to_peer.get(&column.index) { + naughty_peers.push((column.index, *naughty_peer)); + } + } + } + + // If the block signature doesn't match the columns block signature, penalize the peers + if block.signature() != column_block_signature { + for column in &columns { + if let Some(naughty_peer) = column_to_peer.get(&column.index) { + naughty_peers.push((column.index, *naughty_peer)); + } + } + } + + let received_columns = columns.iter().map(|c| c.index).collect::>(); + + let missing_columns = expected_custody_columns + .difference(&received_columns) + .collect::>(); + + // blobs are expected for this slot but there is at least one missing columns + // penalize the peers responsible for those columns. + if block.num_expected_blobs() != 0 && !missing_columns.is_empty() { + for column in missing_columns { + if let Some(naughty_peer) = column_to_peer.get(column) { + naughty_peers.push((*column, *naughty_peer)); + }; + } + } + + result.extend(columns); + } + + if !naughty_peers.is_empty() { + return Err(CouplingError::DataColumnPeerFailure { + error: "Bad or missing columns for some slots".to_string(), + faulty_peers: naughty_peers, + exceeded_retries: attempt >= MAX_COLUMN_RETRIES, + }); + } + + Ok(result) + } +} diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index be01734417..4ce10e23ca 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,18 +1,25 @@ -use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; use super::RangeSyncType; use crate::metrics; use crate::network_beacon_processor::ChainSegmentProcessId; +use crate::sync::batch::BatchId; +use crate::sync::batch::{ + BatchConfig, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, +}; +use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::network_context::{RangeRequestId, RpcRequestSendError, RpcResponseError}; -use crate::sync::{network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult}; -use beacon_chain::block_verification_types::RpcBlock; +use crate::sync::{BatchProcessResult, network_context::SyncNetworkContext}; use beacon_chain::BeaconChainTypes; +use beacon_chain::block_verification_types::RpcBlock; use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerAction, PeerId}; +use lighthouse_tracing::SPAN_SYNCING_CHAIN; use logging::crit; -use std::collections::{btree_map::Entry, BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashSet, btree_map::Entry}; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; use strum::IntoStaticStr; -use tracing::{debug, instrument, warn}; -use types::{Epoch, EthSpec, Hash256, Slot}; +use tracing::{Span, debug, instrument, warn}; +use types::{ColumnIndex, Epoch, EthSpec, Hash256, Slot}; /// Blocks are downloaded in batches from peers. This constant specifies how many epochs worth of /// blocks per batch are requested _at most_. A batch may request less blocks to account for @@ -33,6 +40,35 @@ const BATCH_BUFFER_SIZE: u8 = 5; /// and continued is now in an inconsistent state. pub type ProcessingResult = Result; +type RpcBlocks = Vec>; +type RangeSyncBatchInfo = BatchInfo, RpcBlocks>; +type RangeSyncBatches = BTreeMap>; + +/// The number of times to retry a batch before it is considered failed. +const MAX_BATCH_DOWNLOAD_ATTEMPTS: u8 = 5; + +/// Invalid batches are attempted to be re-downloaded from other peers. If a batch cannot be processed +/// after `MAX_BATCH_PROCESSING_ATTEMPTS` times, it is considered faulty. +const MAX_BATCH_PROCESSING_ATTEMPTS: u8 = 3; + +pub struct RangeSyncBatchConfig { + marker: PhantomData, +} + +impl BatchConfig for RangeSyncBatchConfig { + fn max_batch_download_attempts() -> u8 { + MAX_BATCH_DOWNLOAD_ATTEMPTS + } + fn max_batch_processing_attempts() -> u8 { + MAX_BATCH_PROCESSING_ATTEMPTS + } + fn batch_attempt_hash(data: &D) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() + } +} + /// Reasons for removing a chain #[derive(Debug)] #[allow(dead_code)] @@ -53,7 +89,6 @@ pub struct KeepChain; /// A chain identifier pub type ChainId = Id; -pub type BatchId = Epoch; #[derive(Debug, Copy, Clone, IntoStaticStr)] pub enum SyncingChainType { @@ -83,7 +118,7 @@ pub struct SyncingChain { pub target_head_root: Hash256, /// Sorted map of batches undergoing some kind of processing. - batches: BTreeMap>, + batches: RangeSyncBatches, /// The peers that agree on the `target_head_slot` and `target_head_root` as a canonical chain /// and thus available to download this chain from, as well as the batches we are currently @@ -110,6 +145,9 @@ pub struct SyncingChain { /// The current processing batch, if any. current_processing_batch: Option, + + /// The span to track the lifecycle of the syncing chain. + span: Span, } #[derive(PartialEq, Debug)] @@ -122,6 +160,19 @@ pub enum ChainSyncingState { impl SyncingChain { #[allow(clippy::too_many_arguments)] + #[instrument( + name = SPAN_SYNCING_CHAIN, + parent = None, + level="debug", + skip_all, + fields( + chain_id = %id, + start_epoch = %start_epoch, + target_head_slot = %target_head_slot, + target_head_root = %target_head_root, + chain_type = ?chain_type, + ) + )] pub fn new( id: Id, start_epoch: Epoch, @@ -130,6 +181,7 @@ impl SyncingChain { peer_id: PeerId, chain_type: SyncingChainType, ) -> Self { + let span = Span::current(); SyncingChain { id, chain_type, @@ -144,6 +196,7 @@ impl SyncingChain { attempted_optimistic_starts: HashSet::default(), state: ChainSyncingState::Stopped, current_processing_batch: None, + span, } } @@ -153,25 +206,21 @@ impl SyncingChain { } /// Check if the chain has peers from which to process batches. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn available_peers(&self) -> usize { self.peers.len() } /// Get the chain's id. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn id(&self) -> ChainId { self.id } /// Peers currently syncing this chain. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn peers(&self) -> impl Iterator + '_ { self.peers.iter().cloned() } /// Progress in epochs made by the chain - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn processed_epochs(&self) -> u64 { self.processing_target .saturating_sub(self.start_epoch) @@ -179,7 +228,6 @@ impl SyncingChain { } /// Returns the total count of pending blocks in all the batches of this chain - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn pending_blocks(&self) -> usize { self.batches .values() @@ -189,8 +237,9 @@ impl SyncingChain { /// Removes a peer from the chain. /// If the peer has active batches, those are considered failed and re-requested. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn remove_peer(&mut self, peer_id: &PeerId) -> ProcessingResult { + let _guard = self.span.clone().entered(); + debug!(peer = %peer_id, "Removing peer from chain"); self.peers.remove(peer_id); if self.peers.is_empty() { @@ -201,7 +250,6 @@ impl SyncingChain { } /// Returns the latest slot number that has been processed. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn current_processed_slot(&self) -> Slot { // the last slot we processed was included in the previous batch, and corresponds to the // first slot of the current target epoch @@ -211,7 +259,6 @@ impl SyncingChain { /// A block has been received for a batch on this chain. /// If the block correctly completes the batch it will be processed if possible. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn on_block_response( &mut self, network: &mut SyncNetworkContext, @@ -220,6 +267,7 @@ impl SyncingChain { request_id: Id, blocks: Vec>, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); // check if we have this batch let batch = match self.batches.get_mut(&batch_id) { None => { @@ -234,7 +282,7 @@ impl SyncingChain { // request_id matches // TODO(das): removed peer_id matching as the node may request a different peer for data // columns. - if !batch.is_expecting_block(&request_id) { + if !batch.is_expecting_request_id(&request_id) { return Ok(KeepChain); } batch @@ -245,11 +293,19 @@ impl SyncingChain { // Remove the request from the peer's active batches // TODO(das): should use peer group here https://github.com/sigp/lighthouse/issues/6258 - let received = batch.download_completed(blocks, *peer_id)?; + let received = blocks.len(); + batch.download_completed(blocks, *peer_id)?; let awaiting_batches = batch_id .saturating_sub(self.optimistic_start.unwrap_or(self.processing_target)) / EPOCHS_PER_BATCH; - debug!(epoch = %batch_id, blocks = received, batch_state = self.visualize_batch_state(), %awaiting_batches,"Batch downloaded"); + debug!( + epoch = %batch_id, + blocks = received, + batch_state = self.visualize_batch_state(), + %awaiting_batches, + %peer_id, + "Batch downloaded" + ); // pre-emptively request more blocks from peers whilst we process current blocks, self.request_batches(network)?; @@ -258,7 +314,6 @@ impl SyncingChain { /// Processes the batch with the given id. /// The batch must exist and be ready for processing - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn process_batch( &mut self, network: &mut SyncNetworkContext, @@ -306,7 +361,6 @@ impl SyncingChain { } /// Processes the next ready batch, prioritizing optimistic batches over the processing target. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn process_completed_batches( &mut self, network: &mut SyncNetworkContext, @@ -320,43 +374,44 @@ impl SyncingChain { // // First try our optimistic start, if any. If this batch is ready, we process it. If the // batch has not already been completed, check the current chain target. - if let Some(epoch) = self.optimistic_start { - if let Some(batch) = self.batches.get(&epoch) { - let state = batch.state(); - match state { - BatchState::AwaitingProcessing(..) => { - // this batch is ready - debug!(%epoch, "Processing optimistic start"); - return self.process_batch(network, epoch); - } - BatchState::Downloading(..) => { - // The optimistic batch is being downloaded. We wait for this before - // attempting to process other batches. - return Ok(KeepChain); - } - BatchState::Poisoned => unreachable!("Poisoned batch"), - BatchState::Processing(_) - | BatchState::AwaitingDownload - | BatchState::Failed => { - // these are all inconsistent states: - // - Processing -> `self.current_processing_batch` is None - // - Failed -> non recoverable batch. For an optimistic batch, it should - // have been removed - // - AwaitingDownload -> A recoverable failed batch should have been - // re-requested. - return Err(RemoveChain::WrongChainState(format!( - "Optimistic batch indicates inconsistent chain state: {:?}", - state - ))); - } - BatchState::AwaitingValidation(_) => { - // If an optimistic start is given to the chain after the corresponding - // batch has been requested and processed we can land here. We drop the - // optimistic candidate since we can't conclude whether the batch included - // blocks or not at this point - debug!(batch = %epoch, "Dropping optimistic candidate"); - self.optimistic_start = None; - } + if let Some(epoch) = self.optimistic_start + && let Some(batch) = self.batches.get(&epoch) + { + let state = batch.state(); + match state { + BatchState::AwaitingProcessing(..) => { + // this batch is ready + debug!(%epoch, "Processing optimistic start"); + return self.process_batch(network, epoch); + } + BatchState::Downloading(..) => { + // The optimistic batch is being downloaded. We wait for this before + // attempting to process other batches. + return Ok(KeepChain); + } + BatchState::Poisoned => unreachable!("Poisoned batch"), + // Batches can be in `AwaitingDownload` state if there weren't good data column subnet + // peers to send the request to. + BatchState::AwaitingDownload => return Ok(KeepChain), + BatchState::Processing(_) | BatchState::Failed => { + // these are all inconsistent states: + // - Processing -> `self.current_processing_batch` is None + // - Failed -> non recoverable batch. For an optimistic batch, it should + // have been removed + // - AwaitingDownload -> A recoverable failed batch should have been + // re-requested. + return Err(RemoveChain::WrongChainState(format!( + "Optimistic batch indicates inconsistent chain state: {:?}", + state + ))); + } + BatchState::AwaitingValidation(_) => { + // If an optimistic start is given to the chain after the corresponding + // batch has been requested and processed we can land here. We drop the + // optimistic candidate since we can't conclude whether the batch included + // blocks or not at this point + debug!(batch = %epoch, "Dropping optimistic candidate"); + self.optimistic_start = None; } } } @@ -372,9 +427,12 @@ impl SyncingChain { // Batch is not ready, nothing to process } BatchState::Poisoned => unreachable!("Poisoned batch"), - BatchState::Failed | BatchState::AwaitingDownload | BatchState::Processing(_) => { + // Batches can be in `AwaitingDownload` state if there weren't good data column subnet + // peers to send the request to. + BatchState::AwaitingDownload => return Ok(KeepChain), + BatchState::Failed | BatchState::Processing(_) => { // these are all inconsistent states: - // - Failed -> non recoverable batch. Chain should have beee removed + // - Failed -> non recoverable batch. Chain should have been removed // - AwaitingDownload -> A recoverable failed batch should have been // re-requested. // - Processing -> `self.current_processing_batch` is None @@ -406,23 +464,27 @@ impl SyncingChain { // return an error. return Ok(KeepChain); } else { - return Err(RemoveChain::WrongChainState(format!( - "Batch not found for current processing target {}", - self.processing_target - ))); + // NOTE: It is possible that the batch doesn't exist for the processing id. This can happen + // when we complete a batch and attempt to download a new batch but there are: + // 1. No idle peers to download from + // 2. No good peers on sampling subnets + // + // In these cases, a batch will not yet exist. + debug!(batch = %self.processing_target, "The processing batch has not been scheduled for download yet. Awaiting progress"); } + Ok(KeepChain) } /// The block processor has completed processing a batch. This function handles the result /// of the batch processor. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn on_batch_process_result( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, result: &BatchProcessResult, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); // the first two cases are possible if the chain advances while waiting for a processing // result let batch_state = self.visualize_batch_state(); @@ -523,7 +585,7 @@ impl SyncingChain { imported_blocks, penalty, } => { - // Penalize the peer appropiately. + // Penalize the peer appropriately. network.report_peer(peer, *penalty, "faulty_batch"); // Check if this batch is allowed to continue @@ -565,13 +627,13 @@ impl SyncingChain { } BatchProcessResult::NonFaultyFailure => { batch.processing_completed(BatchProcessingResult::NonFaultyFailure)?; - // Simply redownload the batch. - self.send_batch(network, batch_id) + + // Simply re-download all batches in `AwaitingDownload` state. + self.attempt_send_awaiting_download_batches(network, "non-faulty-failure") } } } - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn reject_optimistic_batch( &mut self, network: &mut SyncNetworkContext, @@ -606,7 +668,6 @@ impl SyncingChain { /// If a previous batch has been validated and it had been re-processed, penalize the original /// peer. #[allow(clippy::modulo_one)] - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn advance_chain(&mut self, network: &mut SyncNetworkContext, validating_epoch: Epoch) { // make sure this epoch produces an advancement if validating_epoch <= self.start_epoch { @@ -628,7 +689,7 @@ impl SyncingChain { // only for batches awaiting validation can we be sure the last attempt is // right, and thus, that any different attempt is wrong match batch.state() { - BatchState::AwaitingValidation(ref processed_attempt) => { + BatchState::AwaitingValidation(processed_attempt) => { for attempt in batch.attempts() { // The validated batch has been re-processed if attempt.hash != processed_attempt.hash { @@ -674,10 +735,10 @@ impl SyncingChain { BatchState::AwaitingProcessing(..) => {} BatchState::Processing(_) => { debug!(batch = %id, %batch, "Advancing chain while processing a batch"); - if let Some(processing_id) = self.current_processing_batch { - if id <= processing_id { - self.current_processing_batch = None; - } + if let Some(processing_id) = self.current_processing_batch + && id <= processing_id + { + self.current_processing_batch = None; } } } @@ -692,15 +753,17 @@ impl SyncingChain { // won't have this batch, so we need to request it. self.to_be_downloaded += EPOCHS_PER_BATCH; } - if let Some(epoch) = self.optimistic_start { - if epoch <= validating_epoch { - self.optimistic_start = None; - } + if let Some(epoch) = self.optimistic_start + && epoch <= validating_epoch + { + self.optimistic_start = None; } + debug!( previous_start = %old_start, new_start = %self.start_epoch, processing_target = %self.processing_target, + id=%self.id, "Chain advanced" ); } @@ -710,7 +773,6 @@ impl SyncingChain { /// These events occur when a peer has successfully responded with blocks, but the blocks we /// have received are incorrect or invalid. This indicates the peer has not performed as /// intended and can result in downvoting a peer. - #[instrument(parent = None,level = "info", fields(service = self.id, network), skip_all)] fn handle_invalid_batch( &mut self, network: &mut SyncNetworkContext, @@ -738,7 +800,6 @@ impl SyncingChain { } // this is our robust `processing_target`. All previous batches must be awaiting // validation - let mut redownload_queue = Vec::new(); for (id, batch) in self.batches.range_mut(..batch_id) { if let BatchOperationOutcome::Failed { blacklist } = batch.validation_failed()? { @@ -748,21 +809,18 @@ impl SyncingChain { failing_batch: *id, }); } - redownload_queue.push(*id); } // no batch maxed out it process attempts, so now the chain's volatile progress must be // reset self.processing_target = self.start_epoch; - for id in redownload_queue { - self.send_batch(network, id)?; - } - // finally, re-request the failed batch. - self.send_batch(network, batch_id) + // finally, re-request the failed batch and all other batches in `AwaitingDownload` state. + self.attempt_send_awaiting_download_batches(network, "handle_invalid_batch") } pub fn stop_syncing(&mut self) { + debug!(parent: &self.span, "Stopping syncing"); self.state = ChainSyncingState::Stopped; } @@ -770,13 +828,18 @@ impl SyncingChain { /// This chain has been requested to start syncing. /// /// This could be new chain, or an old chain that is being resumed. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn start_syncing( &mut self, network: &mut SyncNetworkContext, local_finalized_epoch: Epoch, optimistic_start_epoch: Epoch, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); + debug!( + ?local_finalized_epoch, + ?optimistic_start_epoch, + "Start syncing chain" + ); // to avoid dropping local progress, we advance the chain wrt its batch boundaries. This let align = |epoch| { // start_epoch + (number of batches in between)*length_of_batch @@ -789,6 +852,9 @@ impl SyncingChain { // advance the chain to the new validating epoch self.advance_chain(network, validating_epoch); + // attempt to download any batches stuck in the `AwaitingDownload` state because of + // a lack of peers earlier + self.attempt_send_awaiting_download_batches(network, "start_syncing")?; if self.optimistic_start.is_none() && optimistic_epoch > self.processing_target && !self.attempted_optimistic_starts.contains(&optimistic_epoch) @@ -809,12 +875,13 @@ impl SyncingChain { /// Add a peer to the chain. /// /// If the chain is active, this starts requesting batches from this peer. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn add_peer( &mut self, network: &mut SyncNetworkContext, peer_id: PeerId, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); + debug!(peer_id = %peer_id, "Adding peer to chain"); self.peers.insert(peer_id); self.request_batches(network) } @@ -822,7 +889,6 @@ impl SyncingChain { /// An RPC error has occurred. /// /// If the batch exists it is re-requested. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn inject_error( &mut self, network: &mut SyncNetworkContext, @@ -831,14 +897,62 @@ impl SyncingChain { request_id: Id, err: RpcResponseError, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); let batch_state = self.visualize_batch_state(); if let Some(batch) = self.batches.get_mut(&batch_id) { + if let RpcResponseError::BlockComponentCouplingError(coupling_error) = &err { + match coupling_error { + CouplingError::DataColumnPeerFailure { + error, + faulty_peers, + exceeded_retries, + } => { + debug!(?batch_id, error, "Block components coupling error"); + // Note: we don't fail the batch here because a `CouplingError` is + // recoverable by requesting from other honest peers. + let mut failed_columns = HashSet::new(); + let mut failed_peers = HashSet::new(); + for (column, peer) in faulty_peers { + failed_columns.insert(*column); + failed_peers.insert(*peer); + } + // Retry the failed columns if the column requests haven't exceeded the + // max retries. Otherwise, remove treat it as a failed batch below. + if !*exceeded_retries { + // Set the batch back to `AwaitingDownload` before retrying. + // This is to ensure that the batch doesn't get stuck in `Downloading` state. + // + // DataColumn retries has a retry limit so calling `downloading_to_awaiting_download` + // is safe. + if let BatchOperationOutcome::Failed { blacklist } = + batch.downloading_to_awaiting_download()? + { + return Err(RemoveChain::ChainFailed { + blacklist, + failing_batch: batch_id, + }); + } + return self.retry_partial_batch( + network, + batch_id, + request_id, + failed_columns, + failed_peers, + ); + } + } + CouplingError::BlobPeerFailure(msg) => { + tracing::debug!(?batch_id, msg, "Blob peer failure"); + } + CouplingError::InternalError(msg) => { + tracing::error!(?batch_id, msg, "Block components coupling internal error"); + } + } + } // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer - // TODO(das): removed peer_id matching as the node may request a different peer for data - // columns. - if !batch.is_expecting_block(&request_id) { + if !batch.is_expecting_request_id(&request_id) { debug!( batch_epoch = %batch_id, batch_state = ?batch.state(), @@ -865,7 +979,10 @@ impl SyncingChain { failing_batch: batch_id, }); } - self.send_batch(network, batch_id) + // The errored batch is set to AwaitingDownload above. + // We now just attempt to download all batches stuck in `AwaitingDownload` + // state in the right order. + self.attempt_send_awaiting_download_batches(network, "injecting error") } else { debug!( batch_epoch = %batch_id, @@ -879,26 +996,59 @@ impl SyncingChain { } } + /// Attempts to send all batches that are in `AwaitingDownload` state. + /// + /// Batches might get stuck in `AwaitingDownload` post peerdas because of lack of peers + /// in required subnets. We need to progress them if peers are available at a later point. + pub fn attempt_send_awaiting_download_batches( + &mut self, + network: &mut SyncNetworkContext, + src: &str, + ) -> ProcessingResult { + // Collect all batches in AwaitingDownload state and see if they can be sent + let awaiting_downloads: Vec<_> = self + .batches + .iter() + .filter(|(_, batch)| matches!(batch.state(), BatchState::AwaitingDownload)) + .map(|(batch_id, _)| batch_id) + .copied() + .collect(); + debug!( + ?awaiting_downloads, + src, "Attempting to send batches awaiting download" + ); + + for batch_id in awaiting_downloads { + if self.good_peers_on_sampling_subnets(batch_id, network) { + self.send_batch(network, batch_id)?; + } else { + debug!( + src = "attempt_send_awaiting_download_batches", + "Waiting for peers to be available on sampling column subnets" + ); + } + } + Ok(KeepChain) + } + /// Requests the batch assigned to the given id from a given peer. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn send_batch( &mut self, network: &mut SyncNetworkContext, batch_id: BatchId, ) -> ProcessingResult { + let _guard = self.span.clone().entered(); + debug!(batch_epoch = %batch_id, "Requesting batch"); let batch_state = self.visualize_batch_state(); if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, batch_type) = batch.to_blocks_by_range_request(); let failed_peers = batch.failed_peers(); - // TODO(das): we should request only from peers that are part of this SyncingChain. - // However, then we hit the NoPeer error frequently which causes the batch to fail and - // the SyncingChain to be dropped. We need to handle this case more gracefully. - let synced_peers = network + let synced_column_peers = network .network_globals() .peers .read() - .synced_peers() + .synced_peers_for_epoch(batch_id) .cloned() .collect::>(); @@ -909,7 +1059,13 @@ impl SyncingChain { chain_id: self.id, batch_id, }, - &synced_peers, + // Request blocks only from peers of this specific chain + &self.peers, + // Request column from all synced peers, even if they are not part of this chain. + // This is to avoid splitting of good column peers across many head chains in a heavy forking + // environment. If the column peers and block peer are on different chains, then we return + // a coupling error and retry only the columns that failed to couple. See `Self::retry_partial_batch`. + &synced_column_peers, &failed_peers, ) { Ok(request_id) => { @@ -944,10 +1100,10 @@ impl SyncingChain { return Err(RemoveChain::ChainFailed { blacklist, failing_batch: batch_id, - }) + }); } BatchOperationOutcome::Continue => { - return self.send_batch(network, batch_id) + return self.send_batch(network, batch_id); } } } @@ -958,8 +1114,56 @@ impl SyncingChain { Ok(KeepChain) } + /// Retries partial column requests within the batch by creating new requests for the failed columns. + fn retry_partial_batch( + &mut self, + network: &mut SyncNetworkContext, + batch_id: BatchId, + id: Id, + failed_columns: HashSet, + mut failed_peers: HashSet, + ) -> ProcessingResult { + let _guard = self.span.clone().entered(); + debug!(%batch_id, %id, ?failed_columns, "Retrying partial batch"); + if let Some(batch) = self.batches.get_mut(&batch_id) { + failed_peers.extend(&batch.failed_peers()); + let req = batch.to_blocks_by_range_request().0; + + let synced_peers = network + .network_globals() + .peers + .read() + .synced_peers_for_epoch(batch_id) + .cloned() + .collect::>(); + + match network.retry_columns_by_range( + id, + &synced_peers, + &failed_peers, + req, + &failed_columns, + ) { + Ok(_) => { + // inform the batch about the new request + batch.start_downloading(id)?; + debug!( + ?batch_id, + id, "Retried column requests from different peers" + ); + return Ok(KeepChain); + } + Err(e) => { + // No need to explicitly fail the batch since its in `AwaitingDownload` state + // before we attempted to retry. + debug!(?batch_id, id, e, "Failed to retry partial batch"); + } + } + } + Ok(KeepChain) + } + /// Returns true if this chain is currently syncing. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn is_syncing(&self) -> bool { match self.state { ChainSyncingState::Syncing => true, @@ -969,11 +1173,15 @@ impl SyncingChain { /// Kickstarts the chain by sending for processing batches that are ready and requesting more /// batches if needed. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] pub fn resume( &mut self, network: &mut SyncNetworkContext, ) -> Result { + let _guard = self.span.clone().entered(); + debug!("Resuming chain"); + // attempt to download any batches stuck in the `AwaitingDownload` state because of + // a lack of peers before. + self.attempt_send_awaiting_download_batches(network, "resume")?; // Request more batches if needed. self.request_batches(network)?; // If there is any batch ready for processing, send it. @@ -982,19 +1190,20 @@ impl SyncingChain { /// Attempts to request the next required batches from the peer pool if the chain is syncing. It will exhaust the peer /// pool and left over batches until the batch buffer is reached or all peers are exhausted. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn request_batches(&mut self, network: &mut SyncNetworkContext) -> ProcessingResult { if !matches!(self.state, ChainSyncingState::Syncing) { return Ok(KeepChain); } - // find the next pending batch and request it from the peer // check if we have the batch for our optimistic start. If not, request it first. // We wait for this batch before requesting any other batches. if let Some(epoch) = self.optimistic_start { if !self.good_peers_on_sampling_subnets(epoch, network) { - debug!("Waiting for peers to be available on sampling column subnets"); + debug!( + src = "request_batches_optimistic", + "Waiting for peers to be available on sampling column subnets" + ); return Ok(KeepChain); } @@ -1003,6 +1212,8 @@ impl SyncingChain { let optimistic_batch = BatchInfo::new(&epoch, EPOCHS_PER_BATCH, batch_type); entry.insert(optimistic_batch); self.send_batch(network, epoch)?; + } else { + self.attempt_send_awaiting_download_batches(network, "request_batches_optimistic")?; } return Ok(KeepChain); } @@ -1030,21 +1241,12 @@ impl SyncingChain { ) -> bool { if network.chain.spec.is_peer_das_enabled_for_epoch(epoch) { // Require peers on all sampling column subnets before sending batches - let peers_on_all_custody_subnets = network + let sampling_subnets = network.network_globals().sampling_subnets(); + network .network_globals() - .sampling_subnets - .iter() - .all(|subnet_id| { - let peer_count = network - .network_globals() - .peers - .read() - .good_custody_subnet_peer(*subnet_id) - .count(); - - peer_count > 0 - }); - peers_on_all_custody_subnets + .peers + .read() + .has_good_custody_range_sync_peer(&sampling_subnets, epoch) } else { true } @@ -1052,7 +1254,6 @@ impl SyncingChain { /// Creates the next required batch from the chain. If there are no more batches required, /// `false` is returned. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn include_next_batch(&mut self, network: &mut SyncNetworkContext) -> Option { // don't request batches beyond the target head slot if self @@ -1066,7 +1267,7 @@ impl SyncingChain { // only request batches up to the buffer size limit // NOTE: we don't count batches in the AwaitingValidation state, to prevent stalling sync // if the current processing window is contained in a long range of skip slots. - let in_buffer = |batch: &BatchInfo| { + let in_buffer = |batch: &RangeSyncBatchInfo| { matches!( batch.state(), BatchState::Downloading(..) | BatchState::AwaitingProcessing(..) @@ -1087,7 +1288,10 @@ impl SyncingChain { // block and data column requests are currently coupled. This can be removed once we find a // way to decouple the requests and do retries individually, see issue #6258. if !self.good_peers_on_sampling_subnets(self.to_be_downloaded, network) { - debug!("Waiting for peers to be available on custody column subnets"); + debug!( + src = "include_next_batch", + "Waiting for peers to be available on custody column subnets" + ); return None; } @@ -1115,7 +1319,6 @@ impl SyncingChain { /// This produces a string of the form: [D,E,E,E,E] /// to indicate the current buffer state of the chain. The symbols are defined on each of the /// batch states. See [BatchState::visualize] for symbol definitions. - #[instrument(parent = None,level = "info", fields(chain = self.id , service = "range_sync"), skip_all)] fn visualize_batch_state(&self) -> String { let mut visualization_string = String::with_capacity((BATCH_BUFFER_SIZE * 3) as usize); @@ -1128,7 +1331,7 @@ impl SyncingChain { .get(&(self.processing_target + batch_index as u64 * EPOCHS_PER_BATCH)) { visualization_string.push(batch.visualize()); - if batch_index != BATCH_BUFFER_SIZE { + if batch_index < BATCH_BUFFER_SIZE - 1 { // Add a comma in between elements visualization_string.push(','); } @@ -1151,7 +1354,7 @@ impl SyncingChain { } } -use super::batch::WrongState as WrongBatchState; +use crate::sync::batch::WrongState as WrongBatchState; impl From for RemoveChain { fn from(err: WrongBatchState) -> Self { RemoveChain::WrongBatchState(err.0) diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 9f500c61e0..1d57ee6c3d 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -9,13 +9,13 @@ use crate::metrics; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; -use lighthouse_network::service::api_types::Id; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; +use lighthouse_network::service::api_types::Id; use logging::crit; use smallvec::SmallVec; -use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::sync::Arc; use tracing::{debug, error}; use types::EthSpec; @@ -93,7 +93,7 @@ impl ChainCollection { if let Some(index) = syncing_head_ids .iter() .enumerate() - .find(|(_, &chain_id)| &chain_id == id) + .find(|&(_, &chain_id)| &chain_id == id) .map(|(i, _)| i) { // a syncing head chain was removed diff --git a/beacon_node/network/src/sync/range_sync/mod.rs b/beacon_node/network/src/sync/range_sync/mod.rs index 8f881fba90..dd9f17bfd1 100644 --- a/beacon_node/network/src/sync/range_sync/mod.rs +++ b/beacon_node/network/src/sync/range_sync/mod.rs @@ -1,17 +1,11 @@ //! This provides the logic for syncing a chain when the local node is far behind it's current //! peers. - -mod batch; mod chain; mod chain_collection; mod range; mod sync_type; -pub use batch::{ - BatchConfig, BatchInfo, BatchOperationOutcome, BatchProcessingResult, BatchState, - ByRangeRequestType, -}; -pub use chain::{BatchId, ChainId, EPOCHS_PER_BATCH}; +pub use chain::{ChainId, EPOCHS_PER_BATCH}; #[cfg(test)] pub use chain_collection::SyncChainStatus; pub use range::RangeSync; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 1ec1440991..c9656ad1d0 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -39,13 +39,14 @@ //! Each chain is downloaded in batches of blocks. The batched blocks are processed sequentially //! and further batches are requested as current blocks are being processed. -use super::chain::{BatchId, ChainId, RemoveChain, SyncingChain}; +use super::chain::{ChainId, RemoveChain, SyncingChain}; use super::chain_collection::{ChainCollection, SyncChainStatus}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::status::ToStatusMessage; -use crate::sync::network_context::{RpcResponseError, SyncNetworkContext}; use crate::sync::BatchProcessResult; +use crate::sync::batch::BatchId; +use crate::sync::network_context::{RpcResponseError, SyncNetworkContext}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::rpc::GoodbyeReason; @@ -55,7 +56,7 @@ use logging::crit; use lru_cache::LRUTimeCache; use std::collections::HashMap; use std::sync::Arc; -use tracing::{debug, instrument, trace, warn}; +use tracing::{debug, trace, warn}; use types::{Epoch, EthSpec, Hash256}; /// For how long we store failed finalized chains to prevent retries. @@ -81,12 +82,6 @@ impl RangeSync where T: BeaconChainTypes, { - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn new(beacon_chain: Arc>) -> Self { RangeSync { beacon_chain: beacon_chain.clone(), @@ -103,12 +98,6 @@ where self.failed_chains.keys().copied().collect() } - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn state(&self) -> SyncChainStatus { self.chains.state() } @@ -118,12 +107,6 @@ where /// may need to be synced as a result. A new peer, may increase the peer pool of a finalized /// chain, this may result in a different finalized chain from syncing as finalized chains are /// prioritised by peer-pool size. - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn add_peer( &mut self, network: &mut SyncNetworkContext, @@ -218,12 +201,6 @@ where /// /// This function finds the chain that made this request. Once found, processes the result. /// This request could complete a chain or simply add to its progress. - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn blocks_by_range_response( &mut self, network: &mut SyncNetworkContext, @@ -254,12 +231,6 @@ where } } - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn handle_block_process_result( &mut self, network: &mut SyncNetworkContext, @@ -292,12 +263,6 @@ where /// A peer has disconnected. This removes the peer from any ongoing chains and mappings. A /// disconnected peer could remove a chain - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn peer_disconnect(&mut self, network: &mut SyncNetworkContext, peer_id: &PeerId) { // if the peer is in the awaiting head mapping, remove it self.awaiting_head_peers.remove(peer_id); @@ -310,12 +275,6 @@ where /// which pool the peer is in. The chain may also have a batch or batches awaiting /// for this peer. If so we mark the batch as failed. The batch may then hit it's maximum /// retries. In this case, we need to remove the chain. - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] fn remove_peer(&mut self, network: &mut SyncNetworkContext, peer_id: &PeerId) { for (removed_chain, sync_type, remove_reason) in self.chains.call_all(|chain| chain.remove_peer(peer_id)) @@ -334,12 +293,6 @@ where /// /// Check to see if the request corresponds to a pending batch. If so, re-request it if possible, if there have /// been too many failed attempts for the batch, remove the chain. - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn inject_error( &mut self, network: &mut SyncNetworkContext, @@ -370,12 +323,6 @@ where } } - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] fn on_chain_removed( &mut self, chain: SyncingChain, @@ -390,15 +337,16 @@ where debug!(id = chain.id(), ?sync_type, reason = ?remove_reason, op, "Chain removed"); } - if let RemoveChain::ChainFailed { blacklist, .. } = remove_reason { - if RangeSyncType::Finalized == sync_type && blacklist { - warn!( - id = chain.id(), - "Chain failed! Syncing to its head won't be retried for at least the next {} seconds", - FAILED_CHAINS_EXPIRY_SECONDS - ); - self.failed_chains.insert(chain.target_head_root); - } + if let RemoveChain::ChainFailed { blacklist, .. } = remove_reason + && RangeSyncType::Finalized == sync_type + && blacklist + { + warn!( + id = chain.id(), + "Chain failed! Syncing to its head won't be retried for at least the next {} seconds", + FAILED_CHAINS_EXPIRY_SECONDS + ); + self.failed_chains.insert(chain.target_head_root); } metrics::inc_counter_vec_by( @@ -411,10 +359,11 @@ where let status = self.beacon_chain.status_message(); let local = SyncInfo { - head_slot: status.head_slot, - head_root: status.head_root, - finalized_epoch: status.finalized_epoch, - finalized_root: status.finalized_root, + head_slot: *status.head_slot(), + head_root: *status.head_root(), + finalized_epoch: *status.finalized_epoch(), + finalized_root: *status.finalized_root(), + earliest_available_slot: status.earliest_available_slot().ok().cloned(), }; // update the state of the collection @@ -423,12 +372,6 @@ where } /// Kickstarts sync. - #[instrument(parent = None, - level = "info", - fields(component = "range_sync"), - name = "range_sync", - skip_all - )] pub fn resume(&mut self, network: &mut SyncNetworkContext) { for (removed_chain, sync_type, remove_reason) in self.chains.call_all(|chain| chain.resume(network)) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 5863091cf0..ef52f89678 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,55 +1,54 @@ +use crate::NetworkMessage; use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::sync::block_lookups::{ BlockLookupSummary, PARENT_DEPTH_TOLERANCE, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS, }; use crate::sync::{ + SyncMessage, manager::{BlockProcessType, BlockProcessingResult, SyncManager}, - peer_sampling::SamplingConfig, - SamplingId, SyncMessage, }; -use crate::NetworkMessage; use std::sync::Arc; use std::time::Duration; use super::*; use crate::sync::block_lookups::common::ResponseType; +use beacon_chain::observed_data_sidecars::Observe; use beacon_chain::{ + AvailabilityPendingExecutedBlock, AvailabilityProcessingStatus, BlockError, + PayloadVerificationOutcome, PayloadVerificationStatus, blob_verification::GossipVerifiedBlob, block_verification_types::{AsBlock, BlockImportData}, data_availability_checker::Availability, test_utils::{ - generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, - BeaconChainHarness, EphemeralHarnessType, NumBlobs, + BeaconChainHarness, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, + generate_rand_block_and_data_columns, test_spec, }, validator_monitor::timestamp_now, - AvailabilityPendingExecutedBlock, AvailabilityProcessingStatus, BlockError, - PayloadVerificationOutcome, PayloadVerificationStatus, }; use beacon_processor::WorkEvent; use lighthouse_network::discovery::CombinedKey; use lighthouse_network::{ + NetworkConfig, NetworkGlobals, PeerId, rpc::{RPCError, RequestType, RpcErrorResponse}, service::api_types::{ AppRequestId, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, - SamplingRequester, SingleLookupReqId, SyncRequestId, + SingleLookupReqId, SyncRequestId, }, types::SyncState, - NetworkConfig, NetworkGlobals, PeerId, }; use slot_clock::{SlotClock, TestingSlotClock}; use tokio::sync::mpsc; use tracing::info; use types::{ + BeaconState, BeaconStateBase, BlobSidecar, BlockImportSource, DataColumnSidecar, EthSpec, + ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, data_column_sidecar::ColumnIndex, test_utils::{SeedableRng, TestRandom, XorShiftRng}, - BeaconState, BeaconStateBase, BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, ForkName, - Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; -const SAMPLING_REQUIRED_SUCCESSES: usize = 2; type DCByRootIds = Vec; type DCByRootId = (SyncRequestId, Vec); @@ -105,14 +104,18 @@ impl TestRig { let spec = chain.spec.clone(); // deterministic seed + let rng_08 = ::from_seed([0u8; 32]); let rng = ChaCha20Rng::from_seed([0u8; 32]); + init_tracing(); + TestRig { beacon_processor_rx, beacon_processor_rx_queue: vec![], network_rx, network_rx_queue: vec![], sync_rx, + rng_08, rng, network_globals: beacon_processor.network_globals.clone(), sync_manager: SyncManager::new( @@ -121,9 +124,6 @@ impl TestRig { beacon_processor.into(), // Pass empty recv not tied to any tx mpsc::unbounded_channel().1, - SamplingConfig::Custom { - required_successes: vec![SAMPLING_REQUIRED_SUCCESSES], - }, fork_context, ), harness, @@ -177,10 +177,6 @@ impl TestRig { )); } - fn trigger_sample_block(&mut self, block_root: Hash256, block_slot: Slot) { - self.send_sync_message(SyncMessage::SampleBlock(block_root, block_slot)) - } - /// Drain all sync messages in the sync_rx attached to the beacon processor fn drain_sync_rx(&mut self) { while let Ok(sync_message) = self.sync_rx.try_recv() { @@ -198,7 +194,7 @@ impl TestRig { ) -> (SignedBeaconBlock, Vec>) { let fork_name = self.fork_name; let rng = &mut self.rng; - generate_rand_block_and_blobs::(fork_name, num_blobs, rng, &self.spec) + generate_rand_block_and_blobs::(fork_name, num_blobs, rng) } fn rand_block_and_data_columns( @@ -257,27 +253,6 @@ impl TestRig { ); } - fn expect_no_active_sampling(&mut self) { - assert_eq!( - self.sync_manager.active_sampling_requests(), - Vec::::new(), - "expected no active sampling" - ); - } - - fn expect_active_sampling(&mut self, block_root: &Hash256) { - assert!(self - .sync_manager - .active_sampling_requests() - .contains(block_root)); - } - - fn expect_clean_finished_sampling(&mut self) { - self.expect_empty_network(); - self.expect_sampling_result_work(); - self.expect_no_active_sampling(); - } - fn assert_parent_lookups_count(&self, count: usize) { assert_eq!( self.active_parent_lookups_count(), @@ -310,21 +285,21 @@ impl TestRig { ); } - fn insert_failed_chain(&mut self, block_root: Hash256) { - self.sync_manager.insert_failed_chain(block_root); + fn insert_ignored_chain(&mut self, block_root: Hash256) { + self.sync_manager.insert_ignored_chain(block_root); } - fn assert_not_failed_chain(&mut self, chain_hash: Hash256) { - let failed_chains = self.sync_manager.get_failed_chains(); - if failed_chains.contains(&chain_hash) { - panic!("failed chains contain {chain_hash:?}: {failed_chains:?}"); + fn assert_not_ignored_chain(&mut self, chain_hash: Hash256) { + let chains = self.sync_manager.get_ignored_chains(); + if chains.contains(&chain_hash) { + panic!("ignored chains contain {chain_hash:?}: {chains:?}"); } } - fn assert_failed_chain(&mut self, chain_hash: Hash256) { - let failed_chains = self.sync_manager.get_failed_chains(); - if !failed_chains.contains(&chain_hash) { - panic!("expected failed chains to contain {chain_hash:?}: {failed_chains:?}"); + fn assert_ignored_chain(&mut self, chain_hash: Hash256) { + let chains = self.sync_manager.get_ignored_chains(); + if !chains.contains(&chain_hash) { + panic!("expected ignored chains to contain {chain_hash:?}: {chains:?}"); } } @@ -375,7 +350,7 @@ impl TestRig { } fn determinstic_key(&mut self) -> CombinedKey { - k256::ecdsa::SigningKey::random(&mut self.rng).into() + k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } pub fn new_connected_peers_for_peerdas(&mut self) { @@ -610,39 +585,6 @@ impl TestRig { }) } - fn return_empty_sampling_requests(&mut self, ids: DCByRootIds) { - for id in ids { - self.log(&format!("return empty data column for {id:?}")); - self.return_empty_sampling_request(id) - } - } - - fn return_empty_sampling_request(&mut self, (sync_request_id, _): DCByRootId) { - let peer_id = PeerId::random(); - // Send stream termination - self.send_sync_message(SyncMessage::RpcDataColumn { - sync_request_id, - peer_id, - data_column: None, - seen_timestamp: timestamp_now(), - }); - } - - fn sampling_requests_failed( - &mut self, - sampling_ids: DCByRootIds, - peer_id: PeerId, - error: RPCError, - ) { - for (sync_request_id, _) in sampling_ids { - self.send_sync_message(SyncMessage::RpcError { - peer_id, - sync_request_id, - error: error.clone(), - }) - } - } - fn complete_valid_block_request( &mut self, id: SingleLookupReqId, @@ -669,51 +611,6 @@ impl TestRig { ) } - fn complete_valid_sampling_column_requests( - &mut self, - ids: DCByRootIds, - data_columns: Vec>>, - ) { - for id in ids { - self.log(&format!("return valid data column for {id:?}")); - let indices = &id.1; - let columns_to_send = indices - .iter() - .map(|&i| data_columns[i as usize].clone()) - .collect::>(); - self.complete_valid_sampling_column_request(id, &columns_to_send); - } - } - - fn complete_valid_sampling_column_request( - &mut self, - id: DCByRootId, - data_columns: &[Arc>], - ) { - let first_dc = data_columns.first().unwrap(); - let block_root = first_dc.block_root(); - let sampling_request_id = match id.0 { - SyncRequestId::DataColumnsByRoot(DataColumnsByRootRequestId { - requester: DataColumnsByRootRequester::Sampling(sampling_id), - .. - }) => sampling_id.sampling_request_id, - _ => unreachable!(), - }; - self.complete_data_columns_by_root_request(id, data_columns); - - // Expect work event - self.expect_rpc_sample_verify_work_event(); - - // Respond with valid result - self.send_sync_message(SyncMessage::SampleVerified { - id: SamplingId { - id: SamplingRequester::ImportedBlock(block_root), - sampling_request_id, - }, - result: Ok(()), - }) - } - fn complete_valid_custody_request( &mut self, ids: DCByRootIds, @@ -1044,28 +941,7 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected RPC custody column work: {e}")) } - fn expect_rpc_sample_verify_work_event(&mut self) { - self.pop_received_processor_event(|ev| { - if ev.work_type() == beacon_processor::WorkType::RpcVerifyDataColumn { - Some(()) - } else { - None - } - }) - .unwrap_or_else(|e| panic!("Expected sample verify work: {e}")) - } - - fn expect_sampling_result_work(&mut self) { - self.pop_received_processor_event(|ev| { - if ev.work_type() == beacon_processor::WorkType::SamplingResult { - Some(()) - } else { - None - } - }) - .unwrap_or_else(|e| panic!("Expected sampling result work: {e}")) - } - + #[allow(dead_code)] fn expect_no_work_event(&mut self) { self.drain_processor_rx(); assert!(self.network_rx_queue.is_empty()); @@ -1145,11 +1021,6 @@ impl TestRig { self.log(&format!("Found expected penalty {penalty_msg}")); } - pub fn expect_single_penalty(&mut self, peer_id: PeerId, expect_penalty_msg: &'static str) { - self.expect_penalty(peer_id, expect_penalty_msg); - self.expect_no_penalty_for(peer_id); - } - pub fn block_with_parent_and_blobs( &mut self, parent_root: Hash256, @@ -1202,17 +1073,13 @@ impl TestRig { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }; - let executed_block = AvailabilityPendingExecutedBlock::new( - block, - import_data, - payload_verification_outcome, - self.network_globals.custody_columns_count() as usize, - ); + let executed_block = + AvailabilityPendingExecutedBlock::new(block, import_data, payload_verification_outcome); match self .harness .chain .data_availability_checker - .put_pending_executed_block(executed_block) + .put_executed_block(executed_block) .unwrap() { Availability::Available(_) => panic!("block removed from da_checker, available"), @@ -1227,7 +1094,12 @@ impl TestRig { .harness .chain .data_availability_checker - .put_gossip_blob(GossipVerifiedBlob::__assumed_valid(blob.into())) + .put_gossip_verified_blobs( + blob.block_root(), + std::iter::once(GossipVerifiedBlob::<_, Observe>::__assumed_valid( + blob.into(), + )), + ) .unwrap() { Availability::Available(_) => panic!("blob removed from da_checker, available"), @@ -1237,20 +1109,19 @@ impl TestRig { }; } - fn insert_block_to_processing_cache(&mut self, block: Arc>) { + fn insert_block_to_availability_cache(&mut self, block: Arc>) { self.harness .chain - .reqresp_pre_import_cache - .write() - .insert(block.canonical_root(), block); + .data_availability_checker + .put_pre_execution_block(block.canonical_root(), block, BlockImportSource::Gossip) + .unwrap(); } fn simulate_block_gossip_processing_becomes_invalid(&mut self, block_root: Hash256) { self.harness .chain - .reqresp_pre_import_cache - .write() - .remove(&block_root); + .data_availability_checker + .remove_block_on_execution_error(&block_root); self.send_sync_message(SyncMessage::GossipBlockProcessResult { block_root, @@ -1263,11 +1134,6 @@ impl TestRig { block: Arc>, ) { let block_root = block.canonical_root(); - self.harness - .chain - .reqresp_pre_import_cache - .write() - .remove(&block_root); self.insert_block_to_da_checker(block); @@ -1276,54 +1142,12 @@ impl TestRig { imported: false, }); } - - fn assert_sampling_request_ongoing(&self, block_root: Hash256, indices: &[ColumnIndex]) { - for index in indices { - let status = self - .sync_manager - .get_sampling_request_status(block_root, index) - .unwrap_or_else(|| panic!("No request state for {index}")); - if !matches!(status, crate::sync::peer_sampling::Status::Sampling { .. }) { - panic!("expected {block_root} {index} request to be on going: {status:?}"); - } - } - } - - fn assert_sampling_request_nopeers(&self, block_root: Hash256, indices: &[ColumnIndex]) { - for index in indices { - let status = self - .sync_manager - .get_sampling_request_status(block_root, index) - .unwrap_or_else(|| panic!("No request state for {index}")); - if !matches!(status, crate::sync::peer_sampling::Status::NoPeers) { - panic!("expected {block_root} {index} request to be no peers: {status:?}"); - } - } - } - - fn log_sampling_requests(&self, block_root: Hash256, indices: &[ColumnIndex]) { - let statuses = indices - .iter() - .map(|index| { - let status = self - .sync_manager - .get_sampling_request_status(block_root, index) - .unwrap_or_else(|| panic!("No request state for {index}")); - (index, status) - }) - .collect::>(); - self.log(&format!( - "Sampling request status for {block_root}: {statuses:?}" - )); - } } #[test] fn stable_rng() { - let spec = types::MainnetEthSpec::default_spec(); let mut rng = XorShiftRng::from_seed([42; 16]); - let (block, _) = - generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng, &spec); + let (block, _) = generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng); assert_eq!( block.canonical_root(), Hash256::from_slice( @@ -1624,7 +1448,7 @@ fn test_parent_lookup_too_many_download_attempts_no_blacklist() { // Trigger the request rig.trigger_unknown_parent_block(peer_id, block.into()); for i in 1..=PARENT_FAIL_TOLERANCE { - rig.assert_not_failed_chain(block_root); + rig.assert_not_ignored_chain(block_root); let id = rig.expect_block_parent_request(parent_root); if i % 2 != 0 { // The request fails. It should be tried again. @@ -1637,8 +1461,8 @@ fn test_parent_lookup_too_many_download_attempts_no_blacklist() { } } - rig.assert_not_failed_chain(block_root); - rig.assert_not_failed_chain(parent.canonical_root()); + rig.assert_not_ignored_chain(block_root); + rig.assert_not_ignored_chain(parent.canonical_root()); rig.expect_no_active_lookups_empty_network(); } @@ -1663,7 +1487,7 @@ fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { for _ in 0..PROCESSING_FAILURES { let id = rig.expect_block_parent_request(parent_root); // Blobs are only requested in the previous first iteration as this test only retries blocks - rig.assert_not_failed_chain(block_root); + rig.assert_not_ignored_chain(block_root); // send the right parent but fail processing rig.parent_lookup_block_response(id, peer_id, Some(parent.clone().into())); rig.parent_block_processed(block_root, BlockError::BlockSlotLimitReached.into()); @@ -1671,7 +1495,7 @@ fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { rig.expect_penalty(peer_id, "lookup_block_processing_failure"); } - rig.assert_not_failed_chain(block_root); + rig.assert_not_ignored_chain(block_root); rig.expect_no_active_lookups_empty_network(); } @@ -1714,7 +1538,66 @@ fn test_parent_lookup_too_deep_grow_ancestor() { ); // Should not penalize peer, but network is not clear because of the blocks_by_range requests rig.expect_no_penalty_for(peer_id); - rig.assert_failed_chain(chain_hash); + rig.assert_ignored_chain(chain_hash); +} + +// Regression test for https://github.com/sigp/lighthouse/pull/7118 +// 8042 UPDATE: block was previously added to the failed_chains cache, now it's inserted into the +// ignored chains cache. The regression test still applies as the chaild lookup is not created +#[test] +fn test_child_lookup_not_created_for_ignored_chain_parent_after_processing() { + // GIVEN: A parent chain longer than PARENT_DEPTH_TOLERANCE. + let mut rig = TestRig::test_setup(); + let mut blocks = rig.rand_blockchain(PARENT_DEPTH_TOLERANCE + 1); + let peer_id = rig.new_connected_peer(); + + // The child of the trigger block to be used to extend the chain. + let trigger_block_child = blocks.pop().unwrap(); + // The trigger block that starts the lookup. + let trigger_block = blocks.pop().unwrap(); + let tip_root = trigger_block.canonical_root(); + + // Trigger the initial unknown parent block for the tip. + rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); + + // Simulate the lookup chain building up via `ParentUnknown` errors. + for block in blocks.into_iter().rev() { + let id = rig.expect_block_parent_request(block.canonical_root()); + rig.parent_lookup_block_response(id, peer_id, Some(block.clone())); + rig.parent_lookup_block_response(id, peer_id, None); + rig.expect_block_process(ResponseType::Block); + rig.parent_block_processed( + tip_root, + BlockProcessingResult::Err(BlockError::ParentUnknown { + parent_root: block.parent_root(), + }), + ); + } + + // At this point, the chain should have been deemed too deep and pruned. + // The tip root should have been inserted into ignored chains. + rig.assert_ignored_chain(tip_root); + rig.expect_no_penalty_for(peer_id); + + // WHEN: Trigger the extending block that points to the tip. + let trigger_block_child_root = trigger_block_child.canonical_root(); + rig.trigger_unknown_block_from_attestation(trigger_block_child_root, peer_id); + let id = rig.expect_block_lookup_request(trigger_block_child_root); + rig.single_lookup_block_response(id, peer_id, Some(trigger_block_child.clone())); + rig.single_lookup_block_response(id, peer_id, None); + rig.expect_block_process(ResponseType::Block); + rig.single_block_component_processed( + id.lookup_id, + BlockProcessingResult::Err(BlockError::ParentUnknown { + parent_root: tip_root, + }), + ); + + // THEN: The extending block should not create a lookup because the tip was inserted into + // ignored chains. + rig.expect_no_active_lookups(); + rig.expect_no_penalty_for(peer_id); + rig.expect_empty_network(); } #[test] @@ -1752,7 +1635,7 @@ fn test_parent_lookup_too_deep_grow_tip() { ); // Should not penalize peer, but network is not clear because of the blocks_by_range requests rig.expect_no_penalty_for(peer_id); - rig.assert_failed_chain(tip.canonical_root()); + rig.assert_ignored_chain(tip.canonical_root()); } #[test] @@ -1805,15 +1688,14 @@ fn test_lookup_add_peers_to_parent() { } #[test] -fn test_skip_creating_failed_parent_lookup() { +fn test_skip_creating_ignored_parent_lookup() { let mut rig = TestRig::test_setup(); let (_, block, parent_root, _) = rig.rand_block_and_parent(); let peer_id = rig.new_connected_peer(); - rig.insert_failed_chain(parent_root); + rig.insert_ignored_chain(parent_root); rig.trigger_unknown_parent_block(peer_id, block.into()); - // Expect single penalty for peer, despite dropping two lookups - rig.expect_single_penalty(peer_id, "failed_chain"); - // Both current and parent lookup should be rejected + rig.expect_no_penalty_for(peer_id); + // Both current and parent lookup should not be created rig.expect_no_active_lookups(); } @@ -1951,7 +1833,7 @@ fn block_in_processing_cache_becomes_invalid() { let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); let block_root = block.canonical_root(); let peer_id = r.new_connected_peer(); - r.insert_block_to_processing_cache(block.clone().into()); + r.insert_block_to_availability_cache(block.clone().into()); r.trigger_unknown_block_from_attestation(block_root, peer_id); // Should trigger blob request let id = r.expect_blob_lookup_request(block_root); @@ -1977,7 +1859,7 @@ fn block_in_processing_cache_becomes_valid_imported() { let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); let block_root = block.canonical_root(); let peer_id = r.new_connected_peer(); - r.insert_block_to_processing_cache(block.clone().into()); + r.insert_block_to_availability_cache(block.clone().into()); r.trigger_unknown_block_from_attestation(block_root, peer_id); // Should trigger blob request let id = r.expect_blob_lookup_request(block_root); @@ -2013,137 +1895,6 @@ fn blobs_in_da_checker_skip_download() { r.expect_no_active_lookups(); } -#[test] -fn sampling_happy_path() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { - return; - }; - r.new_connected_peers_for_peerdas(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - r.trigger_sample_block(block_root, block.slot()); - // Retrieve all outgoing sample requests for random column indexes - let sampling_ids = - r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); - // Resolve all of them one by one - r.complete_valid_sampling_column_requests(sampling_ids, data_columns); - r.expect_clean_finished_sampling(); -} - -#[test] -fn sampling_with_retries() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { - return; - }; - r.new_connected_peers_for_peerdas(); - // Add another supernode to ensure that the node can retry. - r.new_connected_supernode_peer(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - r.trigger_sample_block(block_root, block.slot()); - // Retrieve all outgoing sample requests for random column indexes, and return empty responses - let sampling_ids = - r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); - r.return_empty_sampling_requests(sampling_ids); - // Expect retries for all of them, and resolve them - let sampling_ids = - r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); - r.complete_valid_sampling_column_requests(sampling_ids, data_columns); - r.expect_clean_finished_sampling(); -} - -#[test] -fn sampling_avoid_retrying_same_peer() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { - return; - }; - let peer_id_1 = r.new_connected_supernode_peer(); - let peer_id_2 = r.new_connected_supernode_peer(); - let block_root = Hash256::random(); - r.trigger_sample_block(block_root, Slot::new(0)); - // Retrieve all outgoing sample requests for random column indexes, and return empty responses - let sampling_ids = - r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); - r.sampling_requests_failed(sampling_ids, peer_id_1, RPCError::Disconnected); - // Should retry the other peer - let sampling_ids = - r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); - r.sampling_requests_failed(sampling_ids, peer_id_2, RPCError::Disconnected); - // Expect no more retries - r.expect_empty_network(); -} - -#[test] -fn sampling_batch_requests() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { - return; - }; - let _supernode = r.new_connected_supernode_peer(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - r.trigger_sample_block(block_root, block.slot()); - - // Retrieve the sample request, which should be batched. - let (sync_request_id, column_indexes) = r - .expect_only_data_columns_by_root_requests(block_root, 1) - .pop() - .unwrap(); - assert_eq!(column_indexes.len(), SAMPLING_REQUIRED_SUCCESSES); - r.assert_sampling_request_ongoing(block_root, &column_indexes); - - // Resolve the request. - r.complete_valid_sampling_column_requests( - vec![(sync_request_id, column_indexes.clone())], - data_columns, - ); - r.expect_clean_finished_sampling(); -} - -#[test] -fn sampling_batch_requests_not_enough_responses_returned() { - let Some(mut r) = TestRig::test_setup_after_fulu() else { - return; - }; - let _supernode = r.new_connected_supernode_peer(); - let (block, data_columns) = r.rand_block_and_data_columns(); - let block_root = block.canonical_root(); - r.trigger_sample_block(block_root, block.slot()); - - // Retrieve the sample request, which should be batched. - let (sync_request_id, column_indexes) = r - .expect_only_data_columns_by_root_requests(block_root, 1) - .pop() - .unwrap(); - assert_eq!(column_indexes.len(), SAMPLING_REQUIRED_SUCCESSES); - - // The request status should be set to Sampling. - r.assert_sampling_request_ongoing(block_root, &column_indexes); - - // Split the indexes to simulate the case where the supernode doesn't have the requested column. - let (column_indexes_supernode_does_not_have, column_indexes_to_complete) = - column_indexes.split_at(1); - - // Complete the requests but only partially, so a NotEnoughResponsesReturned error occurs. - let data_columns_to_complete = data_columns - .iter() - .filter(|d| column_indexes_to_complete.contains(&d.index)) - .cloned() - .collect::>(); - r.complete_data_columns_by_root_request( - (sync_request_id, column_indexes.clone()), - &data_columns_to_complete, - ); - - // The request status should be set to NoPeers since the supernode, the only peer, returned not enough responses. - r.log_sampling_requests(block_root, &column_indexes); - r.assert_sampling_request_nopeers(block_root, column_indexes_supernode_does_not_have); - - // The sampling request stalls. - r.expect_empty_network(); - r.expect_no_work_event(); - r.expect_active_sampling(&block_root); -} - #[test] fn custody_lookup_happy_path() { let Some(mut r) = TestRig::test_setup_after_fulu() else { @@ -2159,7 +1910,7 @@ fn custody_lookup_happy_path() { let id = r.expect_block_lookup_request(block.canonical_root()); r.complete_valid_block_request(id, block.into(), true); // for each slot we download `samples_per_slot` columns - let sample_column_count = spec.samples_per_slot * spec.data_columns_per_group(); + let sample_column_count = spec.samples_per_slot * spec.data_columns_per_group::(); let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, sample_column_count as usize); r.complete_valid_custody_request(custody_ids, data_columns, false); @@ -2172,17 +1923,14 @@ fn custody_lookup_happy_path() { // - Respond with stream terminator // ^ The stream terminator should be ignored and not close the next retry -// TODO(das): Test error early a sampling request and it getting drop + then receiving responses -// from pending requests. - mod deneb_only { use super::*; use beacon_chain::{ block_verification_types::{AsBlock, RpcBlock}, data_availability_checker::AvailabilityCheckError, }; + use ssz_types::RuntimeVariableList; use std::collections::VecDeque; - use types::RuntimeVariableList; struct DenebTester { rig: TestRig, @@ -2543,7 +2291,7 @@ mod deneb_only { block, self.unknown_parent_blobs .take() - .map(|vec| RuntimeVariableList::from_vec(vec, max_len)), + .map(|vec| RuntimeVariableList::new(vec, max_len).unwrap()), ) .unwrap(); self.rig.parent_block_processed( diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index ec24ddb036..23c14ff63e 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -1,23 +1,27 @@ +use crate::NetworkMessage; +use crate::sync::SyncMessage; use crate::sync::manager::SyncManager; use crate::sync::range_sync::RangeSyncType; -use crate::sync::SyncMessage; -use crate::NetworkMessage; use beacon_chain::builder::Witness; -use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_processor::WorkEvent; use lighthouse_network::NetworkGlobals; use rand_chacha::ChaCha20Rng; use slot_clock::ManualSlotClock; -use std::sync::Arc; +use std::fs::OpenOptions; +use std::io::Write; +use std::sync::{Arc, Once}; use store::MemoryStore; use tokio::sync::mpsc; +use tracing_subscriber::fmt::MakeWriter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; use types::{ChainSpec, ForkName, MinimalEthSpec as E}; mod lookups; mod range; -type T = Witness, E, MemoryStore, MemoryStore>; +type T = Witness, MemoryStore>; /// This test utility enables integration testing of Lighthouse sync components. /// @@ -61,7 +65,60 @@ struct TestRig { /// Beacon chain harness harness: BeaconChainHarness>, /// `rng` for generating test blocks and blobs. + rng_08: rand_chacha_03::ChaCha20Rng, rng: ChaCha20Rng, fork_name: ForkName, spec: Arc, } + +// Environment variable to read if `fork_from_env` feature is enabled. +pub const FORK_NAME_ENV_VAR: &str = "FORK_NAME"; +// Environment variable specifying the log output directory in CI. +pub const CI_LOGGER_DIR_ENV_VAR: &str = "CI_LOGGER_DIR"; + +static INIT_TRACING: Once = Once::new(); + +pub fn init_tracing() { + INIT_TRACING.call_once(|| { + if std::env::var(CI_LOGGER_DIR_ENV_VAR).is_ok() { + // Enable logging to log files for each test and each fork. + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(CILogWriter), + ) + .init(); + } + }); +} + +// CILogWriter writes logs to separate files for each test and each fork. +struct CILogWriter; + +impl<'a> MakeWriter<'a> for CILogWriter { + type Writer = Box; + + // fmt::Layer calls this method each time an event is recorded. + fn make_writer(&'a self) -> Self::Writer { + let log_dir = std::env::var(CI_LOGGER_DIR_ENV_VAR).unwrap(); + let fork_name = std::env::var(FORK_NAME_ENV_VAR) + .map(|s| format!("{s}_")) + .unwrap_or_default(); + + // The current test name can be got via the thread name. + let test_name = std::thread::current() + .name() + .unwrap_or("unnamed") + .replace(|c: char| !c.is_alphanumeric(), "_"); + + let file_path = format!("{log_dir}/{fork_name}{test_name}.log"); + let file = OpenOptions::new() + .append(true) + .create(true) + .open(&file_path) + .expect("failed to open a log file"); + + Box::new(file) + } +} diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 932f485dd0..cb728a90c1 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -1,19 +1,19 @@ use super::*; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::status::ToStatusMessage; +use crate::sync::SyncMessage; use crate::sync::manager::SLOT_IMPORT_TOLERANCE; use crate::sync::network_context::RangeRequestId; use crate::sync::range_sync::RangeSyncType; -use crate::sync::SyncMessage; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; -use beacon_chain::{block_verification_types::RpcBlock, EngineState, NotifyExecutionLayer}; +use beacon_chain::{EngineState, NotifyExecutionLayer, block_verification_types::RpcBlock}; use beacon_processor::WorkType; +use lighthouse_network::rpc::RequestType; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, DataColumnsByRangeRequest, OldBlocksByRangeRequest, - OldBlocksByRangeRequestV2, + OldBlocksByRangeRequestV2, StatusMessageV2, }; -use lighthouse_network::rpc::{RequestType, StatusMessage}; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, SyncRequestId, @@ -77,7 +77,7 @@ impl TestRig { /// Produce a head peer with an advanced head fn add_head_peer_with_root(&mut self, head_root: Hash256) -> PeerId { let local_info = self.local_info(); - self.add_random_peer(SyncInfo { + self.add_supernode_peer(SyncInfo { head_root, head_slot: local_info.head_slot + 1 + Slot::new(SLOT_IMPORT_TOLERANCE as u64), ..local_info @@ -93,11 +93,12 @@ impl TestRig { fn add_finalized_peer_with_root(&mut self, finalized_root: Hash256) -> PeerId { let local_info = self.local_info(); let finalized_epoch = local_info.finalized_epoch + 2; - self.add_random_peer(SyncInfo { + self.add_supernode_peer(SyncInfo { finalized_epoch, finalized_root, head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), head_root: Hash256::random(), + earliest_available_slot: None, }) } @@ -109,32 +110,35 @@ impl TestRig { finalized_root: Hash256::random(), head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), head_root: Hash256::random(), + earliest_available_slot: None, } } fn local_info(&self) -> SyncInfo { - let StatusMessage { + let StatusMessageV2 { fork_digest: _, finalized_root, finalized_epoch, head_root, head_slot, - } = self.harness.chain.status_message(); + earliest_available_slot, + } = self.harness.chain.status_message().status_v2(); SyncInfo { head_slot, head_root, finalized_epoch, finalized_root, + earliest_available_slot: Some(earliest_available_slot), } } - fn add_random_peer_not_supernode(&mut self, remote_info: SyncInfo) -> PeerId { + fn add_fullnode_peer(&mut self, remote_info: SyncInfo) -> PeerId { let peer_id = self.new_connected_peer(); self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info)); peer_id } - fn add_random_peer(&mut self, remote_info: SyncInfo) -> PeerId { + fn add_supernode_peer(&mut self, remote_info: SyncInfo) -> PeerId { // Create valid peer known to network globals // TODO(fulu): Using supernode peers to ensure we have peer across all column // subnets for syncing. Should add tests connecting to full node peers. @@ -144,17 +148,13 @@ impl TestRig { peer_id } - fn add_random_peers(&mut self, remote_info: SyncInfo, count: usize) { - for _ in 0..count { + fn add_fullnode_peers(&mut self, remote_info: SyncInfo, peer_count: usize) { + for _ in 0..peer_count { let peer = self.new_connected_peer(); - self.add_peer(peer, remote_info.clone()); + self.send_sync_message(SyncMessage::AddPeer(peer, remote_info.clone())); } } - fn add_peer(&mut self, peer: PeerId, remote_info: SyncInfo) { - self.send_sync_message(SyncMessage::AddPeer(peer, remote_info)); - } - fn assert_state(&self, state: RangeSyncType) { assert_eq!( self.sync_manager @@ -207,11 +207,12 @@ impl TestRig { return false; } } - if let Some(expected_peer) = request_filter.peer { - if peer != expected_peer { - return false; - } + if let Some(expected_peer) = request_filter.peer + && peer != expected_peer + { + return false; } + true }; @@ -426,7 +427,7 @@ impl TestRig { .chain .process_block( block_root, - build_rpc_block(block.into(), &data_sidecars, &self.spec), + build_rpc_block(block.into(), &data_sidecars), NotifyExecutionLayer::Yes, BlockImportSource::RangeSync, || Ok(()), @@ -442,25 +443,16 @@ impl TestRig { fn build_rpc_block( block: Arc>, data_sidecars: &Option>, - spec: &ChainSpec, ) -> RpcBlock { match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { RpcBlock::new(None, block, Some(blobs.clone())).unwrap() } Some(DataSidecars::DataColumns(columns)) => { - RpcBlock::new_with_custody_columns( - None, - block, - columns.clone(), - // TODO(das): Assumes CGC = max value. Change if we want to do more complex tests - columns.len(), - spec, - ) - .unwrap() + RpcBlock::new_with_custody_columns(None, block, columns.clone()).unwrap() } // Block has no data, expects zero columns - None => RpcBlock::new_without_blobs(None, block, 0), + None => RpcBlock::new_without_blobs(None, block), } } @@ -566,19 +558,14 @@ const EXTRA_SYNCED_EPOCHS: u64 = 2 + 1; fn finalized_sync_enough_global_custody_peers_few_chain_peers() { // Run for all forks let mut r = TestRig::test_setup(); - // This test creates enough global custody peers to satisfy column queries but only adds few - // peers to the chain - r.new_connected_peers_for_peerdas(); let advanced_epochs: u64 = 2; let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); - // Current priorization only sends batches to idle peers, so we need enough peers for each batch - // TODO: Test this with a single peer in the chain, it should still work - r.add_random_peers( - remote_info, - (advanced_epochs + EXTRA_SYNCED_EPOCHS) as usize, - ); + // Generate enough peers and supernodes to cover all custody columns + let peer_count = 100; + r.add_fullnode_peers(remote_info.clone(), peer_count); + r.add_supernode_peer(remote_info); r.assert_state(RangeSyncType::Finalized); let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; @@ -596,9 +583,9 @@ fn finalized_sync_not_enough_custody_peers_on_start() { let advanced_epochs: u64 = 2; let remote_info = r.finalized_remote_info_advanced_by(advanced_epochs.into()); - // Unikely that the single peer we added has enough columns for us. Tests are determinstic and + // Unikely that the single peer we added has enough columns for us. Tests are deterministic and // this error should never be hit - r.add_random_peer_not_supernode(remote_info.clone()); + r.add_fullnode_peer(remote_info.clone()); r.assert_state(RangeSyncType::Finalized); // Because we don't have enough peers on all columns we haven't sent any request. @@ -607,14 +594,9 @@ fn finalized_sync_not_enough_custody_peers_on_start() { r.expect_empty_network(); // Generate enough peers and supernodes to cover all custody columns - r.new_connected_peers_for_peerdas(); - // Note: not necessary to add this peers to the chain, as we draw from the global pool - // We still need to add enough peers to trigger batch downloads with idle peers. Same issue as - // the test above. - r.add_random_peers( - remote_info, - (advanced_epochs + EXTRA_SYNCED_EPOCHS - 1) as usize, - ); + let peer_count = 100; + r.add_fullnode_peers(remote_info.clone(), peer_count); + r.add_supernode_peer(remote_info); let last_epoch = advanced_epochs + EXTRA_SYNCED_EPOCHS; r.complete_and_process_range_sync_until(last_epoch, filter()); diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index 570b74226c..6fab7a752a 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -4,11 +4,16 @@ version = "0.2.0" authors = ["Michael Sproul "] edition = { workspace = true } +[features] +portable = ["beacon_chain/portable"] + [dependencies] bitvec = { workspace = true } -derivative = { workspace = true } +bls = { workspace = true } +educe = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } itertools = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } @@ -17,12 +22,11 @@ rayon = { workspace = true } serde = { workspace = true } state_processing = { workspace = true } store = { workspace = true } +superstruct = { workspace = true } +typenum = { workspace = true } types = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } maplit = { workspace = true } tokio = { workspace = true } - -[features] -portable = ["beacon_chain/portable"] diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 78280278e0..897a7e5ecc 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -1,14 +1,15 @@ use crate::attestation_storage::{CompactAttestationRef, CompactIndexedAttestation}; use crate::max_cover::MaxCover; use crate::reward_cache::RewardCache; +use ssz::BitList; use state_processing::common::{ attesting_indices_base::get_attesting_indices, base, get_attestation_participation_flag_indices, }; use std::collections::HashMap; use types::{ + Attestation, BeaconState, ChainSpec, EthSpec, beacon_state::BeaconStateBase, consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, - Attestation, BeaconState, BitList, ChainSpec, EthSpec, }; pub const PROPOSER_REWARD_DENOMINATOR: u64 = @@ -30,7 +31,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { total_active_balance: u64, spec: &ChainSpec, ) -> Option { - if let BeaconState::Base(ref base_state) = state { + if let BeaconState::Base(base_state) = state { Self::new_for_base(att, state, base_state, total_active_balance, spec) } else { Self::new_for_altair_or_later(att, state, reward_cache, spec) diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index 67c24b9c7a..9094c9cd4d 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -1,10 +1,13 @@ use crate::AttestationStats; +use bls::AggregateSignature; use itertools::Itertools; +use ssz::{BitList, BitVector}; use std::collections::{BTreeMap, HashMap, HashSet}; +use superstruct::superstruct; +use typenum::Unsigned; use types::{ + Attestation, AttestationData, BeaconState, Checkpoint, Epoch, EthSpec, Hash256, Slot, attestation::{AttestationBase, AttestationElectra}, - superstruct, AggregateSignature, Attestation, AttestationData, BeaconState, BitList, BitVector, - Checkpoint, Epoch, EthSpec, Hash256, Slot, Unsigned, }; #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] @@ -96,7 +99,7 @@ impl SplitAttestation { } } - pub fn as_ref(&self) -> CompactAttestationRef { + pub fn as_ref(&self) -> CompactAttestationRef<'_, E> { CompactAttestationRef { checkpoint: &self.checkpoint, data: &self.data, @@ -438,7 +441,7 @@ impl AttestationMap { } /// Iterate all attestations in the map. - pub fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl Iterator> { self.checkpoint_map .iter() .flat_map(|(checkpoint_key, attestation_map)| attestation_map.iter(checkpoint_key)) diff --git a/beacon_node/operation_pool/src/bls_to_execution_changes.rs b/beacon_node/operation_pool/src/bls_to_execution_changes.rs index b36299b51a..485f21b5c8 100644 --- a/beacon_node/operation_pool/src/bls_to_execution_changes.rs +++ b/beacon_node/operation_pool/src/bls_to_execution_changes.rs @@ -1,5 +1,5 @@ use state_processing::SigVerifiedOp; -use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::collections::{HashMap, HashSet, hash_map::Entry}; use std::sync::Arc; use types::{ AbstractExecPayload, BeaconState, ChainSpec, EthSpec, SignedBeaconBlock, @@ -19,7 +19,7 @@ pub enum ReceivedPreCapella { /// /// Using the LIFO queue for block production disincentivises spam on P2P at the Capella fork, /// and is less-relevant after that. -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Eq)] pub struct BlsToExecutionChanges { /// Map from validator index to BLS to execution change. by_validator_index: HashMap>>, diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 7481aa896a..00361450a5 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -9,7 +9,7 @@ mod reward_cache; mod sync_aggregate_id; pub use crate::bls_to_execution_changes::ReceivedPreCapella; -pub use attestation::{earliest_attestation_validators, AttMaxCover, PROPOSER_REWARD_DENOMINATOR}; +pub use attestation::{AttMaxCover, PROPOSER_REWARD_DENOMINATOR, earliest_attestation_validators}; pub use attestation_storage::{CompactAttestationRef, SplitAttestation}; pub use max_cover::MaxCover; pub use persistence::{ @@ -25,21 +25,22 @@ use crate::sync_aggregate_id::SyncAggregateId; use attester_slashing::AttesterSlashingMaxCover; use max_cover::maximum_cover; use parking_lot::{RwLock, RwLockWriteGuard}; +use rand::rng; use rand::seq::SliceRandom; -use rand::thread_rng; use state_processing::per_block_processing::errors::AttestationValidationError; use state_processing::per_block_processing::{ - get_slashable_indices_modular, verify_exit, VerifySignatures, + VerifySignatures, get_slashable_indices_modular, verify_exit, }; use state_processing::{SigVerifiedOp, VerifyOperation}; -use std::collections::{hash_map::Entry, HashMap, HashSet}; +use std::collections::{HashMap, HashSet, hash_map::Entry}; use std::marker::PhantomData; use std::ptr; +use typenum::Unsigned; use types::{ - sync_aggregate::Error as SyncAggregateError, typenum::Unsigned, AbstractExecPayload, - Attestation, AttestationData, AttesterSlashing, BeaconState, BeaconStateError, ChainSpec, - Epoch, EthSpec, ProposerSlashing, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution, Validator, + AbstractExecPayload, Attestation, AttestationData, AttesterSlashing, BeaconState, + BeaconStateError, ChainSpec, Epoch, EthSpec, ProposerSlashing, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedVoluntaryExit, Slot, SyncAggregate, + SyncCommitteeContribution, Validator, sync_aggregate::Error as SyncAggregateError, }; type SyncContributions = RwLock>>>; @@ -456,32 +457,35 @@ impl OperationPool { .collect() } - /// Prune proposer slashings for validators which are exited in the finalized epoch. - pub fn prune_proposer_slashings(&self, head_state: &BeaconState) { + /// Prune proposer slashings for validators which are already slashed or exited in the finalized + /// epoch. + pub fn prune_proposer_slashings(&self, finalized_state: &BeaconState) { prune_validator_hash_map( &mut self.proposer_slashings.write(), - |_, validator| validator.exit_epoch <= head_state.finalized_checkpoint().epoch, - head_state, + |_, validator| { + validator.slashed || validator.exit_epoch <= finalized_state.current_epoch() + }, + finalized_state, ); } /// Prune attester slashings for all slashed or withdrawn validators, or attestations on another /// fork. - pub fn prune_attester_slashings(&self, head_state: &BeaconState) { + pub fn prune_attester_slashings(&self, finalized_state: &BeaconState) { self.attester_slashings.write().retain(|slashing| { // Check that the attestation's signature is still valid wrt the fork version. - let signature_ok = slashing.signature_is_still_valid(&head_state.fork()); + // We might be a bit slower to detect signature staleness by using the finalized state + // here, but we filter when proposing anyway, so in the worst case we just keep some + // stuff around until we finalize. + let signature_ok = slashing.signature_is_still_valid(&finalized_state.fork()); // Slashings that don't slash any validators can also be dropped. let slashing_ok = get_slashable_indices_modular( - head_state, + finalized_state, slashing.as_inner().to_ref(), |_, validator| { - // Declare that a validator is still slashable if they have not exited prior - // to the finalized epoch. - // - // We cannot check the `slashed` field since the `head` is not finalized and - // a fork could un-slash someone. - validator.exit_epoch > head_state.finalized_checkpoint().epoch + // Declare that a validator is still slashable if they have not been slashed in + // the finalized state, and have not exited at the finalized epoch. + !validator.slashed && validator.exit_epoch > finalized_state.current_epoch() }, ) .is_ok_and(|indices| !indices.is_empty()); @@ -530,17 +534,12 @@ impl OperationPool { ) } - /// Prune if validator has already exited at or before the finalized checkpoint of the head. - pub fn prune_voluntary_exits(&self, head_state: &BeaconState) { + /// Prune if validator has already exited in the finalized state. + pub fn prune_voluntary_exits(&self, finalized_state: &BeaconState, spec: &ChainSpec) { prune_validator_hash_map( &mut self.voluntary_exits.write(), - // This condition is slightly too loose, since there will be some finalized exits that - // are missed here. - // - // We choose simplicity over the gain of pruning more exits since they are small and - // should not be seen frequently. - |_, validator| validator.exit_epoch <= head_state.finalized_checkpoint().epoch, - head_state, + |_, validator| validator.exit_epoch != spec.far_future_epoch, + finalized_state, ); } @@ -612,7 +611,7 @@ impl OperationPool { |address_change| address_change.as_inner().clone(), usize::MAX, ); - changes.shuffle(&mut thread_rng()); + changes.shuffle(&mut rng()); changes } @@ -641,14 +640,15 @@ impl OperationPool { &self, head_block: &SignedBeaconBlock, head_state: &BeaconState, + finalized_state: &BeaconState, current_epoch: Epoch, spec: &ChainSpec, ) { self.prune_attestations(current_epoch); self.prune_sync_contributions(head_state.slot()); - self.prune_proposer_slashings(head_state); - self.prune_attester_slashings(head_state); - self.prune_voluntary_exits(head_state); + self.prune_proposer_slashings(finalized_state); + self.prune_attester_slashings(finalized_state); + self.prune_voluntary_exits(finalized_state, spec); self.prune_bls_to_execution_changes(head_block, head_state, spec); } @@ -700,8 +700,8 @@ impl OperationPool { pub fn get_all_proposer_slashings(&self) -> Vec { self.proposer_slashings .read() - .iter() - .map(|(_, slashing)| slashing.as_inner().clone()) + .values() + .map(|slashing| slashing.as_inner().clone()) .collect() } @@ -711,8 +711,8 @@ impl OperationPool { pub fn get_all_voluntary_exits(&self) -> Vec { self.voluntary_exits .read() - .iter() - .map(|(_, exit)| exit.as_inner().clone()) + .values() + .map(|exit| exit.as_inner().clone()) .collect() } @@ -757,14 +757,14 @@ where fn prune_validator_hash_map( map: &mut HashMap>, prune_if: F, - head_state: &BeaconState, + state: &BeaconState, ) where F: Fn(u64, &Validator) -> bool, T: VerifyOperation, { map.retain(|&validator_index, op| { - op.signature_is_still_valid(&head_state.fork()) - && head_state + op.signature_is_still_valid(&state.fork()) + && state .validators() .get(validator_index as usize) .is_none_or(|validator| !prune_if(validator_index, validator)) @@ -782,6 +782,7 @@ impl PartialEq for OperationPool { && *self.attester_slashings.read() == *other.attester_slashings.read() && *self.proposer_slashings.read() == *other.proposer_slashings.read() && *self.voluntary_exits.read() == *other.voluntary_exits.read() + && *self.bls_to_execution_changes.read() == *other.bls_to_execution_changes.read() } } @@ -790,11 +791,13 @@ mod release_tests { use super::attestation::earliest_attestation_validators; use super::*; use beacon_chain::test_utils::{ - test_spec, BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee, + BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee, test_spec, }; + use bls::Keypair; + use fixed_bytes::FixedBytesExtended; use maplit::hashset; use state_processing::epoch_cache::initialize_epoch_cache; - use state_processing::{common::get_attesting_indices_from_state, VerifyOperation}; + use state_processing::{VerifyOperation, common::get_attesting_indices_from_state}; use std::collections::BTreeSet; use std::sync::{Arc, LazyLock}; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; diff --git a/beacon_node/operation_pool/src/persistence.rs b/beacon_node/operation_pool/src/persistence.rs index 79509e5f6c..241b5fec53 100644 --- a/beacon_node/operation_pool/src/persistence.rs +++ b/beacon_node/operation_pool/src/persistence.rs @@ -1,9 +1,9 @@ +use crate::OpPoolError; +use crate::OperationPool; use crate::attestation_storage::AttestationMap; use crate::bls_to_execution_changes::{BlsToExecutionChanges, ReceivedPreCapella}; use crate::sync_aggregate_id::SyncAggregateId; -use crate::OpPoolError; -use crate::OperationPool; -use derivative::Derivative; +use educe::Educe; use parking_lot::RwLock; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -11,6 +11,7 @@ use state_processing::SigVerifiedOp; use std::collections::HashSet; use std::mem; use store::{DBColumn, Error as StoreError, StoreItem}; +use superstruct::superstruct; use types::attestation::AttestationOnDisk; use types::*; @@ -22,10 +23,7 @@ type PersistedSyncContributions = Vec<(SyncAggregateId, Vec PersistedOperationPool { let proposer_slashings = operation_pool .proposer_slashings .read() - .iter() - .map(|(_, slashing)| slashing.clone()) + .values() + .cloned() .collect(); let voluntary_exits = operation_pool .voluntary_exits .read() - .iter() - .map(|(_, exit)| exit.clone()) + .values() + .cloned() .collect(); let bls_to_execution_changes = operation_pool diff --git a/beacon_node/operation_pool/src/reward_cache.rs b/beacon_node/operation_pool/src/reward_cache.rs index adedcb5e39..1e3fc4cf2d 100644 --- a/beacon_node/operation_pool/src/reward_cache.rs +++ b/beacon_node/operation_pool/src/reward_cache.rs @@ -1,8 +1,7 @@ use crate::OpPoolError; use bitvec::vec::BitVec; -use types::{ - BeaconState, BeaconStateError, Epoch, EthSpec, FixedBytesExtended, Hash256, ParticipationFlags, -}; +use fixed_bytes::FixedBytesExtended; +use types::{BeaconState, BeaconStateError, Epoch, EthSpec, Hash256, ParticipationFlags}; #[derive(Debug, PartialEq, Eq, Clone)] struct Initialization { diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 7d086dcc32..e4c7c6ff1f 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1,7 +1,7 @@ use std::time::Duration; -use clap::{builder::ArgPredicate, crate_version, Arg, ArgAction, ArgGroup, Command}; -use clap_utils::{get_color_style, FLAG_HEADER}; +use clap::{Arg, ArgAction, ArgGroup, Command, builder::ArgPredicate, crate_version}; +use clap_utils::{FLAG_HEADER, get_color_style}; use strum::VariantNames; #[allow(clippy::large_stack_frames)] @@ -47,33 +47,47 @@ pub fn cli_app() -> Command { * Network parameters. */ .arg( - Arg::new("subscribe-all-data-column-subnets") - .long("subscribe-all-data-column-subnets") + Arg::new("supernode") + .long("supernode") + .alias("subscribe-all-data-column-subnets") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) - .help("Subscribe to all data column subnets and participate in data custody for \ - all columns. This will also advertise the beacon node as being long-lived \ - subscribed to all data column subnets. \ - NOTE: this is an experimental flag and may change any time without notice!") + .help("Run as a voluntary supernode. This node will subscribe to all data column \ + subnets, custody all data columns, and perform reconstruction and cross-seeding. \ + This requires significantly more bandwidth, storage, and computation requirements but \ + the node will have direct access to all blobs via the beacon API and it \ + helps network resilience by serving all data columns to syncing peers.") + .display_order(0) + ) + .arg( + Arg::new("semi-supernode") + .long("semi-supernode") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .conflicts_with("supernode") + .help("Run in minimal reconstruction mode. This node will subscribe to and custody \ + half of the data columns (enough for reconstruction), enabling efficient \ + data availability with lower bandwidth and storage requirements compared to \ + a supernode, while still supporting full blob reconstruction.") .display_order(0) - .hide(true) ) .arg( - // TODO(das): remove this before PeerDAS release Arg::new("malicious-withhold-count") .long("malicious-withhold-count") .action(ArgAction::Set) .help_heading(FLAG_HEADER) - .help("TESTING ONLY do not use this") + .help("TESTING ONLY: Withholds a subset of data columns during publishing. \ + Do not use in production. Requires the 'testing' feature to be enabled.") .hide(true) .display_order(0) ) .arg( - Arg::new("enable-sampling") - .long("enable-sampling") - .action(ArgAction::SetTrue) + Arg::new("advertise-false-custody-group-count") + .long("advertise-false-custody-group-count") + .action(ArgAction::Set) .help_heading(FLAG_HEADER) - .help("Enable peer sampling on data columns. Disabled by default.") + .help("TESTING ONLY: Advertises a false custody group count for testing PeerDAS. \ + Do not use in production. Requires the 'testing' feature to be enabled.") .hide(true) .display_order(0) ) @@ -235,7 +249,6 @@ pub fn cli_app() -> Command { .long("network-load") .value_name("INTEGER") .help("Lighthouse's network can be tuned for bandwidth/performance. Setting this to a high value, will increase the bandwidth lighthouse uses, increasing the likelihood of redundant information in exchange for faster communication. This can increase profit of validators marginally by receiving messages faster on the network. Lower values decrease bandwidth usage, but makes communication slower which can lead to validator performance reduction. Values are in the range [1,5].") - .default_value("3") .hide(true) .action(ArgAction::Set) .display_order(0) @@ -401,6 +414,16 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("complete-blob-backfill") + .long("complete-blob-backfill") + .help("Download all blobs back to the Deneb fork epoch. This will likely result in \ + the node banning most of its peers.") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + .hide(true) + ) .arg( Arg::new("enable-private-discovery") .long("enable-private-discovery") @@ -688,59 +711,6 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) - - /* - * Eth1 Integration - */ - .arg( - Arg::new("eth1") - .long("eth1") - .help("DEPRECATED") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("dummy-eth1") - .long("dummy-eth1") - .help("DEPRECATED") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with("eth1") - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("eth1-purge-cache") - .long("eth1-purge-cache") - .value_name("PURGE-CACHE") - .help("Purges the eth1 block and deposit caches") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new("eth1-blocks-per-log-query") - .long("eth1-blocks-per-log-query") - .value_name("BLOCKS") - .help("Specifies the number of blocks that a deposit log query should span. \ - This will reduce the size of responses from the Eth1 endpoint.") - .default_value("1000") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new("eth1-cache-follow-distance") - .long("eth1-cache-follow-distance") - .value_name("BLOCKS") - .help("Specifies the distance between the Eth1 chain head and the last block which \ - should be imported into the cache. Setting this value lower can help \ - compensate for irregular Proof-of-Work block times, but setting it too low \ - can make the node vulnerable to re-orgs.") - .action(ArgAction::Set) - .display_order(0) - ) .arg( Arg::new("slots-per-restore-point") .long("slots-per-restore-point") @@ -790,7 +760,7 @@ pub fn cli_app() -> Command { .long("block-cache-size") .value_name("SIZE") .help("Specifies how many blocks the database should cache in memory") - .default_value("5") + .default_value("0") .action(ArgAction::Set) .display_order(0) ) @@ -808,20 +778,32 @@ pub fn cli_app() -> Command { Arg::new("hdiff-buffer-cache-size") .long("hdiff-buffer-cache-size") .value_name("SIZE") - .help("Number of hierarchical diff (hdiff) buffers to cache in memory. Each buffer \ - is around the size of a BeaconState so you should be cautious about setting \ - this value too high. This flag is irrelevant for most nodes, which run with \ - state pruning enabled.") + .help("Number of cold hierarchical diff (hdiff) buffers to cache in memory. Each \ + buffer is around the size of a BeaconState so you should be cautious about \ + setting this value too high. This flag is irrelevant for most nodes, which \ + run with state pruning enabled.") .default_value("16") .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("hot-hdiff-buffer-cache-size") + .long("hot-hdiff-buffer-cache-size") + .value_name("SIZE") + .help("Number of hot hierarchical diff (hdiff) buffers to cache in memory. Each \ + buffer is around the size of a BeaconState so you should be cautious about \ + setting this value too high. Setting this value higher can reduce the time \ + taken to store new states on disk at the cost of higher memory usage.") + .default_value("1") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("state-cache-size") .long("state-cache-size") .value_name("STATE_CACHE_SIZE") .help("Specifies the size of the state cache") - .default_value("32") + .default_value("128") .action(ArgAction::Set) .display_order(0) ) @@ -917,6 +899,14 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("disable-get-blobs") + .long("disable-get-blobs") + .help("Disables the getBlobs optimisation to fetch blobs from the EL mempool") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) .arg( Arg::new("builder-header-timeout") .long("builder-header-timeout") @@ -1488,17 +1478,6 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) - .arg( - Arg::new("disable-deposit-contract-sync") - .long("disable-deposit-contract-sync") - .help("Explicitly disables syncing of deposit logs from the execution node. \ - This overrides any previous option that depends on it. \ - Useful if you intend to run a non-validating beacon node.") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with("staking") - .display_order(0) - ) .arg( Arg::new("disable-optimistic-finalized-sync") .long("disable-optimistic-finalized-sync") @@ -1509,15 +1488,6 @@ pub fn cli_app() -> Command { Lighthouse and only passed to the EL if initial verification fails.") .display_order(0) ) - .arg( - Arg::new("light-client-server") - .long("light-client-server") - .help("DEPRECATED") - .action(ArgAction::SetTrue) - - .help_heading(FLAG_HEADER) - .display_order(0) - ) .arg( Arg::new("disable-light-client-server") .long("disable-light-client-server") @@ -1636,22 +1606,22 @@ pub fn cli_app() -> Command { .value_name("SECONDS") .action(ArgAction::Set) .help_heading(FLAG_HEADER) - .help("TESTING ONLY: Artificially delay block publishing by the specified number of seconds. \ - This only works for if `BroadcastValidation::Gossip` is used (default). \ - DO NOT USE IN PRODUCTION.") + .help("TESTING ONLY: Artificially delays block publishing by the specified number of seconds. \ + This only works if BroadcastValidation::Gossip is used (default). \ + Do not use in production. Requires the 'testing' feature to be enabled.") .hide(true) .display_order(0) ) .arg( Arg::new("delay-data-column-publishing") .long("delay-data-column-publishing") - .value_name("SECONDS") + .value_name("SECONDS") .action(ArgAction::Set) .help_heading(FLAG_HEADER) - .help("TESTING ONLY: Artificially delay data column publishing by the specified number of seconds. \ - Limitation: If `delay-block-publishing` is also used, data columns will be delayed for a \ - minimum of `delay-block-publishing` seconds. - DO NOT USE IN PRODUCTION.") + .help("TESTING ONLY: Artificially delays data column publishing by the specified number of seconds. \ + Limitation: If delay-block-publishing is also used, data columns will be delayed for a \ + minimum of delay-block-publishing seconds. \ + Do not use in production. Requires the 'testing' feature to be enabled.") .hide(true) .display_order(0) ) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index e887aa9abc..26dd3b6642 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1,24 +1,23 @@ -use account_utils::{read_input_from_user, STDIN_INPUTS_FLAG}; +use account_utils::{STDIN_INPUTS_FLAG, read_input_from_user}; use beacon_chain::chain_config::{ - DisallowedReOrgOffsets, ReOrgThreshold, DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, - DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - DEFAULT_RE_ORG_PARENT_THRESHOLD, INVALID_HOLESKY_BLOCK_ROOT, + DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, + DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, + DisallowedReOrgOffsets, INVALID_HOLESKY_BLOCK_ROOT, ReOrgThreshold, }; +use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::graffiti_calculator::GraffitiOrigin; -use beacon_chain::TrustedSetup; -use clap::{parser::ValueSource, ArgMatches, Id}; +use bls::PublicKeyBytes; +use clap::{ArgMatches, Id, parser::ValueSource}; use clap_utils::flags::DISABLE_MALLOC_TUNING_FLAG; use clap_utils::{parse_flag, parse_required}; use client::{ClientConfig, ClientGenesis}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use environment::RuntimeContext; use execution_layer::DEFAULT_JWT_FILE; -use genesis::Eth1Endpoint; use http_api::TlsConfig; -use lighthouse_network::ListenAddress; -use lighthouse_network::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; +use lighthouse_network::{Enr, Multiaddr, NetworkConfig, PeerIdSerialized, multiaddr::Protocol}; +use network_utils::listen_addr::ListenAddress; use sensitive_url::SensitiveUrl; -use std::cmp::max; use std::collections::HashSet; use std::fmt::Debug; use std::fs; @@ -31,7 +30,7 @@ use std::str::FromStr; use std::time::Duration; use tracing::{error, info, warn}; use types::graffiti::GraffitiString; -use types::{Checkpoint, Epoch, EthSpec, Hash256, PublicKeyBytes}; +use types::{Checkpoint, Epoch, EthSpec, Hash256}; const PURGE_DB_CONFIRMATION: &str = "confirm"; @@ -111,6 +110,18 @@ pub fn get_config( set_network_config(&mut client_config.network, cli_args, &data_dir_ref)?; + // Parse custody mode from CLI flags + let is_supernode = parse_flag(cli_args, "supernode"); + let is_semi_supernode = parse_flag(cli_args, "semi-supernode"); + + client_config.chain.node_custody_type = if is_supernode { + NodeCustodyType::Supernode + } else if is_semi_supernode { + NodeCustodyType::SemiSupernode + } else { + NodeCustodyType::Fullnode + }; + /* * Staking flag * Note: the config values set here can be overwritten by other more specific cli params @@ -173,17 +184,14 @@ pub fn get_config( parse_required(cli_args, "http-duplicate-block-status")?; } - if cli_args.get_flag("light-client-server") { - warn!( - "The --light-client-server flag is deprecated. The light client server is enabled \ - by default" - ); - } - if cli_args.get_flag("disable-light-client-server") { client_config.chain.enable_light_client_server = false; } + if cli_args.get_flag("disable-get-blobs") { + client_config.chain.disable_get_blobs = true; + } + if let Some(sync_tolerance_epochs) = clap_utils::parse_optional(cli_args, "sync-tolerance-epochs")? { @@ -194,10 +202,6 @@ pub fn get_config( client_config.chain.shuffling_cache_size = cache_size; } - if cli_args.get_flag("enable-sampling") { - client_config.chain.enable_sampling = true; - } - if let Some(batches) = clap_utils::parse_optional(cli_args, "blob-publication-batches")? { client_config.chain.blob_publication_batches = batches; } @@ -265,34 +269,6 @@ pub fn get_config( client_config.http_metrics.allocator_metrics_enabled = false; } - /* - * Eth1 - */ - - if cli_args.get_flag("dummy-eth1") { - warn!("The --dummy-eth1 flag is deprecated"); - } - - if cli_args.get_flag("eth1") { - warn!("The --eth1 flag is deprecated"); - } - - if let Some(val) = cli_args.get_one::("eth1-blocks-per-log-query") { - client_config.eth1.blocks_per_log_query = val - .parse() - .map_err(|_| "eth1-blocks-per-log-query is not a valid integer".to_string())?; - } - - if cli_args.get_flag("eth1-purge-cache") { - client_config.eth1.purge_cache = true; - } - - if let Some(follow_distance) = - clap_utils::parse_optional(cli_args, "eth1-cache-follow-distance")? - { - client_config.eth1.cache_follow_distance = Some(follow_distance); - } - // `--execution-endpoint` is required now. let endpoints: String = clap_utils::parse_required(cli_args, "execution-endpoint")?; let mut el_config = execution_layer::Config::default(); @@ -358,35 +334,16 @@ pub fn get_config( clap_utils::parse_required(cli_args, "execution-timeout-multiplier")?; el_config.execution_timeout_multiplier = Some(execution_timeout_multiplier); - client_config.eth1.endpoint = Eth1Endpoint::Auth { - endpoint: execution_endpoint, - jwt_path: secret_file, - jwt_id: el_config.jwt_id.clone(), - jwt_version: el_config.jwt_version.clone(), - }; - // Store the EL config in the client config. client_config.execution_layer = Some(el_config); - // 4844 params - if let Some(trusted_setup) = context - .eth2_network_config - .as_ref() - .map(|config| serde_json::from_slice(&config.kzg_trusted_setup)) - .transpose() - .map_err(|e| format!("Unable to read trusted setup file: {}", e))? - { - client_config.trusted_setup = trusted_setup; - }; - // Override default trusted setup file if required if let Some(trusted_setup_file_path) = cli_args.get_one::("trusted-setup-file-override") { - let file = std::fs::File::open(trusted_setup_file_path) - .map_err(|e| format!("Failed to open trusted setup file: {}", e))?; - let trusted_setup: TrustedSetup = serde_json::from_reader(file) - .map_err(|e| format!("Unable to read trusted setup file: {}", e))?; - client_config.trusted_setup = trusted_setup; + client_config.trusted_setup = std::fs::read(trusted_setup_file_path) + .map_err(|e| format!("Failed to read trusted setup file: {}", e))?; + } else if let Some(eth2_network_config) = context.eth2_network_config.as_ref() { + client_config.trusted_setup = eth2_network_config.kzg_trusted_setup.clone(); } if let Some(freezer_dir) = cli_args.get_one::("freezer-dir") { @@ -418,7 +375,13 @@ pub fn get_config( if let Some(hdiff_buffer_cache_size) = clap_utils::parse_optional(cli_args, "hdiff-buffer-cache-size")? { - client_config.store.hdiff_buffer_cache_size = hdiff_buffer_cache_size; + client_config.store.cold_hdiff_buffer_cache_size = hdiff_buffer_cache_size; + } + + if let Some(hdiff_buffer_cache_size) = + clap_utils::parse_optional(cli_args, "hot-hdiff-buffer-cache-size")? + { + client_config.store.hot_hdiff_buffer_cache_size = hdiff_buffer_cache_size; } client_config.store.compact_on_init = cli_args.get_flag("compact-db"); @@ -472,6 +435,7 @@ pub fn get_config( client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; } + #[cfg(feature = "testing")] if let Some(malicious_withhold_count) = clap_utils::parse_optional(cli_args, "malicious-withhold-count")? { @@ -500,32 +464,22 @@ pub fn get_config( .as_ref() .ok_or("Context is missing eth2 network config")?; - client_config.eth1.deposit_contract_address = format!("{:?}", spec.deposit_contract_address); - client_config.eth1.deposit_contract_deploy_block = - eth2_network_config.deposit_contract_deploy_block; - client_config.eth1.lowest_cached_block_number = - client_config.eth1.deposit_contract_deploy_block; - client_config.eth1.follow_distance = spec.eth1_follow_distance; - client_config.eth1.node_far_behind_seconds = - max(5, spec.eth1_follow_distance / 2) * spec.seconds_per_eth1_block; - client_config.eth1.chain_id = spec.deposit_chain_id.into(); - client_config.eth1.set_block_cache_truncation::(spec); - info!( - deploy_block = client_config.eth1.deposit_contract_deploy_block, - address = &client_config.eth1.deposit_contract_address, + deploy_block = eth2_network_config.deposit_contract_deploy_block, + address = ?spec.deposit_contract_address, "Deposit contract" ); // Only append network config bootnodes if discovery is not disabled - if !client_config.network.disable_discovery { - if let Some(boot_nodes) = ð2_network_config.boot_enr { - client_config - .network - .boot_nodes_enr - .extend_from_slice(boot_nodes) - } + if !client_config.network.disable_discovery + && let Some(boot_nodes) = ð2_network_config.boot_enr + { + client_config + .network + .boot_nodes_enr + .extend_from_slice(boot_nodes) } + client_config.chain.checkpoint_sync_url_timeout = clap_utils::parse_required::(cli_args, "checkpoint-sync-url-timeout")?; @@ -809,11 +763,6 @@ pub fn get_config( } } - // Note: This overrides any previous flags that enable this option. - if cli_args.get_flag("disable-deposit-contract-sync") { - client_config.sync_eth1_chain = false; - } - client_config.chain.prepare_payload_lookahead = clap_utils::parse_optional(cli_args, "prepare-payload-lookahead")? .map(Duration::from_millis) @@ -862,6 +811,14 @@ pub fn get_config( client_config.chain.genesis_backfill = true; } + client_config.chain.complete_blob_backfill = cli_args.get_flag("complete-blob-backfill"); + + // Ensure `prune_blobs` is false whenever complete-blob-backfill is set. This overrides any + // setting of `--prune-blobs true` applied earlier in flag parsing. + if client_config.chain.complete_blob_backfill { + client_config.store.prune_blobs = false; + } + // Backfill sync rate-limiting client_config.beacon_processor.enable_backfill_rate_limiting = !cli_args.get_flag("disable-backfill-rate-limiting"); @@ -893,10 +850,12 @@ pub fn get_config( .max_gossip_aggregate_batch_size = clap_utils::parse_required(cli_args, "beacon-processor-aggregate-batch-size")?; + #[cfg(feature = "testing")] if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-block-publishing")? { client_config.chain.block_publishing_delay = Some(Duration::from_secs_f64(delay)); } + #[cfg(feature = "testing")] if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-data-column-publishing")? { client_config.chain.data_column_publishing_delay = Some(Duration::from_secs_f64(delay)); } @@ -953,18 +912,18 @@ pub fn parse_listening_addresses(cli_args: &ArgMatches) -> Result match &maybe_ipv4 { Some(first_ipv4_addr) => { return Err(format!( - "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ + "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ Got two IPv4 addresses {first_ipv4_addr} and {v4_addr}" - )); + )); } None => maybe_ipv4 = Some(v4_addr), }, IpAddr::V6(v6_addr) => match &maybe_ipv6 { Some(first_ipv6_addr) => { return Err(format!( - "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ + "When setting the --listen-address option twice, use an IPv4 address and an IPv6 address. \ Got two IPv6 addresses {first_ipv6_addr} and {v6_addr}" - )); + )); } None => maybe_ipv6 = Some(v6_addr), }, @@ -1037,7 +996,9 @@ pub fn parse_listening_addresses(cli_args: &ArgMatches) -> Result { // A single ipv6 address was provided. Set the ports if cli_args.value_source("port6") == Some(ValueSource::CommandLine) { - warn!("When listening only over IPv6, use the --port flag. The value of --port6 will be ignored."); + warn!( + "When listening only over IPv6, use the --port flag. The value of --port6 will be ignored." + ); } // If we are only listening on ipv6 and the user has specified --port6, lets just use @@ -1046,33 +1007,37 @@ pub fn parse_listening_addresses(cli_args: &ArgMatches) -> Result Result Result Result Result, _>>()?; if config.trusted_peers.len() >= config.target_peers { - warn!( target_peers = config.target_peers, trusted_peers = config.trusted_peers.len(),"More trusted peers than the target peer limit. This will prevent efficient peer selection criteria."); + warn!( + target_peers = config.target_peers, + trusted_peers = config.trusted_peers.len(), + "More trusted peers than the target peer limit. This will prevent efficient peer selection criteria." + ); } } @@ -1397,7 +1369,7 @@ pub fn set_network_config( let addr_str = format!("{addr}:{port}"); match addr_str.to_socket_addrs() { Err(_e) => { - return Err(format!("Failed to parse or resolve address {addr}.")) + return Err(format!("Failed to parse or resolve address {addr}.")); } Ok(resolved_addresses) => { for socket_addr in resolved_addresses { @@ -1488,10 +1460,6 @@ pub fn set_network_config( if parse_flag(cli_args, "proposer-only") { config.subscribe_all_subnets = false; - if cli_args.get_one::("target-peers").is_none() { - // If a custom value is not set, change the default to 15 - config.target_peers = 15; - } config.proposer_only = true; warn!( info = "Proposer-only mode enabled", diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 2275470bfc..148ae464cf 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -2,15 +2,15 @@ mod cli; mod config; pub use beacon_chain; -use beacon_chain::{ - builder::Witness, eth1_chain::CachingEth1Backend, slot_clock::SystemTimeSlotClock, -}; +use beacon_chain::{builder::Witness, slot_clock::SystemTimeSlotClock}; use clap::ArgMatches; pub use cli::cli_app; pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; pub use config::{get_config, get_data_dir, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; +use lighthouse_network::load_private_key; +use network_utils::enr_ext::peer_id_to_node_id; use slasher::{DatabaseBackendOverride, Slasher}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; @@ -19,15 +19,8 @@ use tracing::{info, warn}; use types::{ChainSpec, Epoch, EthSpec, ForkName}; /// A type-alias to the tighten the definition of a production-intended `Client`. -pub type ProductionClient = Client< - Witness< - SystemTimeSlotClock, - CachingEth1Backend, - E, - BeaconNodeBackend, - BeaconNodeBackend, - >, ->; +pub type ProductionClient = + Client, BeaconNodeBackend>>; /// The beacon node `Client` that will be used in production. /// @@ -129,25 +122,14 @@ impl ProductionBeaconNode { builder }; + // Generate or load the node id. + let local_keypair = load_private_key(&client_config.network); + let node_id = peer_id_to_node_id(&local_keypair.public().to_peer_id())?.raw(); + let builder = builder - .beacon_chain_builder(client_genesis, client_config.clone()) + .beacon_chain_builder(client_genesis, client_config.clone(), node_id) .await?; - let builder = if client_config.sync_eth1_chain { - info!( - endpoint = ?client_config.eth1.endpoint, - method = "json rpc via http", - "Block production enabled" - ); - builder - .caching_eth1_backend(client_config.eth1.clone()) - .await? - } else { - info!( - reason = "no eth1 backend configured", - "Block production disabled" - ); - builder.no_eth1_backend()? - }; + info!("Block production enabled"); let builder = builder.system_time_slot_clock()?; @@ -157,7 +139,7 @@ impl ProductionBeaconNode { builder .build_beacon_chain()? - .network(Arc::new(client_config.network)) + .network(Arc::new(client_config.network), local_keypair) .await? .notifier()? .http_metrics_config(client_config.http_metrics.clone()) @@ -230,6 +212,7 @@ mod test { spec.electra_fork_epoch = None; spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.gloas_fork_epoch = None; let result = validator_fork_epochs(&spec); assert_eq!( result, diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 908f0759a9..50028fe73f 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -9,37 +9,41 @@ default = ["leveldb"] leveldb = ["dep:leveldb"] redb = ["dep:redb"] -[dev-dependencies] -beacon_chain = { workspace = true } -criterion = { workspace = true } -rand = { workspace = true, features = ["small_rng"] } -tempfile = { workspace = true } - [dependencies] bls = { workspace = true } db-key = "0.0.5" directory = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } itertools = { workspace = true } leveldb = { version = "0.8.6", optional = true, default-features = false } logging = { workspace = true } lru = { workspace = true } metrics = { workspace = true } +milhouse = { workspace = true } parking_lot = { workspace = true } redb = { version = "2.1.3", optional = true } safe_arith = { workspace = true } serde = { workspace = true } smallvec = { workspace = true } +ssz_types = { workspace = true } state_processing = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +typenum = { workspace = true } types = { workspace = true } xdelta3 = { workspace = true } zstd = { workspace = true } +[dev-dependencies] +beacon_chain = { workspace = true } +criterion = { workspace = true } +rand = { workspace = true, features = ["small_rng"] } +tempfile = { workspace = true } + [[bench]] name = "hdiff" harness = false diff --git a/beacon_node/store/benches/hdiff.rs b/beacon_node/store/benches/hdiff.rs index 2577f03f66..1e295c18a1 100644 --- a/beacon_node/store/benches/hdiff.rs +++ b/beacon_node/store/benches/hdiff.rs @@ -1,10 +1,10 @@ use bls::PublicKeyBytes; -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use rand::Rng; use ssz::Decode; use store::{ - hdiff::{HDiff, HDiffBuffer}, StoreConfig, + hdiff::{HDiff, HDiffBuffer}, }; use types::{BeaconState, Epoch, Eth1Data, EthSpec, MainnetEthSpec as E, Validator}; @@ -12,7 +12,7 @@ pub fn all_benches(c: &mut Criterion) { let spec = E::default_spec(); let genesis_time = 0; let eth1_data = Eth1Data::default(); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let validator_mutations = 1000; let validator_additions = 100; @@ -27,11 +27,11 @@ pub fn all_benches(c: &mut Criterion) { // Change all balances for i in 0..n { let balance = target_state.balances_mut().get_mut(i).unwrap(); - *balance += rng.gen_range(1..=1_000_000); + *balance += rng.random_range(1..=1_000_000); } // And some validator records for _ in 0..validator_mutations { - let index = rng.gen_range(1..n); + let index = rng.random_range(1..n); // TODO: Only change a few things, and not the pubkey *target_state.validators_mut().get_mut(index).unwrap() = rand_validator(&mut rng); } @@ -80,7 +80,7 @@ fn bench_against_states( fn rand_validator(mut rng: impl Rng) -> Validator { let mut pubkey = [0u8; 48]; rng.fill_bytes(&mut pubkey); - let withdrawal_credentials: [u8; 32] = rng.gen(); + let withdrawal_credentials: [u8; 32] = rng.random(); Validator { pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), @@ -97,7 +97,7 @@ fn rand_validator(mut rng: impl Rng) -> Validator { fn append_validator(state: &mut BeaconState, mut rng: impl Rng) { state .balances_mut() - .push(32_000_000_000 + rng.gen_range(1..=1_000_000_000)) + .push(32_000_000_000 + rng.random_range(1..=1_000_000_000)) .unwrap(); if let Ok(inactivity_scores) = state.inactivity_scores_mut() { inactivity_scores.push(0).unwrap(); diff --git a/beacon_node/store/src/chunked_iter.rs b/beacon_node/store/src/chunked_iter.rs deleted file mode 100644 index f2821286ec..0000000000 --- a/beacon_node/store/src/chunked_iter.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::chunked_vector::{chunk_key, Chunk, Field}; -use crate::{HotColdDB, ItemStore}; -use tracing::error; -use types::{ChainSpec, EthSpec, Slot}; - -/// Iterator over the values of a `BeaconState` vector field (like `block_roots`). -/// -/// Uses the freezer DB's separate table to load the values. -pub struct ChunkedVectorIter<'a, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - pub(crate) store: &'a HotColdDB, - current_vindex: usize, - pub(crate) end_vindex: usize, - next_cindex: usize, - current_chunk: Chunk, -} - -impl<'a, F, E, Hot, Cold> ChunkedVectorIter<'a, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - /// Create a new iterator which can yield elements from `start_vindex` up to the last - /// index stored by the restore point at `last_restore_point_slot`. - /// - /// The `freezer_upper_limit` slot should be the slot of a recent restore point as obtained from - /// `Root::freezer_upper_limit`. We pass it as a parameter so that the caller can - /// maintain a stable view of the database (see `HybridForwardsBlockRootsIterator`). - pub fn new( - store: &'a HotColdDB, - start_vindex: usize, - freezer_upper_limit: Slot, - spec: &ChainSpec, - ) -> Self { - let (_, end_vindex) = F::start_and_end_vindex(freezer_upper_limit, spec); - - // Set the next chunk to the one containing `start_vindex`. - let next_cindex = start_vindex / F::chunk_size(); - // Set the current chunk to the empty chunk, it will never be read. - let current_chunk = Chunk::default(); - - Self { - store, - current_vindex: start_vindex, - end_vindex, - next_cindex, - current_chunk, - } - } -} - -impl Iterator for ChunkedVectorIter<'_, F, E, Hot, Cold> -where - F: Field, - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, -{ - type Item = (usize, F::Value); - - fn next(&mut self) -> Option { - let chunk_size = F::chunk_size(); - - // Range exhausted, return `None` forever. - if self.current_vindex >= self.end_vindex { - None - } - // Value lies in the current chunk, return it. - else if self.current_vindex < self.next_cindex * chunk_size { - let vindex = self.current_vindex; - let val = self - .current_chunk - .values - .get(vindex % chunk_size) - .cloned() - .or_else(|| { - error!( - vector_index = vindex, - "Missing chunk value in forwards iterator" - ); - None - })?; - self.current_vindex += 1; - Some((vindex, val)) - } - // Need to load the next chunk, load it and recurse back into the in-range case. - else { - self.current_chunk = Chunk::load( - &self.store.cold_db, - F::column(), - &chunk_key(self.next_cindex), - ) - .map_err(|e| { - error!( - chunk_index = self.next_cindex, - error = ?e, - "Database error in forwards iterator" - ); - e - }) - .ok()? - .or_else(|| { - error!( - chunk_index = self.next_cindex, - "Missing chunk in forwards iterator" - ); - None - })?; - self.next_cindex += 1; - self.next() - } - } -} diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs deleted file mode 100644 index 90e8c17310..0000000000 --- a/beacon_node/store/src/chunked_vector.rs +++ /dev/null @@ -1,919 +0,0 @@ -//! Space-efficient storage for `BeaconState` vector fields. -//! -//! This module provides logic for splitting the `Vector` fields of a `BeaconState` into -//! chunks, and storing those chunks in contiguous ranges in the on-disk database. The motiviation -//! for doing this is avoiding massive duplication in every on-disk state. For example, rather than -//! storing the whole `historical_roots` vector, which is updated once every couple of thousand -//! slots, at every slot, we instead store all the historical values as a chunked vector on-disk, -//! and fetch only the slice we need when reconstructing the `historical_roots` of a state. -//! -//! ## Terminology -//! -//! * **Chunk size**: the number of vector values stored per on-disk chunk. -//! * **Vector index** (vindex): index into all the historical values, identifying a single element -//! of the vector being stored. -//! * **Chunk index** (cindex): index into the keyspace of the on-disk database, identifying a chunk -//! of elements. To find the chunk index of a vector index: `cindex = vindex / chunk_size`. -use self::UpdatePattern::*; -use crate::*; -use ssz::{Decode, Encode}; -use types::historical_summary::HistoricalSummary; - -/// Description of how a `BeaconState` field is updated during state processing. -/// -/// When storing a state, this allows us to efficiently store only those entries -/// which are not present in the DB already. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdatePattern { - /// The value is updated once per `n` slots. - OncePerNSlots { - n: u64, - /// The slot at which the field begins to accumulate values. - /// - /// The field should not be read or written until `activation_slot` is reached, and the - /// activation slot should act as an offset when converting slots to vector indices. - activation_slot: Option, - /// The slot at which the field ceases to accumulate values. - /// - /// If this is `None` then the field is continually updated. - deactivation_slot: Option, - }, - /// The value is updated once per epoch, for the epoch `current_epoch - lag`. - OncePerEpoch { lag: u64 }, -} - -/// Map a chunk index to bytes that can be used to key the NoSQL database. -/// -/// We shift chunks up by 1 to make room for a genesis chunk that is handled separately. -pub fn chunk_key(cindex: usize) -> [u8; 8] { - (cindex as u64 + 1).to_be_bytes() -} - -/// Return the database key for the genesis value. -fn genesis_value_key() -> [u8; 8] { - 0u64.to_be_bytes() -} - -/// Trait for types representing fields of the `BeaconState`. -/// -/// All of the required methods are type-level, because we do most things with fields at the -/// type-level. We require their value-level witnesses to be `Copy` so that we can avoid the -/// turbofish when calling functions like `store_updated_vector`. -pub trait Field: Copy { - /// The type of value stored in this field: the `T` from `Vector`. - /// - /// The `Default` impl will be used to fill extra vector entries. - type Value: Default + std::fmt::Debug + milhouse::Value; - // Decode + Encode + Default + Clone + PartialEq + std::fmt::Debug - - /// The length of this field: the `N` from `Vector`. - type Length: Unsigned; - - /// The database column where the integer-indexed chunks for this field should be stored. - /// - /// Each field's column **must** be unique. - fn column() -> DBColumn; - - /// Update pattern for this field, so that we can do differential updates. - fn update_pattern(spec: &ChainSpec) -> UpdatePattern; - - /// The number of values to store per chunk on disk. - /// - /// Default is 128 so that we read/write 4K pages when the values are 32 bytes. - // TODO: benchmark and optimise this parameter - fn chunk_size() -> usize { - 128 - } - - /// Convert a v-index (vector index) to a chunk index. - fn chunk_index(vindex: usize) -> usize { - vindex / Self::chunk_size() - } - - /// Get the value of this field at the given vector index, from the state. - fn get_value( - state: &BeaconState, - vindex: u64, - spec: &ChainSpec, - ) -> Result; - - /// True if this is a `FixedLengthField`, false otherwise. - fn is_fixed_length() -> bool; - - /// Compute the start and end vector indices of the slice of history required at `current_slot`. - /// - /// ## Example - /// - /// If we have a field that is updated once per epoch, then the end vindex will be - /// `current_epoch + 1`, because we want to include the value for the current epoch, and the - /// start vindex will be `end_vindex - Self::Length`, because that's how far back we can look. - fn start_and_end_vindex(current_slot: Slot, spec: &ChainSpec) -> (usize, usize) { - // We take advantage of saturating subtraction on slots and epochs - match Self::update_pattern(spec) { - OncePerNSlots { - n, - activation_slot, - deactivation_slot, - } => { - // Per-slot changes exclude the index for the current slot, because - // it won't be set until the slot completes (think of `state_roots`, `block_roots`). - // This also works for the `historical_roots` because at the `n`th slot, the 0th - // entry of the list is created, and before that the list is empty. - // - // To account for the switch from historical roots to historical summaries at - // Capella we also modify the current slot by the activation and deactivation slots. - // The activation slot acts as an offset (subtraction) while the deactivation slot - // acts as a clamp (min). - let slot_with_clamp = deactivation_slot.map_or(current_slot, |deactivation_slot| { - std::cmp::min(current_slot, deactivation_slot) - }); - let slot_with_clamp_and_offset = if let Some(activation_slot) = activation_slot { - slot_with_clamp - activation_slot - } else { - // Return (0, 0) to indicate that the field should not be read/written. - return (0, 0); - }; - let end_vindex = slot_with_clamp_and_offset / n; - let start_vindex = end_vindex - Self::Length::to_u64(); - (start_vindex.as_usize(), end_vindex.as_usize()) - } - OncePerEpoch { lag } => { - // Per-epoch changes include the index for the current epoch, because it - // will have been set at the most recent epoch boundary. - let current_epoch = current_slot.epoch(E::slots_per_epoch()); - let end_epoch = current_epoch + 1 - lag; - let start_epoch = end_epoch + lag - Self::Length::to_u64(); - (start_epoch.as_usize(), end_epoch.as_usize()) - } - } - } - - /// Given an `existing_chunk` stored in the DB, construct an updated chunk to replace it. - fn get_updated_chunk( - existing_chunk: &Chunk, - chunk_index: usize, - start_vindex: usize, - end_vindex: usize, - state: &BeaconState, - spec: &ChainSpec, - ) -> Result, Error> { - let chunk_size = Self::chunk_size(); - let mut new_chunk = Chunk::new(vec![Self::Value::default(); chunk_size]); - - for i in 0..chunk_size { - let vindex = chunk_index * chunk_size + i; - if vindex >= start_vindex && vindex < end_vindex { - let vector_value = Self::get_value(state, vindex as u64, spec)?; - - if let Some(existing_value) = existing_chunk.values.get(i) { - if *existing_value != vector_value && *existing_value != Self::Value::default() - { - return Err(ChunkError::Inconsistent { - field: Self::column(), - chunk_index, - existing_value: format!("{:?}", existing_value), - new_value: format!("{:?}", vector_value), - } - .into()); - } - } - - new_chunk.values[i] = vector_value; - } else { - new_chunk.values[i] = existing_chunk.values.get(i).cloned().unwrap_or_default(); - } - } - - Ok(new_chunk) - } - - /// Determine whether a state at `slot` possesses (or requires) the genesis value. - fn slot_needs_genesis_value(slot: Slot, spec: &ChainSpec) -> bool { - let (_, end_vindex) = Self::start_and_end_vindex(slot, spec); - match Self::update_pattern(spec) { - // If the end_vindex is less than the length of the vector, then the vector - // has not yet been completely filled with non-genesis values, and so the genesis - // value is still required. - OncePerNSlots { .. } => { - Self::is_fixed_length() && end_vindex < Self::Length::to_usize() - } - // If the field has lag, then it takes an extra `lag` vindices beyond the - // `end_vindex` before the vector has been filled with non-genesis values. - OncePerEpoch { lag } => { - Self::is_fixed_length() && end_vindex + (lag as usize) < Self::Length::to_usize() - } - } - } - - /// Load the genesis value for a fixed length field from the store. - /// - /// This genesis value should be used to fill the initial state of the vector. - fn load_genesis_value>(store: &S) -> Result { - let key = &genesis_value_key()[..]; - let chunk = - Chunk::load(store, Self::column(), key)?.ok_or(ChunkError::MissingGenesisValue)?; - chunk - .values - .first() - .cloned() - .ok_or_else(|| ChunkError::MissingGenesisValue.into()) - } - - /// Store the given `value` as the genesis value for this field, unless stored already. - /// - /// Check the existing value (if any) for consistency with the value we intend to store, and - /// return an error if they are inconsistent. - fn check_and_store_genesis_value>( - store: &S, - value: Self::Value, - ops: &mut Vec, - ) -> Result<(), Error> { - let key = &genesis_value_key()[..]; - - if let Some(existing_chunk) = Chunk::::load(store, Self::column(), key)? { - if existing_chunk.values.len() != 1 { - Err(ChunkError::InvalidGenesisChunk { - field: Self::column(), - expected_len: 1, - observed_len: existing_chunk.values.len(), - } - .into()) - } else if existing_chunk.values[0] != value { - Err(ChunkError::InconsistentGenesisValue { - field: Self::column(), - existing_value: format!("{:?}", existing_chunk.values[0]), - new_value: format!("{:?}", value), - } - .into()) - } else { - Ok(()) - } - } else { - let chunk = Chunk::new(vec![value]); - chunk.store(Self::column(), &genesis_value_key()[..], ops)?; - Ok(()) - } - } - - /// Extract the genesis value for a fixed length field from an - /// - /// Will only return a correct value if `slot_needs_genesis_value(state.slot(), spec) == true`. - fn extract_genesis_value( - state: &BeaconState, - spec: &ChainSpec, - ) -> Result { - let (_, end_vindex) = Self::start_and_end_vindex(state.slot(), spec); - match Self::update_pattern(spec) { - // Genesis value is guaranteed to exist at `end_vindex`, as it won't yet have been - // updated - OncePerNSlots { .. } => Ok(Self::get_value(state, end_vindex as u64, spec)?), - // If there's lag, the value of the field at the vindex *without the lag* - // should still be set to the genesis value. - OncePerEpoch { lag } => Ok(Self::get_value(state, end_vindex as u64 + lag, spec)?), - } - } -} - -/// Marker trait for fixed-length fields (`Vector`). -pub trait FixedLengthField: Field {} - -/// Marker trait for variable-length fields (`List`). -pub trait VariableLengthField: Field {} - -/// Macro to implement the `Field` trait on a new unit struct type. -macro_rules! field { - ($struct_name:ident, $marker_trait:ident, $value_ty:ty, $length_ty:ty, $column:expr, - $update_pattern:expr, $get_value:expr) => { - #[derive(Clone, Copy)] - pub struct $struct_name; - - impl Field for $struct_name - where - E: EthSpec, - { - type Value = $value_ty; - type Length = $length_ty; - - fn column() -> DBColumn { - $column - } - - fn update_pattern(spec: &ChainSpec) -> UpdatePattern { - let update_pattern = $update_pattern; - update_pattern(spec) - } - - fn get_value( - state: &BeaconState, - vindex: u64, - spec: &ChainSpec, - ) -> Result { - let get_value = $get_value; - get_value(state, vindex, spec) - } - - fn is_fixed_length() -> bool { - stringify!($marker_trait) == "FixedLengthField" - } - } - - impl $marker_trait for $struct_name {} - }; -} - -field!( - BlockRootsChunked, - FixedLengthField, - Hash256, - E::SlotsPerHistoricalRoot, - DBColumn::BeaconBlockRootsChunked, - |_| OncePerNSlots { - n: 1, - activation_slot: Some(Slot::new(0)), - deactivation_slot: None - }, - |state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.block_roots(), index) -); - -field!( - StateRootsChunked, - FixedLengthField, - Hash256, - E::SlotsPerHistoricalRoot, - DBColumn::BeaconStateRootsChunked, - |_| OncePerNSlots { - n: 1, - activation_slot: Some(Slot::new(0)), - deactivation_slot: None, - }, - |state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.state_roots(), index) -); - -field!( - HistoricalRoots, - VariableLengthField, - Hash256, - E::HistoricalRootsLimit, - DBColumn::BeaconHistoricalRoots, - |spec: &ChainSpec| OncePerNSlots { - n: E::SlotsPerHistoricalRoot::to_u64(), - activation_slot: Some(Slot::new(0)), - deactivation_slot: spec - .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), - }, - |state: &BeaconState<_>, index, _| safe_modulo_list_index(state.historical_roots(), index) -); - -field!( - RandaoMixes, - FixedLengthField, - Hash256, - E::EpochsPerHistoricalVector, - DBColumn::BeaconRandaoMixes, - |_| OncePerEpoch { lag: 1 }, - |state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.randao_mixes(), index) -); - -field!( - HistoricalSummaries, - VariableLengthField, - HistoricalSummary, - E::HistoricalRootsLimit, - DBColumn::BeaconHistoricalSummaries, - |spec: &ChainSpec| OncePerNSlots { - n: E::SlotsPerHistoricalRoot::to_u64(), - activation_slot: spec - .capella_fork_epoch - .map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())), - deactivation_slot: None, - }, - |state: &BeaconState<_>, index, _| safe_modulo_list_index( - state - .historical_summaries() - .map_err(|_| ChunkError::InvalidFork)?, - index - ) -); - -pub fn store_updated_vector, E: EthSpec, S: KeyValueStore>( - field: F, - store: &S, - state: &BeaconState, - spec: &ChainSpec, - ops: &mut Vec, -) -> Result<(), Error> { - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(state.slot(), spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - // Store the genesis value if we have access to it, and it hasn't been stored already. - if F::slot_needs_genesis_value(state.slot(), spec) { - let genesis_value = F::extract_genesis_value(state, spec)?; - F::check_and_store_genesis_value(store, genesis_value, ops)?; - } - - // Start by iterating backwards from the last chunk, storing new chunks in the database. - // Stop once a chunk in the database matches what we were about to store, this indicates - // that a previously stored state has already filled-in a portion of the indices covered. - let full_range_checked = store_range( - field, - (start_cindex..=end_cindex).rev(), - start_vindex, - end_vindex, - store, - state, - spec, - ops, - )?; - - // If the previous `store_range` did not check the entire range, it may be the case that the - // state's vector includes elements at low vector indices that are not yet stored in the - // database, so run another `store_range` to ensure these values are also stored. - if !full_range_checked { - store_range( - field, - start_cindex..end_cindex, - start_vindex, - end_vindex, - store, - state, - spec, - ops, - )?; - } - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn store_range( - _: F, - range: I, - start_vindex: usize, - end_vindex: usize, - store: &S, - state: &BeaconState, - spec: &ChainSpec, - ops: &mut Vec, -) -> Result -where - F: Field, - E: EthSpec, - S: KeyValueStore, - I: Iterator, -{ - for chunk_index in range { - let chunk_key = &chunk_key(chunk_index)[..]; - - let existing_chunk = - Chunk::::load(store, F::column(), chunk_key)?.unwrap_or_default(); - - let new_chunk = F::get_updated_chunk( - &existing_chunk, - chunk_index, - start_vindex, - end_vindex, - state, - spec, - )?; - - if new_chunk == existing_chunk { - return Ok(false); - } - - new_chunk.store(F::column(), chunk_key, ops)?; - } - - Ok(true) -} - -// Chunks at the end index are included. -// TODO: could be more efficient with a real range query (perhaps RocksDB) -fn range_query, E: EthSpec, T: Decode + Encode>( - store: &S, - column: DBColumn, - start_index: usize, - end_index: usize, -) -> Result>, Error> { - let range = start_index..=end_index; - let len = range - .end() - // Add one to account for inclusive range. - .saturating_add(1) - .saturating_sub(*range.start()); - let mut result = Vec::with_capacity(len); - - for chunk_index in range { - let key = &chunk_key(chunk_index)[..]; - let chunk = Chunk::load(store, column, key)?.ok_or(ChunkError::Missing { chunk_index })?; - result.push(chunk); - } - - Ok(result) -} - -/// Combine chunks to form a list or vector of all values with vindex in `start_vindex..end_vindex`. -/// -/// The `length` parameter is the length of the vec to construct, with entries set to `default` if -/// they lie outside the vindex range. -fn stitch( - chunks: Vec>, - start_vindex: usize, - end_vindex: usize, - chunk_size: usize, - length: usize, - default: T, -) -> Result, ChunkError> { - if start_vindex + length < end_vindex { - return Err(ChunkError::OversizedRange { - start_vindex, - end_vindex, - length, - }); - } - - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let mut result = vec![default; length]; - - for (chunk_index, chunk) in (start_cindex..=end_cindex).zip(chunks.into_iter()) { - // All chunks but the last chunk must be full-sized - if chunk_index != end_cindex && chunk.values.len() != chunk_size { - return Err(ChunkError::InvalidSize { - chunk_index, - expected: chunk_size, - actual: chunk.values.len(), - }); - } - - // Copy the chunk entries into the result vector - for (i, value) in chunk.values.into_iter().enumerate() { - let vindex = chunk_index * chunk_size + i; - - if vindex >= start_vindex && vindex < end_vindex { - result[vindex % length] = value; - } - } - } - - Ok(result) -} - -pub fn load_vector_from_db, E: EthSpec, S: KeyValueStore>( - store: &S, - slot: Slot, - spec: &ChainSpec, -) -> Result, Error> { - // Do a range query - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let chunks = range_query(store, F::column(), start_cindex, end_cindex)?; - - let default = if F::slot_needs_genesis_value(slot, spec) { - F::load_genesis_value(store)? - } else { - F::Value::default() - }; - - let result = stitch( - chunks, - start_vindex, - end_vindex, - chunk_size, - F::Length::to_usize(), - default, - )?; - - Ok(Vector::new(result).map_err(ChunkError::Milhouse)?) -} - -/// The historical roots are stored in vector chunks, despite not actually being a vector. -pub fn load_variable_list_from_db, E: EthSpec, S: KeyValueStore>( - store: &S, - slot: Slot, - spec: &ChainSpec, -) -> Result, Error> { - let chunk_size = F::chunk_size(); - let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec); - let start_cindex = start_vindex / chunk_size; - let end_cindex = end_vindex / chunk_size; - - let chunks: Vec> = range_query(store, F::column(), start_cindex, end_cindex)?; - - let mut result = Vec::with_capacity(chunk_size * chunks.len()); - - for (chunk_index, chunk) in chunks.into_iter().enumerate() { - for (i, value) in chunk.values.into_iter().enumerate() { - let vindex = chunk_index * chunk_size + i; - - if vindex >= start_vindex && vindex < end_vindex { - result.push(value); - } - } - } - - Ok(List::new(result).map_err(ChunkError::Milhouse)?) -} - -/// Index into a `List` field of the state, avoiding out of bounds and division by 0. -fn safe_modulo_list_index( - values: &List, - index: u64, -) -> Result { - if values.is_empty() { - Err(ChunkError::ZeroLengthList) - } else { - values - .get(index as usize % values.len()) - .copied() - .ok_or(ChunkError::IndexOutOfBounds { index }) - } -} - -fn safe_modulo_vector_index( - values: &Vector, - index: u64, -) -> Result { - if values.is_empty() { - Err(ChunkError::ZeroLengthVector) - } else { - values - .get(index as usize % values.len()) - .copied() - .ok_or(ChunkError::IndexOutOfBounds { index }) - } -} - -/// A chunk of a fixed-size vector from the `BeaconState`, stored in the database. -#[derive(Debug, Clone, PartialEq)] -pub struct Chunk { - /// A vector of up-to `chunk_size` values. - pub values: Vec, -} - -impl Default for Chunk -where - T: Decode + Encode, -{ - fn default() -> Self { - Chunk { values: vec![] } - } -} - -impl Chunk -where - T: Decode + Encode, -{ - pub fn new(values: Vec) -> Self { - Chunk { values } - } - - pub fn load, E: EthSpec>( - store: &S, - column: DBColumn, - key: &[u8], - ) -> Result, Error> { - store - .get_bytes(column, key)? - .map(|bytes| Self::decode(&bytes)) - .transpose() - } - - pub fn store( - &self, - column: DBColumn, - key: &[u8], - ops: &mut Vec, - ) -> Result<(), Error> { - ops.push(KeyValueStoreOp::PutKeyValue( - column, - key.to_vec(), - self.encode()?, - )); - Ok(()) - } - - /// Attempt to decode a single chunk. - pub fn decode(bytes: &[u8]) -> Result { - if !::is_ssz_fixed_len() { - return Err(Error::from(ChunkError::InvalidType)); - } - - let value_size = ::ssz_fixed_len(); - - if value_size == 0 { - return Err(Error::from(ChunkError::InvalidType)); - } - - let values = bytes - .chunks(value_size) - .map(T::from_ssz_bytes) - .collect::>()?; - - Ok(Chunk { values }) - } - - pub fn encoded_size(&self) -> usize { - self.values.len() * ::ssz_fixed_len() - } - - /// Encode a single chunk as bytes. - pub fn encode(&self) -> Result, Error> { - if !::is_ssz_fixed_len() { - return Err(Error::from(ChunkError::InvalidType)); - } - - Ok(self.values.iter().flat_map(T::as_ssz_bytes).collect()) - } -} - -#[derive(Debug, PartialEq)] -pub enum ChunkError { - ZeroLengthVector, - ZeroLengthList, - IndexOutOfBounds { - index: u64, - }, - InvalidSize { - chunk_index: usize, - expected: usize, - actual: usize, - }, - Missing { - chunk_index: usize, - }, - MissingGenesisValue, - Inconsistent { - field: DBColumn, - chunk_index: usize, - existing_value: String, - new_value: String, - }, - InconsistentGenesisValue { - field: DBColumn, - existing_value: String, - new_value: String, - }, - InvalidGenesisChunk { - field: DBColumn, - expected_len: usize, - observed_len: usize, - }, - InvalidType, - OversizedRange { - start_vindex: usize, - end_vindex: usize, - length: usize, - }, - InvalidFork, - Milhouse(milhouse::Error), -} - -impl From for ChunkError { - fn from(e: milhouse::Error) -> ChunkError { - Self::Milhouse(e) - } -} - -#[cfg(test)] -mod test { - use super::*; - use types::MainnetEthSpec as TestSpec; - use types::*; - - fn v(i: u64) -> Hash256 { - Hash256::from_low_u64_be(i) - } - - #[test] - fn stitch_default() { - let chunk_size = 4; - - let chunks = vec![ - Chunk::new(vec![0u64, 1, 2, 3]), - Chunk::new(vec![4, 5, 0, 0]), - ]; - - assert_eq!( - stitch(chunks, 2, 6, chunk_size, 12, 99).unwrap(), - vec![99, 99, 2, 3, 4, 5, 99, 99, 99, 99, 99, 99] - ); - } - - #[test] - fn stitch_basic() { - let chunk_size = 4; - let default = v(0); - - let chunks = vec![ - Chunk::new(vec![v(0), v(1), v(2), v(3)]), - Chunk::new(vec![v(4), v(5), v(6), v(7)]), - Chunk::new(vec![v(8), v(9), v(10), v(11)]), - ]; - - assert_eq!( - stitch(chunks.clone(), 0, 12, chunk_size, 12, default).unwrap(), - (0..12).map(v).collect::>() - ); - - assert_eq!( - stitch(chunks, 2, 10, chunk_size, 8, default).unwrap(), - vec![v(8), v(9), v(2), v(3), v(4), v(5), v(6), v(7)] - ); - } - - #[test] - fn stitch_oversized_range() { - let chunk_size = 4; - let default = 0; - - let chunks = vec![Chunk::new(vec![20u64, 21, 22, 23])]; - - // Args (start_vindex, end_vindex, length) - let args = vec![(0, 21, 20), (0, 2048, 1024), (0, 2, 1)]; - - for (start_vindex, end_vindex, length) in args { - assert_eq!( - stitch( - chunks.clone(), - start_vindex, - end_vindex, - chunk_size, - length, - default - ), - Err(ChunkError::OversizedRange { - start_vindex, - end_vindex, - length, - }) - ); - } - } - - #[test] - fn fixed_length_fields() { - fn test_fixed_length>(_: F, expected: bool) { - assert_eq!(F::is_fixed_length(), expected); - } - test_fixed_length(BlockRootsChunked, true); - test_fixed_length(StateRootsChunked, true); - test_fixed_length(HistoricalRoots, false); - test_fixed_length(RandaoMixes, true); - } - - fn needs_genesis_value_once_per_slot>(_: F) { - let spec = &TestSpec::default_spec(); - let max = F::Length::to_u64(); - for i in 0..max { - assert!( - F::slot_needs_genesis_value(Slot::new(i), spec), - "slot {}", - i - ); - } - assert!(!F::slot_needs_genesis_value(Slot::new(max), spec)); - } - - #[test] - fn needs_genesis_value_block_roots() { - needs_genesis_value_once_per_slot(BlockRootsChunked); - } - - #[test] - fn needs_genesis_value_state_roots() { - needs_genesis_value_once_per_slot(StateRootsChunked); - } - - #[test] - fn needs_genesis_value_historical_roots() { - let spec = &TestSpec::default_spec(); - assert!( - !>::slot_needs_genesis_value(Slot::new(0), spec) - ); - } - - fn needs_genesis_value_test_randao>(_: F) { - let spec = &TestSpec::default_spec(); - let max = TestSpec::slots_per_epoch() * (F::Length::to_u64() - 1); - for i in 0..max { - assert!( - F::slot_needs_genesis_value(Slot::new(i), spec), - "slot {}", - i - ); - } - assert!(!F::slot_needs_genesis_value(Slot::new(max), spec)); - } - - #[test] - fn needs_genesis_value_randao() { - needs_genesis_value_test_randao(RandaoMixes); - } -} diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index a84573eb40..0aa00e659b 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,15 +1,15 @@ use crate::hdiff::HierarchyConfig; -use crate::superstruct; -use crate::{AnchorInfo, DBColumn, Error, Split, StoreItem}; +use crate::{DBColumn, Error, StoreItem}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; -use std::io::Write; +use std::io::{Read, Write}; use std::num::NonZeroUsize; -use strum::{Display, EnumString, EnumVariantNames}; -use types::non_zero_usize::new_non_zero_usize; +use strum::{Display, EnumString, VariantNames}; +use superstruct::superstruct; use types::EthSpec; -use zstd::Encoder; +use types::non_zero_usize::new_non_zero_usize; +use zstd::{Decoder, Encoder}; #[cfg(all(feature = "redb", not(feature = "leveldb")))] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Redb; @@ -19,12 +19,13 @@ pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::LevelDb; pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048; pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192; pub const DEFAULT_EPOCHS_PER_STATE_DIFF: u64 = 8; -pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(64); +pub const DEFAULT_BLOCK_CACHE_SIZE: usize = 0; pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); pub const DEFAULT_STATE_CACHE_HEADROOM: NonZeroUsize = new_non_zero_usize(1); pub const DEFAULT_COMPRESSION_LEVEL: i32 = 1; pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1); -pub const DEFAULT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); +pub const DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); +pub const DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1); const EST_COMPRESSION_FACTOR: usize = 2; pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1; pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; @@ -33,7 +34,7 @@ pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StoreConfig { /// Maximum number of blocks to store in the in-memory block cache. - pub block_cache_size: NonZeroUsize, + pub block_cache_size: usize, /// Maximum number of states to store in the in-memory state cache. pub state_cache_size: NonZeroUsize, /// Minimum number of states to cull from the state cache upon fullness. @@ -42,8 +43,10 @@ pub struct StoreConfig { pub compression_level: i32, /// Maximum number of historic states to store in the in-memory historic state cache. pub historic_state_cache_size: NonZeroUsize, - /// Maximum number of `HDiffBuffer`s to store in memory. - pub hdiff_buffer_cache_size: NonZeroUsize, + /// Maximum number of cold `HDiffBuffer`s to store in memory. + pub cold_hdiff_buffer_cache_size: NonZeroUsize, + /// Maximum number of hot `HDiffBuffers` to store in memory. + pub hot_hdiff_buffer_cache_size: NonZeroUsize, /// Whether to compact the database on initialization. pub compact_on_init: bool, /// Whether to compact the database during database pruning. @@ -65,14 +68,12 @@ pub struct StoreConfig { /// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params. #[superstruct( - variants(V1, V22), + variants(V22), variant_attributes(derive(Debug, Clone, PartialEq, Eq, Encode, Decode)) )] #[derive(Clone, Debug, PartialEq, Eq)] pub struct OnDiskStoreConfig { - #[superstruct(only(V1))] - pub slots_per_restore_point: u64, - /// Prefix byte to future-proof versions of the `OnDiskStoreConfig` post V1 + /// Prefix byte to future-proof versions of the `OnDiskStoreConfig`. #[superstruct(only(V22))] version_byte: u8, #[superstruct(only(V22))] @@ -90,10 +91,6 @@ impl OnDiskStoreConfigV22 { #[derive(Debug, Clone)] pub enum StoreConfigError { - MismatchedSlotsPerRestorePoint { - config: u64, - on_disk: u64, - }, InvalidCompressionLevel { level: i32, }, @@ -112,7 +109,8 @@ impl Default for StoreConfig { state_cache_size: DEFAULT_STATE_CACHE_SIZE, state_cache_headroom: DEFAULT_STATE_CACHE_HEADROOM, historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE, - hdiff_buffer_cache_size: DEFAULT_HDIFF_BUFFER_CACHE_SIZE, + cold_hdiff_buffer_cache_size: DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE, + hot_hdiff_buffer_cache_size: DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE, compression_level: DEFAULT_COMPRESSION_LEVEL, compact_on_init: false, compact_on_prune: true, @@ -134,21 +132,13 @@ impl StoreConfig { pub fn check_compatibility( &self, on_disk_config: &OnDiskStoreConfig, - split: &Split, - anchor: &AnchorInfo, ) -> Result<(), StoreConfigError> { - // Allow changing the hierarchy exponents if no historic states are stored. - let no_historic_states_stored = anchor.no_historic_states_stored(split.slot); - let hierarchy_config_changed = - if let Ok(on_disk_hierarchy_config) = on_disk_config.hierarchy_config() { - *on_disk_hierarchy_config != self.hierarchy_config - } else { - false - }; - - if hierarchy_config_changed && !no_historic_states_stored { + // We previously allowed the hierarchy exponents to change on non-archive nodes, but since + // schema v24 and the use of hdiffs in the hot DB, changing will require a resync. + let current_config = self.as_disk_config(); + if current_config != *on_disk_config { Err(StoreConfigError::IncompatibleStoreConfig { - config: self.as_disk_config(), + config: current_config, on_disk: on_disk_config.clone(), }) } else { @@ -204,15 +194,23 @@ impl StoreConfig { } } - pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result, Error> { + /// Compress bytes using zstd and the compression level from `self`. + pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result, std::io::Error> { let mut compressed_value = Vec::with_capacity(self.estimate_compressed_size(ssz_bytes.len())); - let mut encoder = Encoder::new(&mut compressed_value, self.compression_level) - .map_err(Error::Compression)?; - encoder.write_all(ssz_bytes).map_err(Error::Compression)?; - encoder.finish().map_err(Error::Compression)?; + let mut encoder = Encoder::new(&mut compressed_value, self.compression_level)?; + encoder.write_all(ssz_bytes)?; + encoder.finish()?; Ok(compressed_value) } + + /// Decompress bytes compressed using zstd. + pub fn decompress_bytes(&self, input: &[u8]) -> Result, std::io::Error> { + let mut out = Vec::with_capacity(self.estimate_decompressed_size(input.len())); + let mut decoder = Decoder::new(input)?; + decoder.read_to_end(&mut out)?; + Ok(out) + } } impl StoreItem for OnDiskStoreConfig { @@ -222,32 +220,21 @@ impl StoreItem for OnDiskStoreConfig { fn as_store_bytes(&self) -> Vec { match self { - OnDiskStoreConfig::V1(value) => value.as_ssz_bytes(), OnDiskStoreConfig::V22(value) => value.as_ssz_bytes(), } } fn from_store_bytes(bytes: &[u8]) -> Result { - // NOTE: V22 config can never be deserialized as a V1 because the minimum length of its - // serialization is: 1 prefix byte + 1 offset (OnDiskStoreConfigV1 container) + - // 1 offset (HierarchyConfig container) = 9. - if let Ok(value) = OnDiskStoreConfigV1::from_ssz_bytes(bytes) { - return Ok(Self::V1(value)); + match bytes.first() { + Some(22) => Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?)), + version_byte => Err(StoreConfigError::InvalidVersionByte(version_byte.copied()).into()), } - - Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?)) } } #[cfg(test)] mod test { use super::*; - use crate::{ - metadata::{ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN}, - AnchorInfo, Split, - }; - use ssz::DecodeError; - use types::{Hash256, Slot}; #[test] fn check_compatibility_ok() { @@ -257,24 +244,7 @@ mod test { let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new( store_config.hierarchy_config.clone(), )); - let split = Split::default(); - assert!(store_config - .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) - .is_ok()); - } - - #[test] - fn check_compatibility_after_migration() { - let store_config = StoreConfig { - ..Default::default() - }; - let on_disk_config = OnDiskStoreConfig::V1(OnDiskStoreConfigV1 { - slots_per_restore_point: 8192, - }); - let split = Split::default(); - assert!(store_config - .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) - .is_ok()); + assert!(store_config.check_compatibility(&on_disk_config).is_ok()); } #[test] @@ -283,70 +253,11 @@ mod test { let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { exponents: vec![5, 8, 11, 13, 16, 18, 21], })); - let split = Split { - slot: Slot::new(32), - ..Default::default() - }; - assert!(store_config - .check_compatibility(&on_disk_config, &split, &ANCHOR_FOR_ARCHIVE_NODE) - .is_err()); + assert!(store_config.check_compatibility(&on_disk_config).is_err()); } #[test] - fn check_compatibility_hierarchy_config_update() { - let store_config = StoreConfig { - ..Default::default() - }; - let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { - exponents: vec![5, 8, 11, 13, 16, 18, 21], - })); - let split = Split::default(); - let anchor = AnchorInfo { - anchor_slot: Slot::new(0), - oldest_block_slot: Slot::new(0), - oldest_block_parent: Hash256::ZERO, - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - }; - assert!(store_config - .check_compatibility(&on_disk_config, &split, &anchor) - .is_ok()); - } - - #[test] - fn serde_on_disk_config_v0_from_v1_default() { - let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); - let config_bytes = config.as_store_bytes(); - // On a downgrade, the previous version of lighthouse will attempt to deserialize the - // prefixed V22 as just the V1 version. - assert_eq!( - OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), - DecodeError::InvalidByteLength { - len: 16, - expected: 8 - }, - ); - } - - #[test] - fn serde_on_disk_config_v0_from_v1_empty() { - let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { - exponents: vec![], - })); - let config_bytes = config.as_store_bytes(); - // On a downgrade, the previous version of lighthouse will attempt to deserialize the - // prefixed V22 as just the V1 version. - assert_eq!( - OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), - DecodeError::InvalidByteLength { - len: 9, - expected: 8 - }, - ); - } - - #[test] - fn serde_on_disk_config_v1_roundtrip() { + fn on_disk_config_v22_roundtrip() { let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); let bytes = config.as_store_bytes(); assert_eq!(bytes[0], 22); @@ -356,7 +267,7 @@ mod test { } #[derive( - Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, EnumVariantNames, + Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, VariantNames, )] #[strum(serialize_all = "lowercase")] pub enum DatabaseBackend { diff --git a/beacon_node/store/src/database/interface.rs b/beacon_node/store/src/database/interface.rs index b213433241..5646f1179c 100644 --- a/beacon_node/store/src/database/interface.rs +++ b/beacon_node/store/src/database/interface.rs @@ -2,8 +2,8 @@ use crate::database::leveldb_impl; #[cfg(feature = "redb")] use crate::database::redb_impl; -use crate::{config::DatabaseBackend, KeyValueStoreOp, StoreConfig}; -use crate::{metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore}; +use crate::{ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore, metrics}; +use crate::{KeyValueStoreOp, StoreConfig, config::DatabaseBackend}; use std::collections::HashSet; use std::path::Path; use types::EthSpec; @@ -105,15 +105,6 @@ impl KeyValueStore for BeaconNodeBackend { } } - fn begin_rw_transaction(&self) -> parking_lot::MutexGuard<()> { - match self { - #[cfg(feature = "leveldb")] - BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::begin_rw_transaction(txn), - #[cfg(feature = "redb")] - BeaconNodeBackend::Redb(txn) => redb_impl::Redb::begin_rw_transaction(txn), - } - } - fn compact(&self) -> Result<(), Error> { match self { #[cfg(feature = "leveldb")] @@ -123,7 +114,11 @@ impl KeyValueStore for BeaconNodeBackend { } } - fn iter_column_keys_from(&self, _column: DBColumn, from: &[u8]) -> ColumnKeyIter { + fn iter_column_keys_from( + &self, + _column: DBColumn, + from: &[u8], + ) -> ColumnKeyIter<'_, K> { match self { #[cfg(feature = "leveldb")] BeaconNodeBackend::LevelDb(txn) => { @@ -136,7 +131,7 @@ impl KeyValueStore for BeaconNodeBackend { } } - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter<'_, K> { match self { #[cfg(feature = "leveldb")] BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::iter_column_keys(txn, column), @@ -145,7 +140,7 @@ impl KeyValueStore for BeaconNodeBackend { } } - fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> { match self { #[cfg(feature = "leveldb")] BeaconNodeBackend::LevelDb(txn) => { diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs index 81d6d1d4bd..6b8c615631 100644 --- a/beacon_node/store/src/database/leveldb_impl.rs +++ b/beacon_node/store/src/database/leveldb_impl.rs @@ -1,30 +1,28 @@ -use crate::hot_cold_store::{BytesKey, HotColdDBError}; use crate::Key; +use crate::hot_cold_store::{BytesKey, HotColdDBError}; use crate::{ - get_key_for_col, metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, KeyValueStoreOp, + ColumnIter, ColumnKeyIter, DBColumn, Error, KeyValueStoreOp, get_key_for_col, metrics, }; +use fixed_bytes::FixedBytesExtended; use leveldb::{ compaction::Compaction, database::{ + Database, batch::{Batch, Writebatch}, kv::KV, - Database, }, iterator::{Iterable, LevelDBIterator}, options::{Options, ReadOptions}, }; -use parking_lot::{Mutex, MutexGuard}; use std::collections::HashSet; use std::marker::PhantomData; use std::path::Path; -use types::{EthSpec, FixedBytesExtended, Hash256}; +use types::{EthSpec, Hash256}; use super::interface::WriteOptions; pub struct LevelDB { db: Database, - /// A mutex to synchronise sensitive read-write transactions. - transaction_mutex: Mutex<()>, _phantom: PhantomData, } @@ -43,16 +41,14 @@ impl LevelDB { options.create_if_missing = true; let db = Database::open(path, options)?; - let transaction_mutex = Mutex::new(()); Ok(Self { db, - transaction_mutex, _phantom: PhantomData, }) } - pub fn read_options(&self) -> ReadOptions { + pub fn read_options(&self) -> ReadOptions<'_, BytesKey> { ReadOptions::new() } @@ -177,10 +173,6 @@ impl LevelDB { Ok(()) } - pub fn begin_rw_transaction(&self) -> MutexGuard<()> { - self.transaction_mutex.lock() - } - /// Compact all values in the states and states flag columns. pub fn compact(&self) -> Result<(), Error> { let _timer = metrics::start_timer(&metrics::DISK_DB_COMPACT_TIMES); @@ -216,7 +208,7 @@ impl LevelDB { Ok(()) } - pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> { let start_key = BytesKey::from_vec(get_key_for_col(column, from)); let iter = self.db.iter(self.read_options()); iter.seek(&start_key); @@ -240,7 +232,11 @@ impl LevelDB { ) } - pub fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + pub fn iter_column_keys_from( + &self, + column: DBColumn, + from: &[u8], + ) -> ColumnKeyIter<'_, K> { let start_key = BytesKey::from_vec(get_key_for_col(column, from)); let iter = self.db.keys_iter(self.read_options()); @@ -262,11 +258,11 @@ impl LevelDB { } /// Iterate through all keys and values in a particular column. - pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter<'_, K> { self.iter_column_keys_from(column, &vec![0; column.key_size()]) } - pub fn iter_column(&self, column: DBColumn) -> ColumnIter { + pub fn iter_column(&self, column: DBColumn) -> ColumnIter<'_, K> { self.iter_column_from(column, &vec![0; column.key_size()]) } @@ -287,7 +283,8 @@ impl LevelDB { ) -> Result<(), Error> { let mut leveldb_batch = Writebatch::new(); let iter = self.db.iter(self.read_options()); - + let start_key = BytesKey::from_vec(column.as_bytes().to_vec()); + iter.seek(&start_key); iter.take_while(move |(key, _)| key.matches_column(column)) .for_each(|(key, value)| { if f(&value).unwrap_or(false) { diff --git a/beacon_node/store/src/database/redb_impl.rs b/beacon_node/store/src/database/redb_impl.rs index cbe575d184..4077326eca 100644 --- a/beacon_node/store/src/database/redb_impl.rs +++ b/beacon_node/store/src/database/redb_impl.rs @@ -1,6 +1,6 @@ -use crate::{metrics, ColumnIter, ColumnKeyIter, Key}; +use crate::{ColumnIter, ColumnKeyIter, Key, metrics}; use crate::{DBColumn, Error, KeyValueStoreOp}; -use parking_lot::{Mutex, MutexGuard, RwLock}; +use parking_lot::RwLock; use redb::TableDefinition; use std::collections::HashSet; use std::{borrow::BorrowMut, marker::PhantomData, path::Path}; @@ -13,7 +13,6 @@ pub const DB_FILE_NAME: &str = "database.redb"; pub struct Redb { db: RwLock, - transaction_mutex: Mutex<()>, _phantom: PhantomData, } @@ -31,7 +30,6 @@ impl Redb { pub fn open(path: &Path) -> Result { let db_file = path.join(DB_FILE_NAME); let db = redb::Database::create(db_file)?; - let transaction_mutex = Mutex::new(()); for column in DBColumn::iter() { Redb::::create_table(&db, column.into())?; @@ -39,7 +37,6 @@ impl Redb { Ok(Self { db: db.into(), - transaction_mutex, _phantom: PhantomData, }) } @@ -61,10 +58,6 @@ impl Redb { opts } - pub fn begin_rw_transaction(&self) -> MutexGuard<()> { - self.transaction_mutex.lock() - } - pub fn put_bytes_with_options( &self, col: DBColumn, @@ -211,7 +204,11 @@ impl Redb { mut_db.compact().map_err(Into::into).map(|_| ()) } - pub fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + pub fn iter_column_keys_from( + &self, + column: DBColumn, + from: &[u8], + ) -> ColumnKeyIter<'_, K> { let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(column.into()); @@ -239,11 +236,11 @@ impl Redb { } /// Iterate through all keys and values in a particular column. - pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter<'_, K> { self.iter_column_keys_from(column, &vec![0; column.key_size()]) } - pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> { let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(column.into()); @@ -276,7 +273,7 @@ impl Redb { } } - pub fn iter_column(&self, column: DBColumn) -> ColumnIter { + pub fn iter_column(&self, column: DBColumn) -> ColumnIter<'_, K> { self.iter_column_from(column, &vec![0; column.key_size()]) } diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index cff08bc655..a07cc83886 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,21 +1,18 @@ -use crate::chunked_vector::ChunkError; use crate::config::StoreConfigError; -use crate::hot_cold_store::HotColdDBError; -use crate::{hdiff, DBColumn}; +use crate::hot_cold_store::{HotColdDBError, StateSummaryIteratorError}; +use crate::{DBColumn, hdiff}; #[cfg(feature = "leveldb")] use leveldb::error::Error as LevelDBError; use ssz::DecodeError; use state_processing::BlockReplayError; -use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; +use types::{BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { SszDecodeError(DecodeError), - VectorChunkError(ChunkError), BeaconStateError(BeaconStateError), - PartialBeaconStateError, HotColdDBError(HotColdDBError), DBError { message: String, @@ -26,6 +23,9 @@ pub enum Error { SplitPointModified(Slot, Slot), ConfigError(StoreConfigError), MigrationError(String), + /// The store's `anchor_info` is still the default uninitialized value when attempting a state + /// write + AnchorUninitialized, /// The store's `anchor_info` was mutated concurrently, the latest modification wasn't applied. AnchorInfoConcurrentMutation, /// The store's `blob_info` was mutated concurrently, the latest modification wasn't applied. @@ -47,11 +47,16 @@ pub enum Error { expected: Hash256, computed: Hash256, }, + MissingState(Hash256), + MissingHotStateSummary(Hash256), + MissingHotStateSnapshot(Hash256, Slot), MissingGenesisState, MissingSnapshot(Slot), + LoadingHotHdiffBufferError(String, Hash256, Box), + LoadingHotStateError(String, Hash256, Box), BlockReplayError(BlockReplayError), AddPayloadLogicError, - InvalidKey, + InvalidKey(String), InvalidBytes, InconsistentFork(InconsistentFork), #[cfg(feature = "leveldb")] @@ -61,6 +66,7 @@ pub enum Error { CacheBuildError(EpochCacheError), RandaoMixOutOfBounds, MilhouseError(milhouse::Error), + SszTypesError(ssz_types::Error), Compression(std::io::Error), FinalizedStateDecreasingSlot, FinalizedStateUnaligned, @@ -75,6 +81,26 @@ pub enum Error { MissingBlock(Hash256), GenesisStateUnknown, ArithError(safe_arith::ArithError), + MismatchedDiffBaseState { + expected_slot: Slot, + stored_slot: Slot, + }, + SnapshotDiffBaseState { + slot: Slot, + }, + LoadAnchorInfo(Box), + LoadSplit(Box), + LoadBlobInfo(Box), + LoadDataColumnInfo(Box), + LoadConfig(Box), + LoadHotStateSummary(Hash256, Box), + LoadHotStateSummaryForSplit(Box), + StateSummaryIteratorError { + error: StateSummaryIteratorError, + from_state_root: Hash256, + from_state_slot: Slot, + target_slot: Slot, + }, } pub trait HandleUnavailable { @@ -97,12 +123,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: ChunkError) -> Error { - Error::VectorChunkError(e) - } -} - impl From for Error { fn from(e: HotColdDBError) -> Error { Error::HotColdDBError(e) @@ -133,6 +153,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz_types::Error) -> Self { + Self::SszTypesError(e) + } +} + impl From for Error { fn from(e: hdiff::Error) -> Self { Self::Hdiff(e) diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs index a659c65452..323c87a914 100644 --- a/beacon_node/store/src/hdiff.rs +++ b/beacon_node/store/src/hdiff.rs @@ -1,19 +1,18 @@ //! Hierarchical diff implementation. -use crate::{metrics, DBColumn, StoreConfig, StoreItem}; +use crate::{DBColumn, StoreConfig, StoreItem, metrics}; use bls::PublicKeyBytes; use itertools::Itertools; +use milhouse::List; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::cmp::Ordering; -use std::io::{Read, Write}; use std::ops::RangeInclusive; use std::str::FromStr; use std::sync::LazyLock; use superstruct::superstruct; use types::historical_summary::HistoricalSummary; -use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, List, Slot, Validator}; -use zstd::{Decoder, Encoder}; +use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot, Validator}; static EMPTY_PUBKEY: LazyLock = LazyLock::new(PublicKeyBytes::empty); @@ -27,6 +26,7 @@ pub enum Error { Compression(std::io::Error), InvalidSszState(ssz::DecodeError), InvalidBalancesLength, + LessThanStart(Slot, Slot), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] @@ -67,6 +67,10 @@ impl FromStr for HierarchyConfig { return Err("hierarchy-exponents must be in ascending order".to_string()); } + if exponents.is_empty() { + return Err("empty exponents".to_string()); + } + Ok(HierarchyConfig { exponents }) } } @@ -390,13 +394,17 @@ impl CompressedU64Diff { .collect(); Ok(CompressedU64Diff { - bytes: compress_bytes(&uncompressed_bytes, config)?, + bytes: config + .compress_bytes(&uncompressed_bytes) + .map_err(Error::Compression)?, }) } pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { // Decompress balances diff. - let balances_diff_bytes = uncompress_bytes(&self.bytes, config)?; + let balances_diff_bytes = config + .decompress_bytes(&self.bytes) + .map_err(Error::Compression)?; for (i, diff_bytes) in balances_diff_bytes .chunks(u64::BITS as usize / 8) @@ -423,22 +431,6 @@ impl CompressedU64Diff { } } -fn compress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { - let compression_level = config.compression_level; - let mut out = Vec::with_capacity(config.estimate_compressed_size(input.len())); - let mut encoder = Encoder::new(&mut out, compression_level).map_err(Error::Compression)?; - encoder.write_all(input).map_err(Error::Compression)?; - encoder.finish().map_err(Error::Compression)?; - Ok(out) -} - -fn uncompress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { - let mut out = Vec::with_capacity(config.estimate_decompressed_size(input.len())); - let mut decoder = Decoder::new(input).map_err(Error::Compression)?; - decoder.read_to_end(&mut out).map_err(Error::Compression)?; - Ok(out) -} - impl ValidatorsDiff { pub fn compute( xs: &[Validator], @@ -478,7 +470,9 @@ impl ValidatorsDiff { Hash256::ZERO }, // effective_balance can increase and decrease - effective_balance: y.effective_balance - x.effective_balance, + effective_balance: y + .effective_balance + .wrapping_sub(x.effective_balance), // slashed can only change from false into true. In an index re-use it can // switch back to false, but in that case the pubkey will also change. slashed: y.slashed, @@ -527,12 +521,16 @@ impl ValidatorsDiff { .collect::>(); Ok(Self { - bytes: compress_bytes(&uncompressed_bytes, config)?, + bytes: config + .compress_bytes(&uncompressed_bytes) + .map_err(Error::Compression)?, }) } pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { - let validator_diff_bytes = uncompress_bytes(&self.bytes, config)?; + let validator_diff_bytes = config + .decompress_bytes(&self.bytes) + .map_err(Error::Compression)?; for diff_bytes in validator_diff_bytes.chunks(::ssz_fixed_len()) @@ -642,10 +640,26 @@ impl HierarchyConfig { Err(Error::InvalidHierarchy) } } + + pub fn exponent_for_slot(slot: Slot) -> u32 { + slot.as_u64().trailing_zeros() + } } impl HierarchyModuli { - pub fn storage_strategy(&self, slot: Slot) -> Result { + /// * `slot` - Slot of the storage strategy + /// * `start_slot` - Slot before which states are not available. Initial snapshot point, which + /// may not be aligned to the hierarchy moduli values. Given an example of + /// exponents [5,13,21], to reconstruct state at slot 3,000,003: if start = 3,000,002 + /// layer 2 diff will point to the start snapshot instead of the layer 1 diff at + /// 2998272. + pub fn storage_strategy(&self, slot: Slot, start_slot: Slot) -> Result { + match slot.cmp(&start_slot) { + Ordering::Less => return Err(Error::LessThanStart(slot, start_slot)), + Ordering::Equal => return Ok(StorageStrategy::Snapshot), + Ordering::Greater => {} // continue + } + // last = full snapshot interval let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; // first = most frequent diff layer, need to replay blocks from this layer @@ -667,14 +681,22 @@ impl HierarchyModuli { .find_map(|(&n_big, &n_small)| { if slot % n_small == 0 { // Diff from the previous layer. - Some(StorageStrategy::DiffFrom(slot / n_big * n_big)) + let from = slot / n_big * n_big; + // Or from start point + let from = std::cmp::max(from, start_slot); + Some(StorageStrategy::DiffFrom(from)) } else { // Keep trying with next layer None } }) // Exhausted layers, need to replay from most frequent layer - .unwrap_or(StorageStrategy::ReplayFrom(slot / first * first))) + .unwrap_or_else(|| { + let from = slot / first * first; + // Or from start point + let from = std::cmp::max(from, start_slot); + StorageStrategy::ReplayFrom(from) + })) } /// Return the smallest slot greater than or equal to `slot` at which a full snapshot should @@ -703,6 +725,26 @@ impl HierarchyModuli { |second_layer_moduli| Ok(slot % *second_layer_moduli == 0), ) } + + /// For each layer, returns the closest diff less than or equal to `slot`. + pub fn closest_layer_points(&self, slot: Slot, start_slot: Slot) -> Vec { + let mut layers = self + .moduli + .iter() + .map(|&n| { + let from = slot / n * n; + // Or from start point + std::cmp::max(from, start_slot) + }) + .collect::>(); + + // Remove duplication caused by the capping at `start_slot` (multiple + // layers may have the same slot equal to `start_slot`), or shared multiples (a slot that is + // a multiple of 2**n will also be a multiple of 2**m for all m < n). + layers.dedup(); + + layers + } } impl StorageStrategy { @@ -732,45 +774,69 @@ impl StorageStrategy { } .map(Slot::from) } + + /// Returns the slot that storage_strategy points to. + pub fn diff_base_slot(&self) -> Option { + match self { + Self::ReplayFrom(from) => Some(*from), + Self::DiffFrom(from) => Some(*from), + Self::Snapshot => None, + } + } + + pub fn is_replay_from(&self) -> bool { + matches!(self, Self::ReplayFrom(_)) + } + + pub fn is_diff_from(&self) -> bool { + matches!(self, Self::DiffFrom(_)) + } + + pub fn is_snapshot(&self) -> bool { + matches!(self, Self::Snapshot) + } } #[cfg(test)] mod tests { use super::*; - use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; + use rand::{Rng, SeedableRng, rng, rngs::SmallRng}; #[test] fn default_storage_strategy() { let config = HierarchyConfig::default(); config.validate().unwrap(); + let sslot = Slot::new(0); let moduli = config.to_moduli().unwrap(); // Full snapshots at multiples of 2^21. let snapshot_freq = Slot::new(1 << 21); assert_eq!( - moduli.storage_strategy(Slot::new(0)).unwrap(), + moduli.storage_strategy(Slot::new(0), sslot).unwrap(), StorageStrategy::Snapshot ); assert_eq!( - moduli.storage_strategy(snapshot_freq).unwrap(), + moduli.storage_strategy(snapshot_freq, sslot).unwrap(), StorageStrategy::Snapshot ); assert_eq!( - moduli.storage_strategy(snapshot_freq * 3).unwrap(), + moduli.storage_strategy(snapshot_freq * 3, sslot).unwrap(), StorageStrategy::Snapshot ); // Diffs should be from the previous layer (the snapshot in this case), and not the previous diff in the same layer. let first_layer = Slot::new(1 << 18); assert_eq!( - moduli.storage_strategy(first_layer * 2).unwrap(), + moduli.storage_strategy(first_layer * 2, sslot).unwrap(), StorageStrategy::DiffFrom(Slot::new(0)) ); let replay_strategy_slot = first_layer + 1; assert_eq!( - moduli.storage_strategy(replay_strategy_slot).unwrap(), + moduli + .storage_strategy(replay_strategy_slot, sslot) + .unwrap(), StorageStrategy::ReplayFrom(first_layer) ); } @@ -839,7 +905,7 @@ mod tests { fn compressed_validators_diff() { assert_eq!(::ssz_fixed_len(), 129); - let mut rng = thread_rng(); + let mut rng = rng(); let config = &StoreConfig::default(); let xs = (0..10) .map(|_| rand_validator(&mut rng)) @@ -857,7 +923,7 @@ mod tests { fn rand_validator(mut rng: impl Rng) -> Validator { let mut pubkey = [0u8; 48]; rng.fill_bytes(&mut pubkey); - let withdrawal_credentials: [u8; 32] = rng.gen(); + let withdrawal_credentials: [u8; 32] = rng.random(); Validator { pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), @@ -940,4 +1006,93 @@ mod tests { ] ); } + + // Test that the diffs and snapshots required for storage of split states are retained in the + // hot DB as the split slot advances, if we begin from an initial configuration where this + // invariant holds. + fn test_slots_retained_invariant(hierarchy: HierarchyModuli, start_slot: u64, epoch_jump: u64) { + let start_slot = Slot::new(start_slot); + let mut finalized_slot = start_slot; + + // Initially we have just one snapshot stored at the `start_slot`. This is what checkpoint + // sync sets up (or the V24 migration). + let mut retained_slots = vec![finalized_slot]; + + // Iterate until we've reached two snapshots in the future. + let stop_at = hierarchy + .next_snapshot_slot(hierarchy.next_snapshot_slot(start_slot).unwrap() + 1) + .unwrap(); + + while finalized_slot <= stop_at { + // Jump multiple epocsh at a time because inter-epoch states are not interesting and + // would take too long to iterate over. + let new_finalized_slot = finalized_slot + 32 * epoch_jump; + + let new_retained_slots = hierarchy.closest_layer_points(new_finalized_slot, start_slot); + + for slot in &new_retained_slots { + // All new retained slots must either be already stored prior to the old finalized + // slot, OR newer than the finalized slot (i.e. stored in the hot DB as part of + // regular state storage). + assert!(retained_slots.contains(slot) || *slot >= finalized_slot); + } + + retained_slots = new_retained_slots; + finalized_slot = new_finalized_slot; + } + } + + #[test] + fn slots_retained_invariant() { + let cases = [ + // Default hierarchy with a start_slot between the 2^13 and 2^16 layers. + ( + HierarchyConfig::default().to_moduli().unwrap(), + 2 * (1 << 14) - 5 * 32, + 1, + ), + // Default hierarchy with a start_slot between the 2^13 and 2^16 layers, with 8 epochs + // finalizing at a time (should not make any difference). + ( + HierarchyConfig::default().to_moduli().unwrap(), + 2 * (1 << 14) - 5 * 32, + 8, + ), + // Very dense hierarchy config. + ( + HierarchyConfig::from_str("5,7") + .unwrap() + .to_moduli() + .unwrap(), + 32, + 1, + ), + // Very dense hierarchy config that skips a whole snapshot on its first finalization. + ( + HierarchyConfig::from_str("5,7") + .unwrap() + .to_moduli() + .unwrap(), + 32, + 1 << 7, + ), + ]; + + for (hierarchy, start_slot, epoch_jump) in cases { + test_slots_retained_invariant(hierarchy, start_slot, epoch_jump); + } + } + + #[test] + fn closest_layer_points_unique() { + let hierarchy = HierarchyConfig::default().to_moduli().unwrap(); + + let start_slot = Slot::new(0); + let end_slot = hierarchy.next_snapshot_slot(Slot::new(1)).unwrap(); + + for slot in (0..end_slot.as_u64()).map(Slot::new) { + let closest_layer_points = hierarchy.closest_layer_points(slot, start_slot); + assert!(closest_layer_points.is_sorted_by(|a, b| a > b)); + } + } } diff --git a/beacon_node/store/src/historic_state_cache.rs b/beacon_node/store/src/historic_state_cache.rs index c0e8f8346c..e5abb04c07 100644 --- a/beacon_node/store/src/historic_state_cache.rs +++ b/beacon_node/store/src/historic_state_cache.rs @@ -34,11 +34,17 @@ impl HistoricStateCache { pub fn get_hdiff_buffer(&mut self, slot: Slot) -> Option { if let Some(buffer_ref) = self.hdiff_buffers.get(&slot) { - let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + let _timer = metrics::start_timer_vec( + &metrics::BEACON_HDIFF_BUFFER_CLONE_TIME, + metrics::COLD_METRIC, + ); Some(buffer_ref.clone()) } else if let Some(state) = self.states.get(&slot) { let buffer = HDiffBuffer::from_state(state.clone()); - let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + let _timer = metrics::start_timer_vec( + &metrics::BEACON_HDIFF_BUFFER_CLONE_TIME, + metrics::COLD_METRIC, + ); let cloned = buffer.clone(); drop(_timer); self.hdiff_buffers.put(slot, cloned); diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index d4b68357b2..c413719174 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,23 +1,25 @@ use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::database::interface::BeaconNodeBackend; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; -use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; +use crate::hdiff::{HDiff, HDiffBuffer, HierarchyConfig, HierarchyModuli, StorageStrategy}; use crate::historic_state_cache::HistoricStateCache; -use crate::impls::beacon_state::{get_full_state, store_full_state}; use crate::iter::{BlockRootsIterator, ParentRootBlockIterator, RootsIterator}; use crate::memory_store::MemoryStore; use crate::metadata::{ - AnchorInfo, BlobInfo, CompactionTimestamp, DataColumnInfo, PruningCheckpoint, SchemaVersion, - ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_INFO_KEY, ANCHOR_UNINITIALIZED, BLOB_INFO_KEY, - COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, DATA_COLUMN_INFO_KEY, - PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, + ANCHOR_INFO_KEY, ANCHOR_UNINITIALIZED, AnchorInfo, BLOB_INFO_KEY, BlobInfo, + COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, CompactionTimestamp, + DATA_COLUMN_CUSTODY_INFO_KEY, DATA_COLUMN_INFO_KEY, DataColumnCustodyInfo, DataColumnInfo, + SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, SchemaVersion, }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, DBColumn, - DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem, StoreOp, + BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem, + StoreOp, get_data_column_key, + metrics::{self, COLD_METRIC, HOT_METRIC}, + parse_data_column_key, }; -use itertools::{process_results, Itertools}; +use fixed_bytes::FixedBytesExtended; +use itertools::{Itertools, process_results}; use lru::LruCache; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; @@ -25,10 +27,10 @@ use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use state_processing::{ - block_replayer::PreSlotHook, AllCaches, BlockProcessingError, BlockReplayer, - SlotProcessingError, + AllCaches, BlockProcessingError, BlockReplayer, SlotProcessingError, + block_replayer::PreSlotHook, }; -use std::cmp::min; +use std::cmp::{Ordering, min}; use std::collections::{HashMap, HashSet}; use std::io::{Read, Write}; use std::marker::PhantomData; @@ -36,7 +38,8 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, instrument, warn}; +use typenum::Unsigned; use types::data_column_sidecar::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; use zstd::{Decoder, Encoder}; @@ -59,7 +62,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// The starting slots for the range of data columns stored in the database. data_column_info: RwLock, pub(crate) config: StoreConfig, - pub(crate) hierarchy: HierarchyModuli, + pub hierarchy: HierarchyModuli, /// Cold database containing compact historical data. pub cold_db: Cold, /// Database containing blobs. If None, store falls back to use `cold_db`. @@ -69,7 +72,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// The hot database also contains all blocks. pub hot_db: Hot, /// LRU cache of deserialized blocks and blobs. Updated whenever a block or blob is loaded. - block_cache: Mutex>, + block_cache: Option>>, /// Cache of beacon states. /// /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. @@ -90,6 +93,7 @@ struct BlockCache { block_cache: LruCache>, blob_cache: LruCache>, data_column_cache: LruCache>>>, + data_column_custody_info_cache: Option, } impl BlockCache { @@ -98,6 +102,7 @@ impl BlockCache { block_cache: LruCache::new(size), blob_cache: LruCache::new(size), data_column_cache: LruCache::new(size), + data_column_custody_info_cache: None, } } pub fn put_block(&mut self, block_root: Hash256, block: SignedBeaconBlock) { @@ -111,6 +116,12 @@ impl BlockCache { .get_or_insert_mut(block_root, Default::default) .insert(data_column.index, data_column); } + pub fn put_data_column_custody_info( + &mut self, + data_column_custody_info: Option, + ) { + self.data_column_custody_info_cache = data_column_custody_info; + } pub fn get_block<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a SignedBeaconBlock> { self.block_cache.get(block_root) } @@ -128,15 +139,22 @@ impl BlockCache { .get(block_root) .and_then(|map| map.get(column_index).cloned()) } + pub fn get_data_column_custody_info(&self) -> Option { + self.data_column_custody_info_cache.clone() + } pub fn delete_block(&mut self, block_root: &Hash256) { let _ = self.block_cache.pop(block_root); } pub fn delete_blobs(&mut self, block_root: &Hash256) { let _ = self.blob_cache.pop(block_root); } + pub fn delete_data_columns(&mut self, block_root: &Hash256) { + let _ = self.data_column_cache.pop(block_root); + } pub fn delete(&mut self, block_root: &Hash256) { - let _ = self.block_cache.pop(block_root); - let _ = self.blob_cache.pop(block_root); + self.delete_block(block_root); + self.delete_blobs(block_root); + self.delete_data_columns(block_root); } } @@ -159,9 +177,13 @@ pub enum HotColdDBError { MissingColdStateSummary(Hash256), MissingHotStateSummary(Hash256), MissingEpochBoundaryState(Hash256, Hash256), + MissingHotState { + state_root: Hash256, + requested_by_state_summary: (Hash256, Slot), + }, MissingPrevState(Hash256), MissingSplitState(Hash256, Slot), - MissingStateDiff(Hash256), + MissingHotHDiff(Hash256), MissingHDiff(Slot), MissingExecutionPayload(Hash256), MissingFullBlockExecutionPayloadPruned(Hash256, Slot), @@ -170,7 +192,7 @@ pub enum HotColdDBError { MissingFrozenBlock(Slot), MissingPathToBlobsDatabase, BlobsPreviouslyInDefaultStore, - HotStateSummaryError(BeaconStateError), + HdiffGetPriorStateRootError(Slot, Slot), RestorePointDecodeError(ssz::DecodeError), BlockReplayBeaconError(BeaconStateError), BlockReplaySlotError(SlotProcessingError), @@ -203,6 +225,8 @@ impl HotColdDB, MemoryStore> { let hierarchy = config.hierarchy_config.to_moduli()?; + // NOTE: Anchor slot is initialized to 0, which is only valid for new DBs. We shouldn't + // be reusing memory stores, but if we want to do that we should redo this. let db = HotColdDB { split: RwLock::new(Split::default()), anchor_info: RwLock::new(ANCHOR_UNINITIALIZED), @@ -211,13 +235,16 @@ impl HotColdDB, MemoryStore> { cold_db: MemoryStore::open(), blobs_db: MemoryStore::open(), hot_db: MemoryStore::open(), - block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), + block_cache: NonZeroUsize::new(config.block_cache_size) + .map(BlockCache::new) + .map(Mutex::new), state_cache: Mutex::new(StateCache::new( config.state_cache_size, config.state_cache_headroom, + config.hot_hdiff_buffer_cache_size, )), historic_state_cache: Mutex::new(HistoricStateCache::new( - config.hdiff_buffer_cache_size, + config.cold_hdiff_buffer_cache_size, config.historic_state_cache_size, )), config, @@ -243,12 +270,16 @@ impl HotColdDB, BeaconNodeBackend> { config: StoreConfig, spec: Arc, ) -> Result, Error> { + debug!("Opening HotColdDB"); config.verify::()?; let hierarchy = config.hierarchy_config.to_moduli()?; + debug!(?hot_path, "Opening LevelDB"); let hot_db = BeaconNodeBackend::open(&config, hot_path)?; + let anchor_info = RwLock::new(Self::load_anchor_info(&hot_db)?); + debug!(?anchor_info, "Loaded anchor info"); let db = HotColdDB { split: RwLock::new(Split::default()), @@ -258,13 +289,16 @@ impl HotColdDB, BeaconNodeBackend> { blobs_db: BeaconNodeBackend::open(&config, blobs_db_path)?, cold_db: BeaconNodeBackend::open(&config, cold_path)?, hot_db, - block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), + block_cache: NonZeroUsize::new(config.block_cache_size) + .map(BlockCache::new) + .map(Mutex::new), state_cache: Mutex::new(StateCache::new( config.state_cache_size, config.state_cache_headroom, + config.hot_hdiff_buffer_cache_size, )), historic_state_cache: Mutex::new(HistoricStateCache::new( - config.hdiff_buffer_cache_size, + config.cold_hdiff_buffer_cache_size, config.historic_state_cache_size, )), config, @@ -279,12 +313,27 @@ impl HotColdDB, BeaconNodeBackend> { // Load the previous split slot from the database (if any). This ensures we can // stop and restart correctly. This needs to occur *before* running any migrations // because some migrations load states and depend on the split. + // + // We use a method that is ambivalent to the state summaries being V22 or V24, because + // we need to support several scenarios: + // + // - Migrating from V22 to V24: initially summaries are V22 , and we need + // to be able to load a block root from them. Loading the split partially at first + // (without reading a V24 summary) and then completing the full load after the migration + // runs is possible in this case, but not in the next case. + // - Migrating from V24 to V22: initially summaries are V24, but after the migration runs + // they will be V22. If we used the "load full split after migration" approach with strict + // V24 summaries, it would break when trying to read V22 summaries after the migration. + // + // Therefore we take the most flexible approach of reading _either_ a V22 or V24 summary and + // using this to load the split correctly the first time. if let Some(split) = db.load_split()? { *db.split.write() = split; info!( %split.slot, - split_state = ?split.state_root, + ?split.state_root, + ?split.block_root, "Hot-Cold DB initialized" ); } @@ -353,6 +402,16 @@ impl HotColdDB, BeaconNodeBackend> { "Blob DB initialized" ); + // Ensure that any on-disk config is compatible with the supplied config. + // + // We do this prior to the migration now, because we don't want the migration using the + // in-memory config if it is inconsistent with the on-disk config. In future we may need + // to put this in/after the migration if the migration changes the config format. + if let Some(disk_config) = db.load_config()? { + db.config.check_compatibility(&disk_config)?; + } + db.store_config()?; + // Ensure that the schema version of the on-disk database matches the software. // If the version is mismatched, an automatic migration will be attempted. let db = Arc::new(db); @@ -362,31 +421,16 @@ impl HotColdDB, BeaconNodeBackend> { to_version = CURRENT_SCHEMA_VERSION.as_u64(), "Attempting schema migration" ); - migrate_schema(db.clone(), schema_version, CURRENT_SCHEMA_VERSION)?; + migrate_schema(db.clone(), schema_version, CURRENT_SCHEMA_VERSION).map_err(|e| { + Error::MigrationError(format!( + "Migrating from {:?} to {:?}: {:?}", + schema_version, CURRENT_SCHEMA_VERSION, e + )) + })?; } else { db.store_schema_version(CURRENT_SCHEMA_VERSION)?; } - // Ensure that any on-disk config is compatible with the supplied config. - if let Some(disk_config) = db.load_config()? { - let split = db.get_split_info(); - let anchor = db.get_anchor_info(); - db.config - .check_compatibility(&disk_config, &split, &anchor)?; - - // Inform user if hierarchy config is changing. - if let Ok(hierarchy_config) = disk_config.hierarchy_config() { - if &db.config.hierarchy_config != hierarchy_config { - info!( - previous_config = %hierarchy_config, - new_config = %db.config.hierarchy_config, - "Updating historic state config" - ); - } - } - } - db.store_config()?; - // TODO(tree-states): Here we can choose to prune advanced states to reclaim disk space. As // it's a foreground task there's no risk of race condition that can corrupt the DB. // Advanced states for invalid blocks that were never written to the DB, or descendants of @@ -400,20 +444,51 @@ impl HotColdDB, BeaconNodeBackend> { info!("Foreground compaction complete"); } + debug!(anchor = ?db.get_anchor_info(), "Store anchor info"); + Ok(db) } } impl, Cold: ItemStore> HotColdDB { + fn cold_storage_strategy(&self, slot: Slot) -> Result { + // The start slot for the freezer HDiff is always 0 + Ok(self.hierarchy.storage_strategy(slot, Slot::new(0))?) + } + + pub fn hot_storage_strategy(&self, slot: Slot) -> Result { + Ok(self + .hierarchy + .storage_strategy(slot, self.hot_hdiff_start_slot()?)?) + } + + pub fn hot_hdiff_start_slot(&self) -> Result { + let anchor_slot = self.anchor_info.read_recursive().anchor_slot; + if anchor_slot == u64::MAX { + // If hot_hdiff_start_slot returns such a high value all writes will fail. This should + // never happen, but it's best to stop this useless value from propagating downstream + Err(Error::AnchorUninitialized) + } else { + Ok(anchor_slot) + } + } + pub fn update_finalized_state( &self, state_root: Hash256, block_root: Hash256, state: BeaconState, ) -> Result<(), Error> { - self.state_cache - .lock() - .update_finalized_state(state_root, block_root, state) + let start_slot = self.get_anchor_info().anchor_slot; + let pre_finalized_slots_to_retain = self + .hierarchy + .closest_layer_points(state.slot(), start_slot); + self.state_cache.lock().update_finalized_state( + state_root, + block_root, + state, + &pre_finalized_slots_to_retain, + ) } pub fn state_cache_len(&self) -> usize { @@ -423,28 +498,45 @@ impl, Cold: ItemStore> HotColdDB pub fn register_metrics(&self) { let hsc_metrics = self.historic_state_cache.lock().metrics(); - metrics::set_gauge( - &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, - self.block_cache.lock().block_cache.len() as i64, - ); - metrics::set_gauge( - &metrics::STORE_BEACON_BLOB_CACHE_SIZE, - self.block_cache.lock().blob_cache.len() as i64, - ); + if let Some(block_cache) = &self.block_cache { + let cache = block_cache.lock(); + metrics::set_gauge( + &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, + cache.block_cache.len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_BLOB_CACHE_SIZE, + cache.blob_cache.len() as i64, + ); + } + let state_cache = self.state_cache.lock(); metrics::set_gauge( &metrics::STORE_BEACON_STATE_CACHE_SIZE, - self.state_cache.lock().len() as i64, + state_cache.len() as i64, ); + metrics::set_gauge_vec( + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE, + HOT_METRIC, + state_cache.num_hdiff_buffers() as i64, + ); + metrics::set_gauge_vec( + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, + HOT_METRIC, + state_cache.hdiff_buffer_mem_usage() as i64, + ); + drop(state_cache); metrics::set_gauge( &metrics::STORE_BEACON_HISTORIC_STATE_CACHE_SIZE, hsc_metrics.num_state as i64, ); - metrics::set_gauge( + metrics::set_gauge_vec( &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE, + COLD_METRIC, hsc_metrics.num_hdiff as i64, ); - metrics::set_gauge( + metrics::set_gauge_vec( &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, + COLD_METRIC, hsc_metrics.hdiff_byte_size as i64, ); @@ -474,7 +566,9 @@ impl, Cold: ItemStore> HotColdDB let block = self.block_as_kv_store_ops(block_root, block, &mut ops)?; self.hot_db.do_atomically(ops)?; // Update cache. - self.block_cache.lock().put_block(*block_root, block); + self.block_cache + .as_ref() + .inspect(|cache| cache.lock().put_block(*block_root, block)); Ok(()) } @@ -526,7 +620,9 @@ impl, Cold: ItemStore> HotColdDB metrics::inc_counter(&metrics::BEACON_BLOCK_GET_COUNT); // Check the cache. - if let Some(block) = self.block_cache.lock().get_block(block_root) { + if let Some(cache) = &self.block_cache + && let Some(block) = cache.lock().get_block(block_root) + { metrics::inc_counter(&metrics::BEACON_BLOCK_CACHE_HIT_COUNT); return Ok(Some(DatabaseBlock::Full(block.clone()))); } @@ -551,13 +647,19 @@ impl, Cold: ItemStore> HotColdDB // Add to cache. self.block_cache - .lock() - .put_block(*block_root, full_block.clone()); + .as_ref() + .inspect(|cache| cache.lock().put_block(*block_root, full_block.clone())); DatabaseBlock::Full(full_block) - } else if !self.config.prune_payloads { + } else if !self.config.prune_payloads || *block_root == split.block_root { // If payload pruning is disabled there's a chance we may have the payload of // this finalized block. Attempt to load it but don't error in case it's missing. + // + // We also allow for the split block's payload to be loaded *if it exists*. This is + // necessary on startup when syncing from an unaligned checkpoint (a checkpoint state + // at a skipped slot), and then loading the canonical head (with payload). If we modify + // payload pruning in future so that it doesn't prune the split block's payload, then + // this case could move to the case above where we error if the payload is missing. let fork_name = blinded_block.fork_name(&self.spec)?; if let Some(payload) = self.get_execution_payload(block_root, fork_name)? { DatabaseBlock::Full( @@ -577,6 +679,7 @@ impl, Cold: ItemStore> HotColdDB } /// Fetch a full block with execution payload from the store. + #[instrument(skip_all)] pub fn get_full_block( &self, block_root: &Hash256, @@ -822,7 +925,9 @@ impl, Cold: ItemStore> HotColdDB /// Delete a block from the store and the block cache. pub fn delete_block(&self, block_root: &Hash256) -> Result<(), Error> { - self.block_cache.lock().delete(block_root); + self.block_cache + .as_ref() + .inspect(|cache| cache.lock().delete(block_root)); self.hot_db .key_delete(DBColumn::BeaconBlock, block_root.as_slice())?; self.hot_db @@ -837,7 +942,9 @@ impl, Cold: ItemStore> HotColdDB block_root.as_slice(), &blobs.as_ssz_bytes(), )?; - self.block_cache.lock().put_blobs(*block_root, blobs); + self.block_cache + .as_ref() + .inspect(|cache| cache.lock().put_blobs(*block_root, blobs)); Ok(()) } @@ -854,6 +961,39 @@ impl, Cold: ItemStore> HotColdDB )); } + pub fn data_column_as_kv_store_ops( + &self, + block_root: &Hash256, + data_column: Arc>, + ops: &mut Vec, + ) { + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconDataColumn, + get_data_column_key(block_root, &data_column.index), + data_column.as_ssz_bytes(), + )); + } + + pub fn put_data_column_custody_info( + &self, + earliest_data_column_slot: Option, + ) -> Result<(), Error> { + let data_column_custody_info = DataColumnCustodyInfo { + earliest_data_column_slot, + }; + + self.blobs_db + .put(&DATA_COLUMN_CUSTODY_INFO_KEY, &data_column_custody_info)?; + + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_data_column_custody_info(Some(data_column_custody_info)) + }); + + Ok(()) + } + pub fn put_data_columns( &self, block_root: &Hash256, @@ -866,8 +1006,8 @@ impl, Cold: ItemStore> HotColdDB &data_column.as_ssz_bytes(), )?; self.block_cache - .lock() - .put_data_column(*block_root, data_column); + .as_ref() + .inspect(|cache| cache.lock().put_data_column(*block_root, data_column)); } Ok(()) } @@ -887,14 +1027,6 @@ impl, Cold: ItemStore> HotColdDB } } - pub fn put_state_summary( - &self, - state_root: &Hash256, - summary: HotStateSummary, - ) -> Result<(), Error> { - self.hot_db.put(state_root, &summary) - } - /// Store a state in the store. pub fn put_state(&self, state_root: &Hash256, state: &BeaconState) -> Result<(), Error> { let mut ops: Vec = Vec::new(); @@ -951,6 +1083,7 @@ impl, Cold: ItemStore> HotColdDB /// - `result_state_root == state.canonical_root()` /// - `state.slot() <= max_slot` /// - `state.get_latest_block_root(result_state_root) == block_root` + #[instrument(skip_all, fields(?block_root, %max_slot, ?state_root), level = "debug")] pub fn get_advanced_hot_state( &self, block_root: Hash256, @@ -986,7 +1119,14 @@ impl, Cold: ItemStore> HotColdDB }; // It's a bit redundant but we elect to cache the state here and down below. let mut opt_state = self - .load_hot_state(&state_root, true)? + .load_hot_state(&state_root, true) + .map_err(|e| { + Error::LoadingHotStateError( + format!("get advanced {block_root} {max_slot}"), + state_root, + e.into(), + ) + })? .map(|(state, _block_root)| (state_root, state)); if let Some((state_root, state)) = opt_state.as_mut() { @@ -1015,6 +1155,7 @@ impl, Cold: ItemStore> HotColdDB /// If this function returns `Some(state)` then that `state` will always have /// `latest_block_header` matching `block_root` but may not be advanced all the way through to /// `max_slot`. + #[instrument(skip_all, fields(?block_root, %max_slot), level = "debug")] pub fn get_advanced_hot_state_from_cache( &self, block_root: Hash256, @@ -1058,7 +1199,7 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - ) -> Result, Error> { + ) -> Result, Error> { HybridForwardsBlockRootsIterator::new( self, DBColumn::BeaconBlockRoots, @@ -1088,7 +1229,7 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - ) -> Result, Error> { + ) -> Result, Error> { HybridForwardsStateRootsIterator::new( self, DBColumn::BeaconStateRoots, @@ -1098,41 +1239,6 @@ impl, Cold: ItemStore> HotColdDB ) } - /// Load an epoch boundary state by using the hot state summary look-up. - /// - /// Will fall back to the cold DB if a hot state summary is not found. - /// - /// NOTE: only used in tests at the moment - pub fn load_epoch_boundary_state( - &self, - state_root: &Hash256, - ) -> Result>, Error> { - if let Some(HotStateSummary { - epoch_boundary_state_root, - .. - }) = self.load_hot_state_summary(state_root)? - { - // NOTE: minor inefficiency here because we load an unnecessary hot state summary - let (state, _) = self - .load_hot_state(&epoch_boundary_state_root, true)? - .ok_or(HotColdDBError::MissingEpochBoundaryState( - epoch_boundary_state_root, - *state_root, - ))?; - Ok(Some(state)) - } else { - // Try the cold DB - match self.load_cold_state_slot(state_root)? { - Some(state_slot) => { - let epoch_boundary_slot = - state_slot / E::slots_per_epoch() * E::slots_per_epoch(); - self.load_cold_state_by_slot(epoch_boundary_slot).map(Some) - } - None => Ok(None), - } - } - } - pub fn put_item(&self, key: &Hash256, item: &I) -> Result<(), Error> { self.hot_db.put(key, item) } @@ -1206,13 +1312,44 @@ impl, Cold: ItemStore> HotColdDB StoreOp::DeleteState(state_root, slot) => { // Delete the hot state summary. key_value_batch.push(KeyValueStoreOp::DeleteKey( - DBColumn::BeaconStateSummary, + DBColumn::BeaconStateHotSummary, state_root.as_slice().to_vec(), )); - if slot.is_none_or(|slot| slot % E::slots_per_epoch() == 0) { + // NOTE: `hot_storage_strategy` can error if there are states in the database + // prior to the `anchor_slot`. This can happen if checkpoint sync has been + // botched and left some states in the database prior to completing. + if let Some(slot) = slot + && let Ok(strategy) = self.hot_storage_strategy(slot) + { + match strategy { + StorageStrategy::Snapshot => { + // Full state stored in this position + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateHotSnapshot, + state_root.as_slice().to_vec(), + )); + } + StorageStrategy::DiffFrom(_) => { + // Diff stored in this position + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateHotDiff, + state_root.as_slice().to_vec(), + )); + } + StorageStrategy::ReplayFrom(_) => { + // Nothing else to delete + } + } + } else { + // NOTE(hdiff): Attempt to delete both snapshots and diffs if we don't know + // the slot. key_value_batch.push(KeyValueStoreOp::DeleteKey( - DBColumn::BeaconState, + DBColumn::BeaconStateHotSnapshot, + state_root.as_slice().to_vec(), + )); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateHotDiff, state_root.as_slice().to_vec(), )); } @@ -1309,7 +1446,7 @@ impl, Cold: ItemStore> HotColdDB // Update database whilst holding a lock on cache, to ensure that the cache updates // atomically with the database. - let mut guard = self.block_cache.lock(); + let guard = self.block_cache.as_ref().map(|cache| cache.lock()); let blob_cache_ops = blobs_ops.clone(); // Try to execute blobs store ops. @@ -1356,56 +1493,67 @@ impl, Cold: ItemStore> HotColdDB return Err(e); } - for op in hot_db_cache_ops { + // Delete from the state cache. + for op in &hot_db_cache_ops { match op { - StoreOp::PutBlock(block_root, block) => { - guard.put_block(block_root, (*block).clone()); - } - - StoreOp::PutBlobs(_, _) => (), - - StoreOp::PutDataColumns(_, _) => (), - - StoreOp::PutState(_, _) => (), - - StoreOp::PutStateSummary(_, _) => (), - StoreOp::DeleteBlock(block_root) => { - guard.delete_block(&block_root); - self.state_cache.lock().delete_block_states(&block_root); + self.state_cache.lock().delete_block_states(block_root); } - StoreOp::DeleteState(state_root, _) => { - self.state_cache.lock().delete_state(&state_root) + self.state_cache.lock().delete_state(state_root) } - - StoreOp::DeleteBlobs(_) => (), - - StoreOp::DeleteDataColumns(_, _) => (), - - StoreOp::DeleteExecutionPayload(_) => (), - - StoreOp::DeleteSyncCommitteeBranch(_) => (), - - StoreOp::KeyValueOp(_) => (), - } - } - - for op in blob_cache_ops { - match op { - StoreOp::PutBlobs(block_root, blobs) => { - guard.put_blobs(block_root, blobs); - } - - StoreOp::DeleteBlobs(block_root) => { - guard.delete_blobs(&block_root); - } - _ => (), } } - drop(guard); + // If the block cache is enabled, also delete from the block cache. + if let Some(mut guard) = guard { + for op in hot_db_cache_ops { + match op { + StoreOp::PutBlock(block_root, block) => { + guard.put_block(block_root, (*block).clone()); + } + + StoreOp::PutBlobs(_, _) => (), + + StoreOp::PutDataColumns(_, _) => (), + + StoreOp::PutState(_, _) => (), + + StoreOp::PutStateSummary(_, _) => (), + + StoreOp::DeleteBlock(block_root) => { + guard.delete_block(&block_root); + } + + StoreOp::DeleteState(_, _) => (), + + StoreOp::DeleteBlobs(_) => (), + + StoreOp::DeleteDataColumns(_, _) => (), + + StoreOp::DeleteExecutionPayload(_) => (), + + StoreOp::DeleteSyncCommitteeBranch(_) => (), + + StoreOp::KeyValueOp(_) => (), + } + } + + for op in blob_cache_ops { + match op { + StoreOp::PutBlobs(block_root, blobs) => { + guard.put_blobs(block_root, blobs); + } + + StoreOp::DeleteBlobs(block_root) => { + guard.delete_blobs(&block_root); + } + + _ => (), + } + } + } Ok(()) } @@ -1420,9 +1568,6 @@ impl, Cold: ItemStore> HotColdDB state: &BeaconState, ops: &mut Vec, ) -> Result<(), Error> { - // Avoid storing states in the database if they already exist in the state cache. - // The exception to this is the finalized state, which must exist in the cache before it - // is stored on disk. match self.state_cache.lock().put_state( *state_root, state.get_latest_block_root(*state_root), @@ -1443,28 +1588,127 @@ impl, Cold: ItemStore> HotColdDB state_slot = %state.slot(), "State already exists in state cache", ); - return Ok(()); + // NOTE: We used to return early here, but had some issues with states being + // in the cache but not on disk. Instead of relying on the cache we try loading + // the state summary below and rely on that instead. } - PutStateOutcome::Finalized => {} // Continue to store. + // Continue to store. + PutStateOutcome::Finalized | PutStateOutcome::PreFinalizedHDiffBuffer => {} } - // On the epoch boundary, store the full state. - if state.slot() % E::slots_per_epoch() == 0 { + // Computing diffs is expensive so we avoid it if we already have this state stored on + // disk. + if self.load_hot_state_summary(state_root)?.is_some() { debug!( slot = %state.slot(), ?state_root, - "Storing full state on epoch boundary" + "Skipping storage of state already in the DB" ); - store_full_state(state_root, state, ops)?; + return Ok(()); } + let summary = self.store_hot_state_summary(state_root, state, ops)?; + self.store_hot_state_diffs(state_root, state, ops)?; + + debug!( + ?state_root, + slot = %state.slot(), + storage_strategy = ?self.hot_storage_strategy(state.slot())?, + diff_base_state = %summary.diff_base_state, + previous_state_root = ?summary.previous_state_root, + "Storing hot state summary and diffs" + ); + + Ok(()) + } + + /// Store a post-finalization state efficiently in the hot database. + pub fn store_hot_state_summary( + &self, + state_root: &Hash256, + state: &BeaconState, + ops: &mut Vec, + ) -> Result { // Store a summary of the state. // We store one even for the epoch boundary states, as we may need their slots // when doing a look up by state root. - let hot_state_summary = HotStateSummary::new(state_root, state)?; - let op = hot_state_summary.as_kv_store_op(*state_root); - ops.push(op); + let hot_state_summary = HotStateSummary::new( + self, + *state_root, + state, + self.hot_storage_strategy(state.slot())?, + )?; + ops.push(hot_state_summary.as_kv_store_op(*state_root)); + Ok(hot_state_summary) + } + pub fn store_hot_state_diffs( + &self, + state_root: &Hash256, + state: &BeaconState, + ops: &mut Vec, + ) -> Result<(), Error> { + let slot = state.slot(); + let storage_strategy = self.hot_storage_strategy(slot)?; + match storage_strategy { + StorageStrategy::ReplayFrom(_) => { + // Already have persisted the state summary, don't persist anything else + } + StorageStrategy::Snapshot => { + self.store_hot_state_as_snapshot(state_root, state, ops)?; + } + StorageStrategy::DiffFrom(from_slot) => { + let from_root = get_ancestor_state_root(self, state, from_slot).map_err(|e| { + Error::StateSummaryIteratorError { + error: e, + from_state_root: *state_root, + from_state_slot: state.slot(), + target_slot: slot, + } + })?; + self.store_hot_state_as_diff(state_root, state, from_root, ops)?; + } + } + Ok(()) + } + + fn store_hot_state_as_diff( + &self, + state_root: &Hash256, + state: &BeaconState, + from_root: Hash256, + ops: &mut Vec, + ) -> Result<(), Error> { + let base_buffer = { + let _t = metrics::start_timer_vec( + &metrics::BEACON_HDIFF_BUFFER_LOAD_BEFORE_STORE_TIME, + HOT_METRIC, + ); + self.load_hot_hdiff_buffer(from_root).map_err(|e| { + Error::LoadingHotHdiffBufferError( + format!("store state as diff {state_root:?} {}", state.slot()), + from_root, + e.into(), + ) + })? + }; + let target_buffer = HDiffBuffer::from_state(state.clone()); + let diff = { + let _timer = metrics::start_timer_vec(&metrics::BEACON_HDIFF_COMPUTE_TIME, HOT_METRIC); + HDiff::compute(&base_buffer, &target_buffer, &self.config)? + }; + let diff_bytes = diff.as_ssz_bytes(); + let layer = HierarchyConfig::exponent_for_slot(state.slot()); + metrics::observe_vec( + &metrics::BEACON_HDIFF_SIZES, + &[&layer.to_string()], + diff_bytes.len() as f64, + ); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconStateHotDiff, + state_root.as_slice().to_vec(), + diff_bytes, + )); Ok(()) } @@ -1483,7 +1727,9 @@ impl, Cold: ItemStore> HotColdDB warn!(?state_root, "State cache missed"); } - let state_from_disk = self.load_hot_state(state_root, update_cache)?; + let state_from_disk = self.load_hot_state(state_root, update_cache).map_err(|e| { + Error::LoadingHotStateError("get state".to_owned(), *state_root, e.into()) + })?; if let Some((mut state, block_root)) = state_from_disk { state.update_tree_hash_cache()?; @@ -1516,6 +1762,88 @@ impl, Cold: ItemStore> HotColdDB } } + fn load_hot_hdiff_buffer(&self, state_root: Hash256) -> Result { + if let Some(buffer) = self + .state_cache + .lock() + .get_hdiff_buffer_by_state_root(state_root) + { + return Ok(buffer); + } + + let Some(HotStateSummary { + slot, + diff_base_state, + .. + }) = self.load_hot_state_summary(&state_root)? + else { + return Err(Error::MissingHotStateSummary(state_root)); + }; + + let buffer = match self.hot_storage_strategy(slot)? { + StorageStrategy::Snapshot => { + let Some(state) = self.load_hot_state_as_snapshot(state_root)? else { + let existing_snapshots = self.load_hot_state_snapshot_roots()?; + debug!( + requested = ?state_root, + existing_snapshots = ?existing_snapshots, + "Missing hot state snapshot" + ); + return Err(Error::MissingHotStateSnapshot(state_root, slot)); + }; + HDiffBuffer::from_state(state) + } + StorageStrategy::DiffFrom(from_slot) => { + let from_state_root = diff_base_state.get_root(from_slot)?; + let mut buffer = self.load_hot_hdiff_buffer(from_state_root).map_err(|e| { + Error::LoadingHotHdiffBufferError( + format!("load hdiff DiffFrom {from_slot} {state_root}"), + from_state_root, + e.into(), + ) + })?; + let diff = self.load_hot_hdiff(state_root)?; + { + let _timer = + metrics::start_timer_vec(&metrics::BEACON_HDIFF_APPLY_TIME, HOT_METRIC); + diff.apply(&mut buffer, &self.config)?; + } + buffer + } + StorageStrategy::ReplayFrom(from_slot) => { + let from_state_root = diff_base_state.get_root(from_slot)?; + self.load_hot_hdiff_buffer(from_state_root).map_err(|e| { + Error::LoadingHotHdiffBufferError( + format!("load hdiff ReplayFrom {from_slot} {state_root}"), + from_state_root, + e.into(), + ) + })? + } + }; + + // Add buffer to cache for future calls. + self.state_cache + .lock() + .put_hdiff_buffer(state_root, slot, &buffer); + + Ok(buffer) + } + + fn load_hot_hdiff(&self, state_root: Hash256) -> Result { + let bytes = { + let _t = metrics::start_timer_vec(&metrics::BEACON_HDIFF_READ_TIME, HOT_METRIC); + self.hot_db + .get_bytes(DBColumn::BeaconStateHotDiff, state_root.as_slice())? + .ok_or(HotColdDBError::MissingHotHDiff(state_root))? + }; + let hdiff = { + let _t = metrics::start_timer_vec(&metrics::BEACON_HDIFF_DECODE_TIME, HOT_METRIC); + HDiff::from_ssz_bytes(&bytes)? + }; + Ok(hdiff) + } + /// Load a post-finalization state from the hot database. /// /// Will replay blocks from the nearest epoch boundary. @@ -1532,64 +1860,64 @@ impl, Cold: ItemStore> HotColdDB if let Some(HotStateSummary { slot, latest_block_root, - epoch_boundary_state_root, + diff_base_state, + .. }) = self.load_hot_state_summary(state_root)? { - let mut boundary_state = - get_full_state(&self.hot_db, &epoch_boundary_state_root, &self.spec)?.ok_or( - HotColdDBError::MissingEpochBoundaryState( - epoch_boundary_state_root, - *state_root, - ), - )?; + let mut state = match self.hot_storage_strategy(slot)? { + strat @ StorageStrategy::Snapshot | strat @ StorageStrategy::DiffFrom(_) => { + let buffer_timer = metrics::start_timer_vec( + &metrics::BEACON_HDIFF_BUFFER_LOAD_TIME, + HOT_METRIC, + ); + let buffer = self.load_hot_hdiff_buffer(*state_root).map_err(|e| { + Error::LoadingHotHdiffBufferError( + format!("load state {strat:?} {slot}"), + *state_root, + e.into(), + ) + })?; + drop(buffer_timer); + let mut state = buffer.as_state(&self.spec)?; - // Immediately rebase the state from disk on the finalized state so that we can reuse - // parts of the tree for state root calculation in `replay_blocks`. - self.state_cache - .lock() - .rebase_on_finalized(&mut boundary_state, &self.spec)?; + // Immediately rebase the state from diffs on the finalized state so that we + // can utilise structural sharing and don't consume excess memory. + self.state_cache + .lock() + .rebase_on_finalized(&mut state, &self.spec)?; - // Optimization to avoid even *thinking* about replaying blocks if we're already - // on an epoch boundary. - let mut state = if slot % E::slots_per_epoch() == 0 { - boundary_state - } else { - // If replaying blocks, and `update_cache` is true, also cache the epoch boundary - // state that this state is based on. It may be useful as the basis of more states - // in the same epoch. - let state_cache_hook = |state_root, state: &mut BeaconState| { - if !update_cache || state.slot() % E::slots_per_epoch() != 0 { - return Ok(()); - } - // Ensure all caches are built before attempting to cache. - state.update_tree_hash_cache()?; - state.build_all_caches(&self.spec)?; + state + } + StorageStrategy::ReplayFrom(from_slot) => { + let from_state_root = diff_base_state.get_root(from_slot)?; - let latest_block_root = state.get_latest_block_root(state_root); - if let PutStateOutcome::New(_) = - self.state_cache - .lock() - .put_state(state_root, latest_block_root, state)? - { - debug!( - ?state_root, - state_slot = %state.slot(), - descendant_slot = %slot, - "Cached ancestor state", - ); - } - Ok(()) - }; - let blocks = - self.load_blocks_to_replay(boundary_state.slot(), slot, latest_block_root)?; - let _t = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_HOT_BLOCKS_TIME); - self.replay_blocks( - boundary_state, - blocks, - slot, - no_state_root_iter(), - Some(Box::new(state_cache_hook)), - )? + let (mut base_state, _) = self + .load_hot_state(&from_state_root, update_cache) + .map_err(|e| { + Error::LoadingHotStateError( + format!("load state ReplayFrom {from_slot}"), + *state_root, + e.into(), + ) + })? + .ok_or(HotColdDBError::MissingHotState { + state_root: from_state_root, + requested_by_state_summary: (*state_root, slot), + })?; + + // Immediately rebase the state from disk on the finalized state so that we can + // reuse parts of the tree for state root calculation in `replay_blocks`. + self.state_cache + .lock() + .rebase_on_finalized(&mut base_state, &self.spec)?; + + self.load_hot_state_using_replay( + base_state, + slot, + latest_block_root, + update_cache, + )? + } }; state.apply_pending_mutations()?; @@ -1599,6 +1927,56 @@ impl, Cold: ItemStore> HotColdDB } } + pub fn load_hot_state_using_replay( + &self, + base_state: BeaconState, + slot: Slot, + latest_block_root: Hash256, + update_cache: bool, + ) -> Result, Error> { + if base_state.slot() == slot { + return Ok(base_state); + } + + let blocks = self.load_blocks_to_replay(base_state.slot(), slot, latest_block_root)?; + let _t = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_HOT_BLOCKS_TIME); + + // If replaying blocks, and `update_cache` is true, also cache the epoch boundary + // state that this state is based on. It may be useful as the basis of more states + // in the same epoch. + let state_cache_hook = |state_root, state: &mut BeaconState| { + if !update_cache || state.slot() % E::slots_per_epoch() != 0 { + return Ok(()); + } + // Ensure all caches are built before attempting to cache. + state.update_tree_hash_cache()?; + state.build_all_caches(&self.spec)?; + + let latest_block_root = state.get_latest_block_root(state_root); + if let PutStateOutcome::New(_) = + self.state_cache + .lock() + .put_state(state_root, latest_block_root, state)? + { + debug!( + ?state_root, + state_slot = %state.slot(), + descendant_slot = %slot, + "Cached ancestor state", + ); + } + Ok(()) + }; + + self.replay_blocks( + base_state, + blocks, + slot, + no_state_root_iter(), + Some(Box::new(state_cache_hook)), + ) + } + pub fn store_cold_state_summary( &self, state_root: &Hash256, @@ -1624,7 +2002,7 @@ impl, Cold: ItemStore> HotColdDB self.store_cold_state_summary(state_root, state.slot(), ops)?; let slot = state.slot(); - match self.hierarchy.storage_strategy(slot)? { + match self.cold_storage_strategy(slot)? { StorageStrategy::ReplayFrom(from) => { debug!( strategy = "replay", @@ -1699,6 +2077,54 @@ impl, Cold: ItemStore> HotColdDB } } + pub fn store_hot_state_as_snapshot( + &self, + state_root: &Hash256, + state: &BeaconState, + ops: &mut Vec, + ) -> Result<(), Error> { + let bytes = state.as_ssz_bytes(); + let compressed_value = { + let _timer = metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_COMPRESS_TIME); + let mut out = Vec::with_capacity(self.config.estimate_compressed_size(bytes.len())); + let mut encoder = Encoder::new(&mut out, self.config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + out + }; + + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconStateHotSnapshot, + state_root.as_slice().to_vec(), + compressed_value, + )); + Ok(()) + } + + fn load_hot_state_bytes_as_snapshot( + &self, + state_root: Hash256, + ) -> Result>, Error> { + match self + .hot_db + .get_bytes(DBColumn::BeaconStateHotSnapshot, state_root.as_slice())? + { + Some(bytes) => { + let _timer = + metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME); + let mut ssz_bytes = + Vec::with_capacity(self.config.estimate_decompressed_size(bytes.len())); + let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut ssz_bytes) + .map_err(Error::Compression)?; + Ok(Some(ssz_bytes)) + } + None => Ok(None), + } + } + fn load_cold_state_as_snapshot(&self, slot: Slot) -> Result>, Error> { Ok(self .load_cold_state_bytes_as_snapshot(slot)? @@ -1706,6 +2132,22 @@ impl, Cold: ItemStore> HotColdDB .transpose()?) } + fn load_hot_state_as_snapshot( + &self, + state_root: Hash256, + ) -> Result>, Error> { + Ok(self + .load_hot_state_bytes_as_snapshot(state_root)? + .map(|bytes| BeaconState::from_ssz_bytes(&bytes, &self.spec)) + .transpose()?) + } + + fn load_hot_state_snapshot_roots(&self) -> Result, Error> { + self.hot_db + .iter_column_keys::(DBColumn::BeaconStateHotSnapshot) + .collect() + } + pub fn store_cold_state_as_diff( &self, state: &BeaconState, @@ -1714,15 +2156,24 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { // Load diff base state bytes. let (_, base_buffer) = { - let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME); + let _t = metrics::start_timer_vec( + &metrics::BEACON_HDIFF_BUFFER_LOAD_BEFORE_STORE_TIME, + COLD_METRIC, + ); self.load_hdiff_buffer_for_slot(from_slot)? }; let target_buffer = HDiffBuffer::from_state(state.clone()); let diff = { - let _timer = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME); + let _timer = metrics::start_timer_vec(&metrics::BEACON_HDIFF_COMPUTE_TIME, COLD_METRIC); HDiff::compute(&base_buffer, &target_buffer, &self.config)? }; let diff_bytes = diff.as_ssz_bytes(); + let layer = HierarchyConfig::exponent_for_slot(state.slot()); + metrics::observe_vec( + &metrics::BEACON_HDIFF_SIZES, + &[&layer.to_string()], + diff_bytes.len() as f64, + ); ops.push(KeyValueStoreOp::PutKeyValue( DBColumn::BeaconStateDiff, @@ -1746,7 +2197,7 @@ impl, Cold: ItemStore> HotColdDB /// /// Will reconstruct the state if it lies between restore points. pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result, Error> { - let storage_strategy = self.hierarchy.storage_strategy(slot)?; + let storage_strategy = self.cold_storage_strategy(slot)?; // Search for a state from this slot or a recent prior slot in the historic state cache. let mut historic_state_cache = self.historic_state_cache.lock(); @@ -1775,10 +2226,10 @@ impl, Cold: ItemStore> HotColdDB // Load using the diff hierarchy. For states that require replay we recurse into this // function so that we can try to get their pre-state *as a state* rather than an hdiff // buffer. - match self.hierarchy.storage_strategy(slot)? { + match self.cold_storage_strategy(slot)? { StorageStrategy::Snapshot | StorageStrategy::DiffFrom(_) => { let buffer_timer = - metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME); + metrics::start_timer_vec(&metrics::BEACON_HDIFF_BUFFER_LOAD_TIME, COLD_METRIC); let (_, buffer) = self.load_hdiff_buffer_for_slot(slot)?; drop(buffer_timer); let state = buffer.as_state(&self.spec)?; @@ -1847,13 +2298,13 @@ impl, Cold: ItemStore> HotColdDB fn load_hdiff_for_slot(&self, slot: Slot) -> Result { let bytes = { - let _t = metrics::start_timer(&metrics::BEACON_HDIFF_READ_TIMES); + let _t = metrics::start_timer_vec(&metrics::BEACON_HDIFF_READ_TIME, COLD_METRIC); self.cold_db .get_bytes(DBColumn::BeaconStateDiff, &slot.as_u64().to_be_bytes())? .ok_or(HotColdDBError::MissingHDiff(slot))? }; let hdiff = { - let _t = metrics::start_timer(&metrics::BEACON_HDIFF_DECODE_TIMES); + let _t = metrics::start_timer_vec(&metrics::BEACON_HDIFF_DECODE_TIME, COLD_METRIC); HDiff::from_ssz_bytes(&bytes)? }; Ok(hdiff) @@ -1867,15 +2318,15 @@ impl, Cold: ItemStore> HotColdDB %slot, "Hit hdiff buffer cache" ); - metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT); + metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT, COLD_METRIC); return Ok((slot, buffer)); } - metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); + metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS, COLD_METRIC); // Load buffer for the previous state. // This amount of recursion (<10 levels) should be OK. let t = std::time::Instant::now(); - match self.hierarchy.storage_strategy(slot)? { + match self.cold_storage_strategy(slot)? { // Base case. StorageStrategy::Snapshot => { let state = self @@ -1904,7 +2355,7 @@ impl, Cold: ItemStore> HotColdDB let diff = self.load_hdiff_for_slot(slot)?; { let _timer = - metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_APPLY_TIME); + metrics::start_timer_vec(&metrics::BEACON_HDIFF_APPLY_TIME, COLD_METRIC); diff.apply(&mut buffer, &self.config)?; } @@ -2028,6 +2479,29 @@ impl, Cold: ItemStore> HotColdDB }) } + /// Fetch custody info from the cache. + /// If custody info doesn't exist in the cache, + /// try to fetch from the DB and prime the cache. + pub fn get_data_column_custody_info(&self) -> Result, Error> { + if let Some(cache) = &self.block_cache + && let Some(data_column_custody_info) = cache.lock().get_data_column_custody_info() + { + return Ok(Some(data_column_custody_info)); + } + let data_column_custody_info = self + .blobs_db + .get::(&DATA_COLUMN_CUSTODY_INFO_KEY)?; + + // Update the cache + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_data_column_custody_info(data_column_custody_info.clone()) + }); + + Ok(data_column_custody_info) + } + /// Fetch all columns for a given block from the store. pub fn get_data_columns( &self, @@ -2046,9 +2520,13 @@ impl, Cold: ItemStore> HotColdDB /// Fetch blobs for a given block from the store. pub fn get_blobs(&self, block_root: &Hash256) -> Result, Error> { // Check the cache. - if let Some(blobs) = self.block_cache.lock().get_blobs(block_root) { + if let Some(blobs) = self + .block_cache + .as_ref() + .and_then(|cache| cache.lock().get_blobs(block_root).cloned()) + { metrics::inc_counter(&metrics::BEACON_BLOBS_CACHE_HIT_COUNT); - return Ok(blobs.clone().into()); + return Ok(blobs.into()); } match self @@ -2065,10 +2543,10 @@ impl, Cold: ItemStore> HotColdDB .first() .map(|blob| self.spec.max_blobs_per_block(blob.epoch())) { - let blobs = BlobSidecarList::from_vec(blobs, max_blobs_per_block as usize); + let blobs = BlobSidecarList::new(blobs, max_blobs_per_block as usize)?; self.block_cache - .lock() - .put_blobs(*block_root, blobs.clone()); + .as_ref() + .inspect(|cache| cache.lock().put_blobs(*block_root, blobs.clone())); Ok(BlobSidecarListFromRoot::Blobs(blobs)) } else { @@ -2092,6 +2570,16 @@ impl, Cold: ItemStore> HotColdDB .collect() } + /// Fetch all possible data column keys for a given `block_root`. + /// + /// Unlike `get_data_column_keys`, these keys are not necessarily all present in the database, + /// due to the node's custody requirements many just store a subset. + pub fn get_all_data_column_keys(&self, block_root: Hash256) -> Vec> { + (0..E::number_of_columns() as u64) + .map(|column_index| get_data_column_key(&block_root, &column_index)) + .collect() + } + /// Fetch a single data_column for a given block from the store. pub fn get_data_column( &self, @@ -2101,8 +2589,8 @@ impl, Cold: ItemStore> HotColdDB // Check the cache. if let Some(data_column) = self .block_cache - .lock() - .get_data_column(block_root, column_index) + .as_ref() + .and_then(|cache| cache.lock().get_data_column(block_root, column_index)) { metrics::inc_counter(&metrics::BEACON_DATA_COLUMNS_CACHE_HIT_COUNT); return Ok(Some(data_column)); @@ -2114,9 +2602,11 @@ impl, Cold: ItemStore> HotColdDB )? { Some(ref data_column_bytes) => { let data_column = Arc::new(DataColumnSidecar::from_ssz_bytes(data_column_bytes)?); - self.block_cache - .lock() - .put_data_column(*block_root, data_column.clone()); + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_data_column(*block_root, data_column.clone()) + }); Ok(Some(data_column)) } None => Ok(None), @@ -2176,11 +2666,11 @@ impl, Cold: ItemStore> HotColdDB /// Initialise the anchor info for checkpoint sync starting from `block`. pub fn init_anchor_info( &self, - block: BeaconBlockRef<'_, E>, + oldest_block_parent: Hash256, + oldest_block_slot: Slot, + anchor_slot: Slot, retain_historic_states: bool, ) -> Result { - let anchor_slot = block.slot(); - // Set the `state_upper_limit` to the slot of the *next* checkpoint. let next_snapshot_slot = self.hierarchy.next_snapshot_slot(anchor_slot)?; let state_upper_limit = if !retain_historic_states { @@ -2188,17 +2678,12 @@ impl, Cold: ItemStore> HotColdDB } else { next_snapshot_slot }; - let anchor_info = if state_upper_limit == 0 && anchor_slot == 0 { - // Genesis archive node: no anchor because we *will* store all states. - ANCHOR_FOR_ARCHIVE_NODE - } else { - AnchorInfo { - anchor_slot, - oldest_block_slot: anchor_slot, - oldest_block_parent: block.parent_root(), - state_upper_limit, - state_lower_limit: self.spec.genesis_slot, - } + let anchor_info = AnchorInfo { + anchor_slot, + oldest_block_slot, + oldest_block_parent, + state_upper_limit, + state_lower_limit: self.spec.genesis_slot, }; self.compare_and_set_anchor_info(ANCHOR_UNINITIALIZED, anchor_info) } @@ -2245,7 +2730,8 @@ impl, Cold: ItemStore> HotColdDB /// Load the anchor info from disk. fn load_anchor_info(hot_db: &Hot) -> Result { Ok(hot_db - .get(&ANCHOR_INFO_KEY)? + .get(&ANCHOR_INFO_KEY) + .map_err(|e| Error::LoadAnchorInfo(e.into()))? .unwrap_or(ANCHOR_UNINITIALIZED)) } @@ -2328,7 +2814,9 @@ impl, Cold: ItemStore> HotColdDB /// Load the blob info from disk, but do not set `self.blob_info`. fn load_blob_info(&self) -> Result, Error> { - self.hot_db.get(&BLOB_INFO_KEY) + self.hot_db + .get(&BLOB_INFO_KEY) + .map_err(|e| Error::LoadBlobInfo(e.into())) } /// Store the given `blob_info` to disk. @@ -2373,7 +2861,9 @@ impl, Cold: ItemStore> HotColdDB /// Load the blob info from disk, but do not set `self.data_column_info`. fn load_data_column_info(&self) -> Result, Error> { - self.hot_db.get(&DATA_COLUMN_INFO_KEY) + self.hot_db + .get(&DATA_COLUMN_INFO_KEY) + .map_err(|e| Error::LoadDataColumnInfo(e.into())) } /// Store the given `data_column_info` to disk. @@ -2432,7 +2922,9 @@ impl, Cold: ItemStore> HotColdDB /// Load previously-stored config from disk. fn load_config(&self) -> Result, Error> { - self.hot_db.get(&CONFIG_KEY) + self.hot_db + .get(&CONFIG_KEY) + .map_err(|e| Error::LoadConfig(e.into())) } /// Write the config to disk. @@ -2442,18 +2934,24 @@ impl, Cold: ItemStore> HotColdDB /// Load the split point from disk, sans block root. fn load_split_partial(&self) -> Result, Error> { - self.hot_db.get(&SPLIT_KEY) + self.hot_db + .get(&SPLIT_KEY) + .map_err(|e| Error::LoadSplit(e.into())) } /// Load the split point from disk, including block root. fn load_split(&self) -> Result, Error> { match self.load_split_partial()? { Some(mut split) => { + debug!(?split, "Loaded split partial"); // Load the hot state summary to get the block root. - let summary = self.load_hot_state_summary(&split.state_root)?.ok_or( - HotColdDBError::MissingSplitState(split.state_root, split.slot), - )?; - split.block_root = summary.latest_block_root; + let latest_block_root = self + .load_block_root_from_summary_any_version(&split.state_root) + .ok_or(HotColdDBError::MissingSplitState( + split.state_root, + split.slot, + ))?; + split.block_root = latest_block_root; Ok(Some(split)) } None => Ok(None), @@ -2478,13 +2976,41 @@ impl, Cold: ItemStore> HotColdDB &self, state_root: &Hash256, ) -> Result, Error> { - self.hot_db.get(state_root) + self.hot_db + .get(state_root) + .map_err(|e| Error::LoadHotStateSummary(*state_root, e.into())) + } + + /// Load a hot state's summary in V22 format, given its root. + pub fn load_hot_state_summary_v22( + &self, + state_root: &Hash256, + ) -> Result, Error> { + self.hot_db + .get(state_root) + .map_err(|e| Error::LoadHotStateSummary(*state_root, e.into())) + } + + /// Load the latest block root for a hot state summary either in modern form, or V22 form. + /// + /// This function is required to open a V22 database for migration to V24, or vice versa. + pub fn load_block_root_from_summary_any_version( + &self, + state_root: &Hash256, + ) -> Option { + if let Ok(Some(summary)) = self.load_hot_state_summary(state_root) { + return Some(summary.latest_block_root); + } + if let Ok(Some(summary)) = self.load_hot_state_summary_v22(state_root) { + return Some(summary.latest_block_root); + } + None } /// Load all hot state summaries present in the hot DB pub fn load_hot_state_summaries(&self) -> Result, Error> { self.hot_db - .iter_column::(DBColumn::BeaconStateSummary) + .iter_column::(DBColumn::BeaconStateHotSummary) .map(|res| { let (state_root, value) = res?; let summary = HotStateSummary::from_ssz_bytes(&value)?; @@ -2501,27 +3027,13 @@ impl, Cold: ItemStore> HotColdDB /// Run a compaction pass on the freezer DB to free up space used by deleted states. pub fn compact_freezer(&self) -> Result<(), Error> { - let current_schema_columns = vec![ + let columns = vec![ DBColumn::BeaconColdStateSummary, DBColumn::BeaconStateSnapshot, DBColumn::BeaconStateDiff, DBColumn::BeaconStateRoots, ]; - // We can remove this once schema V21 has been gone for a while. - let previous_schema_columns = vec![ - DBColumn::BeaconState, - DBColumn::BeaconStateSummary, - DBColumn::BeaconBlockRootsChunked, - DBColumn::BeaconStateRootsChunked, - DBColumn::BeaconRestorePoint, - DBColumn::BeaconHistoricalRoots, - DBColumn::BeaconRandaoMixes, - DBColumn::BeaconHistoricalSummaries, - ]; - let mut columns = current_schema_columns; - columns.extend(previous_schema_columns); - for column in columns { info!(?column, "Starting compaction"); self.cold_db.compact_column(column)?; @@ -2535,25 +3047,6 @@ impl, Cold: ItemStore> HotColdDB self.config.compact_on_prune } - /// Load the checkpoint to begin pruning from (the "old finalized checkpoint"). - pub fn load_pruning_checkpoint(&self) -> Result, Error> { - Ok(self - .hot_db - .get(&PRUNING_CHECKPOINT_KEY)? - .map(|pc: PruningCheckpoint| pc.checkpoint)) - } - - /// Store the checkpoint to begin pruning from (the "old finalized checkpoint"). - pub fn store_pruning_checkpoint(&self, checkpoint: Checkpoint) -> Result<(), Error> { - self.hot_db - .do_atomically(vec![self.pruning_checkpoint_store_op(checkpoint)]) - } - - /// Create a staged store for the pruning checkpoint. - pub fn pruning_checkpoint_store_op(&self, checkpoint: Checkpoint) -> KeyValueStoreOp { - PruningCheckpoint { checkpoint }.as_kv_store_op(PRUNING_CHECKPOINT_KEY) - } - /// Load the timestamp of the last compaction as a `Duration` since the UNIX epoch. pub fn load_compaction_timestamp(&self) -> Result, Error> { Ok(self @@ -2590,6 +3083,30 @@ impl, Cold: ItemStore> HotColdDB Ok(ops) } + /// Return a single block root from the cold DB. + /// + /// If the slot is unavailable due to partial block history, `Ok(None)` will be returned. + pub fn get_cold_block_root(&self, slot: Slot) -> Result, Error> { + Ok(self + .cold_db + .get_bytes(DBColumn::BeaconBlockRoots, &slot.as_u64().to_be_bytes())? + .map(|bytes| Hash256::from_ssz_bytes(&bytes)) + .transpose()?) + } + + /// Return a single state root from the cold DB. + /// + /// If the slot is unavailable due to partial state history, `Ok(None)` will be returned. + /// + /// This function will usually only work on an archive node. + pub fn get_cold_state_root(&self, slot: Slot) -> Result, Error> { + Ok(self + .cold_db + .get_bytes(DBColumn::BeaconStateRoots, &slot.as_u64().to_be_bytes())? + .map(|bytes| Hash256::from_ssz_bytes(&bytes)) + .transpose()?) + } + /// Try to prune all execution payloads, returning early if there is no need to prune. pub fn try_prune_execution_payloads(&self, force: bool) -> Result<(), Error> { let split = self.get_split_info(); @@ -2686,29 +3203,29 @@ impl, Cold: ItemStore> HotColdDB /// Try to prune blobs, approximating the current epoch from the split slot. pub fn try_prune_most_blobs(&self, force: bool) -> Result<(), Error> { - let Some(deneb_fork_epoch) = self.spec.deneb_fork_epoch else { - debug!("Deneb fork is disabled"); - return Ok(()); - }; // The current epoch is >= split_epoch + 2. It could be greater if the database is // configured to delay updating the split or finalization has ceased. In this instance we // choose to also delay the pruning of blobs (we never prune without finalization anyway). let min_current_epoch = self.get_split_slot().epoch(E::slots_per_epoch()) + 2; - let min_data_availability_boundary = std::cmp::max( - deneb_fork_epoch, - min_current_epoch.saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), - ); + let Some(min_data_availability_boundary) = self + .spec + .min_epoch_data_availability_boundary(min_current_epoch) + else { + debug!("Deneb fork is disabled"); + return Ok(()); + }; self.try_prune_blobs(force, min_data_availability_boundary) } - /// Try to prune blobs older than the data availability boundary. + /// Try to prune blobs and data columns older than the data availability boundary. /// /// Blobs from the epoch `data_availability_boundary - blob_prune_margin_epochs` are retained. /// This epoch is an _exclusive_ endpoint for the pruning process. /// - /// This function only supports pruning blobs older than the split point, which is older than - /// (or equal to) finalization. Pruning blobs newer than finalization is not supported. + /// This function only supports pruning blobs and data columns older than the split point, + /// which is older than (or equal to) finalization. Pruning blobs and data columns newer than + /// finalization is not supported. /// /// This function also assumes that the split is stationary while it runs. It should only be /// run from the migrator thread (where `migrate_database` runs) or the database manager. @@ -2732,18 +3249,20 @@ impl, Cold: ItemStore> HotColdDB } let blob_info = self.get_blob_info(); + let data_column_info = self.get_data_column_info(); let Some(oldest_blob_slot) = blob_info.oldest_blob_slot else { error!("Slot of oldest blob is not known"); return Err(HotColdDBError::BlobPruneLogicError.into()); }; - // Start pruning from the epoch of the oldest blob stored. - // The start epoch is inclusive (blobs in this epoch will be pruned). + // The start epoch is not necessarily iterated back to, but is used for deciding whether we + // should attempt pruning. We could probably refactor it out eventually (while reducing our + // dependence on BlobInfo). let start_epoch = oldest_blob_slot.epoch(E::slots_per_epoch()); // Prune blobs up until the `data_availability_boundary - margin` or the split // slot's epoch, whichever is older. We can't prune blobs newer than the split. - // The end epoch is also inclusive (blobs in this epoch will be pruned). + // The end epoch is inclusive (blobs in this epoch will be pruned). let split = self.get_split_info(); let end_epoch = std::cmp::min( data_availability_boundary - margin_epochs - 1, @@ -2766,20 +3285,30 @@ impl, Cold: ItemStore> HotColdDB return Ok(()); } - // Sanity checks. - let anchor = self.get_anchor_info(); - if oldest_blob_slot < anchor.oldest_block_slot { - error!( - %oldest_blob_slot, - oldest_block_slot = %anchor.oldest_block_slot, - "Oldest blob is older than oldest block" + // Iterate blocks backwards from the `end_epoch` (usually the data availability boundary). + let Some((end_block_root, _)) = self + .forwards_block_roots_iterator_until(end_slot, end_slot, || { + self.get_hot_state(&split.state_root, true)? + .ok_or(HotColdDBError::MissingSplitState( + split.state_root, + split.slot, + )) + .map(|state| (state, split.state_root)) + .map_err(Into::into) + })? + .next() + .transpose()? + else { + // Can't prune blobs if we don't know the block at `end_slot`. This is expected if we + // have checkpoint synced and haven't backfilled to the DA boundary yet. + debug!( + %end_epoch, + %data_availability_boundary, + "No blobs to prune" ); - return Err(HotColdDBError::BlobPruneLogicError.into()); - } - - // Iterate block roots forwards from the oldest blob slot. + return Ok(()); + }; debug!( - %start_epoch, %end_epoch, %data_availability_boundary, "Pruning blobs" @@ -2788,55 +3317,78 @@ impl, Cold: ItemStore> HotColdDB // We collect block roots of deleted blobs in memory. Even for 10y of blob history this // vec won't go beyond 1GB. We can probably optimise this out eventually. let mut removed_block_roots = vec![]; + let mut blobs_db_ops = vec![]; - let remove_blob_if = |blobs_bytes: &[u8]| { - let blobs = Vec::from_ssz_bytes(blobs_bytes)?; - let Some(blob): Option<&Arc>> = blobs.first() else { - return Ok(false); + // Iterate blocks backwards until we reach a block for which we've already pruned + // blobs/columns. + for tuple in ParentRootBlockIterator::new(self, end_block_root) { + let (block_root, blinded_block) = tuple?; + let slot = blinded_block.slot(); + + // If the block has no blobs we can't tell if they've been pruned, and there is nothing + // to prune, so we just skip. + if !blinded_block.message().body().has_blobs() { + continue; + } + + // Check if we have blobs or columns stored. If not, we assume pruning has already + // reached this point. + let (db_column, db_keys) = if blinded_block.fork_name_unchecked().fulu_enabled() { + ( + DBColumn::BeaconDataColumn, + self.get_all_data_column_keys(block_root), + ) + } else { + (DBColumn::BeaconBlob, vec![block_root.as_slice().to_vec()]) }; - if blob.slot() <= end_slot { - // Store the block root so we can delete from the blob cache - removed_block_roots.push(blob.block_root()); - // Delete from the on-disk db - return Ok(true); - }; - Ok(false) - }; + // For data columns, consider a block pruned if ALL column indices are absent. + // In future we might want to refactor this to read the data column indices that *exist* + // from the DB, which could be slightly more efficient than checking existence for every + // possible column. + let mut data_stored_for_block = false; + for db_key in db_keys { + if self.blobs_db.key_exists(db_column, &db_key)? { + data_stored_for_block = true; + blobs_db_ops.push(KeyValueStoreOp::DeleteKey(db_column, db_key)); + } + } - self.blobs_db - .delete_if(DBColumn::BeaconBlob, remove_blob_if)?; - - if self.spec.is_peer_das_enabled_for_epoch(start_epoch) { - let remove_data_column_if = |blobs_bytes: &[u8]| { - let data_column: DataColumnSidecar = - DataColumnSidecar::from_ssz_bytes(blobs_bytes)?; - - if data_column.slot() <= end_slot { - return Ok(true); - }; - - Ok(false) - }; - - self.blobs_db - .delete_if(DBColumn::BeaconDataColumn, remove_data_column_if)?; + if data_stored_for_block { + debug!( + ?block_root, + %slot, + "Pruning blobs or columns for block" + ); + removed_block_roots.push(block_root); + } else { + debug!( + %slot, + ?block_root, + "Reached slot with blobs or columns already pruned" + ); + break; + } } // Remove deleted blobs from the cache. - let mut block_cache = self.block_cache.lock(); - for block_root in removed_block_roots { - block_cache.delete_blobs(&block_root); + if let Some(mut block_cache) = self.block_cache.as_ref().map(|cache| cache.lock()) { + for block_root in removed_block_roots { + block_cache.delete_blobs(&block_root); + block_cache.delete_data_columns(&block_root); + } } - drop(block_cache); - let new_blob_info = BlobInfo { - oldest_blob_slot: Some(end_slot + 1), - blobs_db: blob_info.blobs_db, - }; + // Remove from disk. + if !blobs_db_ops.is_empty() { + debug!( + num_deleted = blobs_db_ops.len(), + "Deleting blobs and data columns from disk" + ); + self.blobs_db.do_atomically(blobs_db_ops)?; + } - let op = self.compare_and_set_blob_info(blob_info, new_blob_info)?; - self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::KeyValueOp(op)])?; + self.update_blob_or_data_column_info(start_epoch, end_slot, blob_info, data_column_info)?; debug!("Blob pruning complete"); @@ -2871,32 +3423,13 @@ impl, Cold: ItemStore> HotColdDB // migrating to the tree-states schema (delete everything in the freezer then start afresh). let mut cold_ops = vec![]; - let current_schema_columns = vec![ + let columns = vec![ DBColumn::BeaconColdStateSummary, DBColumn::BeaconStateSnapshot, DBColumn::BeaconStateDiff, DBColumn::BeaconStateRoots, ]; - // This function is intended to be able to clean up leftover V21 freezer database stuff in - // the case where the V22 schema upgrade failed *after* commiting the version increment but - // *before* cleaning up the freezer DB. - // - // We can remove this once schema V21 has been gone for a while. - let previous_schema_columns = vec![ - DBColumn::BeaconState, - DBColumn::BeaconStateSummary, - DBColumn::BeaconBlockRootsChunked, - DBColumn::BeaconStateRootsChunked, - DBColumn::BeaconRestorePoint, - DBColumn::BeaconHistoricalRoots, - DBColumn::BeaconRandaoMixes, - DBColumn::BeaconHistoricalSummaries, - ]; - - let mut columns = current_schema_columns; - columns.extend(previous_schema_columns); - for column in columns { for res in self.cold_db.iter_column_keys::>(column) { let key = res?; @@ -2922,6 +3455,31 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } + + fn update_blob_or_data_column_info( + &self, + start_epoch: Epoch, + end_slot: Slot, + blob_info: BlobInfo, + data_column_info: DataColumnInfo, + ) -> Result<(), Error> { + let op = if self.spec.is_peer_das_enabled_for_epoch(start_epoch) { + let new_data_column_info = DataColumnInfo { + oldest_data_column_slot: Some(end_slot + 1), + }; + self.compare_and_set_data_column_info(data_column_info, new_data_column_info)? + } else { + let new_blob_info = BlobInfo { + oldest_blob_slot: Some(end_slot + 1), + blobs_db: blob_info.blobs_db, + }; + self.compare_and_set_blob_info(blob_info, new_blob_info)? + }; + + self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::KeyValueOp(op)])?; + + Ok(()) + } } /// Advance the split point of the store, copying new finalized states to the freezer. @@ -2934,7 +3492,7 @@ pub fn migrate_database, Cold: ItemStore>( finalized_state_root: Hash256, finalized_block_root: Hash256, finalized_state: &BeaconState, -) -> Result<(), Error> { +) -> Result { debug!( slot = %finalized_state.slot(), "Freezer migration started" @@ -2943,12 +3501,12 @@ pub fn migrate_database, Cold: ItemStore>( // 0. Check that the migration is sensible. // The new finalized state must increase the current split slot, and lie on an epoch // boundary (in order for the hot state summary scheme to work). - let current_split_slot = store.split.read_recursive().slot; + let current_split = *store.split.read_recursive(); let anchor_info = store.anchor_info.read_recursive().clone(); - if finalized_state.slot() < current_split_slot { + if finalized_state.slot() < current_split.slot { return Err(HotColdDBError::FreezeSlotError { - current_split_slot, + current_split_slot: current_split.slot, proposed_split_slot: finalized_state.slot(), } .into()); @@ -2965,7 +3523,7 @@ pub fn migrate_database, Cold: ItemStore>( // Iterate in descending order until the current split slot let state_roots: Vec<_> = process_results(RootsIterator::new(&store, finalized_state), |iter| { - iter.take_while(|(_, _, slot)| *slot >= current_split_slot) + iter.take_while(|(_, _, slot)| *slot >= current_split.slot) .collect() })?; @@ -2990,7 +3548,7 @@ pub fn migrate_database, Cold: ItemStore>( // Only store the cold state if it's on a diff boundary. // Calling `store_cold_state_summary` instead of `store_cold_state` for those allows us // to skip loading many hot states. - if let StorageStrategy::ReplayFrom(from) = store.hierarchy.storage_strategy(slot)? { + if let StorageStrategy::ReplayFrom(from) = store.cold_storage_strategy(slot)? { // Store slot -> state_root and state_root -> slot mappings. debug!( strategy = "replay", @@ -3024,40 +3582,41 @@ pub fn migrate_database, Cold: ItemStore>( // in the worst case we will restart with the old split and re-run the migration. store.cold_db.do_atomically(cold_db_block_ops)?; store.cold_db.sync()?; - { + let new_split = { let mut split_guard = store.split.write(); - let latest_split_slot = split_guard.slot; + let latest_split = *split_guard; // Detect a situation where the split point is (erroneously) changed from more than one // place in code. - if latest_split_slot != current_split_slot { + if latest_split.slot != current_split.slot { error!( - previous_split_slot = %current_split_slot, - current_split_slot = %latest_split_slot, + previous_split_slot = %current_split.slot, + current_split_slot = %latest_split.slot, "Race condition detected: Split point changed while copying states to the freezer" ); // Assume the freezing procedure will be retried in case this happens. return Err(Error::SplitPointModified( - current_split_slot, - latest_split_slot, + current_split.slot, + latest_split.slot, )); } // Before updating the in-memory split value, we flush it to disk first, so that should the // OS process die at this point, we pick up from the right place after a restart. - let split = Split { + let new_split = Split { slot: finalized_state.slot(), state_root: finalized_state_root, block_root: finalized_block_root, }; - store.hot_db.put_sync(&SPLIT_KEY, &split)?; + store.hot_db.put_sync(&SPLIT_KEY, &new_split)?; // Split point is now persisted in the hot database on disk. The in-memory split point // hasn't been modified elsewhere since we keep a write lock on it. It's safe to update // the in-memory split point now. - *split_guard = split; - } + *split_guard = new_split; + new_split + }; // Update the cache's view of the finalized state. store.update_finalized_state( @@ -3071,7 +3630,16 @@ pub fn migrate_database, Cold: ItemStore>( "Freezer migration complete" ); - Ok(()) + Ok(SplitChange { + previous: current_split, + new: new_split, + }) +} + +#[derive(Debug)] +pub struct SplitChange { + pub previous: Split, + pub new: Split, } /// Struct for storing the split slot and state root in the database. @@ -3108,19 +3676,221 @@ fn no_state_root_iter() -> Option), + LoadStateRootError(Box), + MissingStateRoot { + target_slot: Slot, + state_upper_limit: Slot, + }, + OutOfBoundsInitialSlot, +} + +/// Return the ancestor state root of a state beyond SlotsPerHistoricalRoot using the roots iterator +/// and the store +pub fn get_ancestor_state_root<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore>( + store: &'a HotColdDB, + from_state: &'a BeaconState, + target_slot: Slot, +) -> Result { + // Use the state itself for recent roots + if let Ok(target_state_root) = from_state.get_state_root(target_slot) { + return Ok(*target_state_root); + } + + // Fetch the anchor info prior to obtaining the split lock. We don't need to hold a lock because + // the `state_upper_limit` can't increase (and rug us) unless state pruning runs, and it never + // runs concurrently. + let state_upper_limit = store.get_anchor_info().state_upper_limit; + + // Hold the split lock so that state summaries are not pruned concurrently with this function + // running. + let split = store.split.read_recursive(); + + // If the state root is in range of the freezer DB's linear state root storage, fetch it + // directly from there. This is useful on archive nodes to avoid some of the complexity of + // traversing the sparse portion of the hdiff grid (prior to the split slot). It is also + // necessary for the v24 schema migration on archive nodes, where there isn't yet any grid + // to traverse. + if target_slot < split.slot && target_slot >= state_upper_limit { + drop(split); + return store + .get_cold_state_root(target_slot) + .map_err(Box::new) + .map_err(StateSummaryIteratorError::LoadStateRootError)? + .ok_or(StateSummaryIteratorError::MissingStateRoot { + target_slot, + state_upper_limit, + }); + } + + let mut state_root = { + // We can not start loading summaries from `state_root` since its summary has not yet been + // imported. This code path is called during block import. + // + // We need to choose a state_root to start that is + // - An ancestor of `from_state`, AND + // - Its state summary is already written (and not pruned) in the DB + // - Its slot is >= target_slot + // + // If we get to this codepath, (target_slot not in state's state_roots) it means that + // `state.slot()` is greater than `SlotsPerHistoricalRoot`, and `target_slot < state.slot() + // - SlotsPerHistoricalRoot`. + // + // Values we could start from: + // - `state.slot() - 1`: TODO if we don't immediately commit all each state to the DB + // individually, we may be attempting to read a state summary that is stored in a DB ops + // vector but not yet written to the DB. Also starting from this slot is wasteful as we + // know that the target slot is `< state.slot() - SlotsPerHistoricalRoot`. + // - `state.slot() - SlotsPerHistoricalRoot`: The most efficient slot to start. But we risk + // jumping to a state summary that has already been pruned. See the `max(.., split_slot)` + // below + let oldest_slot_in_state_roots = from_state + .slot() + .saturating_sub(Slot::new(E::SlotsPerHistoricalRoot::to_u64())); + + // Don't start with a slot that prior to the finalized state slot. We may be attempting to read + // a hot state summary that has already been pruned as part of the migration and error. HDiffs + // can reference diffs with a slot prior to the finalized checkpoint. But those are sparse so + // the probabiliy of hitting `MissingSummary` error is high. Instead, the summary for the + // finalized state is always available. + let start_slot = std::cmp::max(oldest_slot_in_state_roots, split.slot); + + *from_state + .get_state_root(start_slot) + .map_err(|_| StateSummaryIteratorError::OutOfBoundsInitialSlot)? + }; + + let mut previous_slot = None; + + loop { + let state_summary = store + .load_hot_state_summary(&state_root) + .map_err(|e| StateSummaryIteratorError::LoadSummaryError(Box::new(e)))? + .ok_or(StateSummaryIteratorError::MissingSummary(state_root))?; + + // Protect against infinite loops if the state summaries are not strictly descending + if let Some(previous_slot) = previous_slot + && state_summary.slot >= previous_slot + { + drop(split); + return Err(StateSummaryIteratorError::CircularSummaries { + state_root, + state_slot: state_summary.slot, + previous_slot, + }); + } + previous_slot = Some(state_summary.slot); + + match state_summary.slot.cmp(&target_slot) { + Ordering::Less => { + drop(split); + return Err(StateSummaryIteratorError::BelowTarget(state_summary.slot)); + } + Ordering::Equal => return Ok(state_root), + Ordering::Greater => {} // keep going + } + + // Jump to an older state summary that is an ancestor of `state_root` + if let OptionalDiffBaseState::BaseState(DiffBaseState { + slot, + state_root: diff_base_state_root, + }) = state_summary.diff_base_state + { + if target_slot <= slot { + // As an optimization use the HDiff state root to jump states faster + state_root = diff_base_state_root; + } + continue; + } + // Else jump slot by slot + state_root = state_summary.previous_state_root; + } +} + /// Struct for summarising a state in the hot database. /// /// Allows full reconstruction by replaying blocks. -#[derive(Debug, Clone, Copy, Default, Encode, Decode)] +#[derive(Debug, Clone, Copy, Encode, Decode)] pub struct HotStateSummary { pub slot: Slot, pub latest_block_root: Hash256, - epoch_boundary_state_root: Hash256, + pub latest_block_slot: Slot, + pub diff_base_state: OptionalDiffBaseState, + pub previous_state_root: Hash256, +} + +/// Information about the state that a hot state is diffed from or replays blocks from, if any. +/// +/// In the case of a snapshot, there is no diff base state, so this value will be +/// `DiffBaseState::Snapshot`. +#[derive(Debug, Clone, Copy, Encode, Decode)] +#[ssz(enum_behaviour = "union")] +pub enum OptionalDiffBaseState { + // The SSZ crate requires *something* in each variant so we just store a u8 set to 0. + Snapshot(u8), + BaseState(DiffBaseState), +} + +#[derive(Debug, Clone, Copy, Encode, Decode)] +pub struct DiffBaseState { + slot: Slot, + state_root: Hash256, +} + +impl OptionalDiffBaseState { + pub fn new(slot: Slot, state_root: Hash256) -> Self { + Self::BaseState(DiffBaseState { slot, state_root }) + } + + pub fn get_root(&self, slot: Slot) -> Result { + match *self { + Self::Snapshot(_) => Err(Error::SnapshotDiffBaseState { slot }), + Self::BaseState(DiffBaseState { + slot: stored_slot, + state_root, + }) => { + if stored_slot == slot { + Ok(state_root) + } else { + Err(Error::MismatchedDiffBaseState { + expected_slot: slot, + stored_slot, + }) + } + } + } + } +} + +// Succint rendering of (slot, state_root) pair for "Storing hot state summary and diffs" log +impl std::fmt::Display for OptionalDiffBaseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Snapshot(_) => write!(f, "snapshot"), + Self::BaseState(base_state) => write!(f, "{base_state}"), + } + } +} + +impl std::fmt::Display for DiffBaseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{:?}", self.slot, self.state_root) + } } impl StoreItem for HotStateSummary { fn db_column() -> DBColumn { - DBColumn::BeaconStateSummary + DBColumn::BeaconStateHotSummary } fn as_store_bytes(&self) -> Vec { @@ -3134,27 +3904,78 @@ impl StoreItem for HotStateSummary { impl HotStateSummary { /// Construct a new summary of the given state. - pub fn new(state_root: &Hash256, state: &BeaconState) -> Result { + pub fn new, Cold: ItemStore>( + store: &HotColdDB, + state_root: Hash256, + state: &BeaconState, + storage_strategy: StorageStrategy, + ) -> Result { // Fill in the state root on the latest block header if necessary (this happens on all // slots where there isn't a skip). - let latest_block_root = state.get_latest_block_root(*state_root); - let epoch_boundary_slot = state.slot() / E::slots_per_epoch() * E::slots_per_epoch(); - let epoch_boundary_state_root = if epoch_boundary_slot == state.slot() { - *state_root + let latest_block_root = state.get_latest_block_root(state_root); + + let get_state_root = |slot| { + if slot == state.slot() { + Ok::<_, Error>(state_root) + } else { + Ok(get_ancestor_state_root(store, state, slot).map_err(|e| { + Error::StateSummaryIteratorError { + error: e, + from_state_root: state_root, + from_state_slot: state.slot(), + target_slot: slot, + } + })?) + } + }; + let diff_base_slot = storage_strategy.diff_base_slot(); + let diff_base_state = if let Some(diff_base_slot) = diff_base_slot { + OptionalDiffBaseState::new(diff_base_slot, get_state_root(diff_base_slot)?) } else { - *state - .get_state_root(epoch_boundary_slot) - .map_err(HotColdDBError::HotStateSummaryError)? + OptionalDiffBaseState::Snapshot(0) + }; + + let previous_state_root = if state.slot() == 0 { + // Set to 0x0 for genesis state to prevent any sort of circular reference. + Hash256::zero() + } else { + get_state_root(state.slot().safe_sub(1_u64)?)? }; Ok(HotStateSummary { slot: state.slot(), latest_block_root, - epoch_boundary_state_root, + latest_block_slot: state.latest_block_header().slot, + diff_base_state, + previous_state_root, }) } } +/// Legacy hot state summary used in schema V22 and before. +/// +/// This can be deleted when we remove V22 support. +#[derive(Debug, Clone, Copy, Encode, Decode)] +pub struct HotStateSummaryV22 { + pub slot: Slot, + pub latest_block_root: Hash256, + pub epoch_boundary_state_root: Hash256, +} + +impl StoreItem for HotStateSummaryV22 { + fn db_column() -> DBColumn { + DBColumn::BeaconStateSummary + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} + /// Struct for summarising a state in the freezer database. #[derive(Debug, Clone, Copy, Default, Encode, Decode)] pub(crate) struct ColdStateSummary { diff --git a/beacon_node/store/src/impls.rs b/beacon_node/store/src/impls.rs index 736585a72a..691c79ace7 100644 --- a/beacon_node/store/src/impls.rs +++ b/beacon_node/store/src/impls.rs @@ -1,2 +1 @@ -pub mod beacon_state; pub mod execution_payload; diff --git a/beacon_node/store/src/impls/beacon_state.rs b/beacon_node/store/src/impls/beacon_state.rs deleted file mode 100644 index fd08e547f1..0000000000 --- a/beacon_node/store/src/impls/beacon_state.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::*; -use ssz::{DecodeError, Encode}; -use ssz_derive::Encode; - -pub fn store_full_state( - state_root: &Hash256, - state: &BeaconState, - ops: &mut Vec, -) -> Result<(), Error> { - let bytes = { - let _overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_WRITE_OVERHEAD_TIMES); - StorageContainer::new(state).as_ssz_bytes() - }; - metrics::inc_counter_by(&metrics::BEACON_STATE_WRITE_BYTES, bytes.len() as u64); - metrics::inc_counter(&metrics::BEACON_STATE_WRITE_COUNT); - ops.push(KeyValueStoreOp::PutKeyValue( - DBColumn::BeaconState, - state_root.as_slice().to_vec(), - bytes, - )); - Ok(()) -} - -pub fn get_full_state, E: EthSpec>( - db: &KV, - state_root: &Hash256, - spec: &ChainSpec, -) -> Result>, Error> { - let total_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_TIMES); - - match db.get_bytes(DBColumn::BeaconState, state_root.as_slice())? { - Some(bytes) => { - let overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_OVERHEAD_TIMES); - let container = StorageContainer::from_ssz_bytes(&bytes, spec)?; - - metrics::stop_timer(overhead_timer); - metrics::stop_timer(total_timer); - metrics::inc_counter(&metrics::BEACON_STATE_READ_COUNT); - metrics::inc_counter_by(&metrics::BEACON_STATE_READ_BYTES, bytes.len() as u64); - - Ok(Some(container.try_into()?)) - } - None => Ok(None), - } -} - -/// A container for storing `BeaconState` components. -// TODO: would be more space efficient with the caches stored separately and referenced by hash -#[derive(Encode)] -pub struct StorageContainer { - state: BeaconState, - committee_caches: Vec>, -} - -impl StorageContainer { - /// Create a new instance for storing a `BeaconState`. - pub fn new(state: &BeaconState) -> Self { - Self { - state: state.clone(), - committee_caches: state.committee_caches().to_vec(), - } - } - - pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { - // We need to use the slot-switching `from_ssz_bytes` of `BeaconState`, which doesn't - // compose with the other SSZ utils, so we duplicate some parts of `ssz_derive` here. - let mut builder = ssz::SszDecoderBuilder::new(bytes); - - builder.register_anonymous_variable_length_item()?; - builder.register_type::>()?; - - let mut decoder = builder.build()?; - - let state = decoder.decode_next_with(|bytes| BeaconState::from_ssz_bytes(bytes, spec))?; - let committee_caches = decoder.decode_next()?; - - Ok(Self { - state, - committee_caches, - }) - } -} - -impl TryInto> for StorageContainer { - type Error = Error; - - fn try_into(mut self) -> Result, Error> { - let mut state = self.state; - - for i in (0..CACHED_EPOCHS).rev() { - if i >= self.committee_caches.len() { - return Err(Error::SszDecodeError(DecodeError::BytesInvalid( - "Insufficient committees for BeaconState".to_string(), - ))); - }; - - state.committee_caches_mut()[i] = self.committee_caches.remove(i); - } - - Ok(state) - } -} diff --git a/beacon_node/store/src/impls/execution_payload.rs b/beacon_node/store/src/impls/execution_payload.rs index b832324d86..22b3d4b51d 100644 --- a/beacon_node/store/src/impls/execution_payload.rs +++ b/beacon_node/store/src/impls/execution_payload.rs @@ -3,6 +3,7 @@ use ssz::{Decode, Encode}; use types::{ EthSpec, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, + ExecutionPayloadGloas, }; macro_rules! impl_store_item { @@ -26,8 +27,9 @@ impl_store_item!(ExecutionPayloadBellatrix); impl_store_item!(ExecutionPayloadCapella); impl_store_item!(ExecutionPayloadDeneb); impl_store_item!(ExecutionPayloadElectra); -impl_store_item!(ExecutionPayloadEip7805); impl_store_item!(ExecutionPayloadFulu); +impl_store_item!(ExecutionPayloadEip7805); +impl_store_item!(ExecutionPayloadGloas); /// This fork-agnostic implementation should be only used for writing. /// @@ -43,28 +45,32 @@ impl StoreItem for ExecutionPayload { } fn from_store_bytes(bytes: &[u8]) -> Result { - ExecutionPayloadFulu::from_ssz_bytes(bytes) - .map(Self::Fulu) - .or_else(|_| { - ExecutionPayloadEip7805::from_ssz_bytes(bytes) - .map(Self::Eip7805) - .or_else(|_| { - ExecutionPayloadElectra::from_ssz_bytes(bytes) - .map(Self::Electra) - .or_else(|_| { - ExecutionPayloadDeneb::from_ssz_bytes(bytes) - .map(Self::Deneb) - .or_else(|_| { - ExecutionPayloadCapella::from_ssz_bytes(bytes) - .map(Self::Capella) - .or_else(|_| { - ExecutionPayloadBellatrix::from_ssz_bytes(bytes) - .map(Self::Bellatrix) - }) - }) - }) - }) - }) + if let Ok(payload) = ExecutionPayloadGloas::from_ssz_bytes(bytes) { + return Ok(Self::Gloas(payload)); + } + + if let Ok(payload) = ExecutionPayloadEip7805::from_ssz_bytes(bytes) { + return Ok(Self::Eip7805(payload)); + } + + if let Ok(payload) = ExecutionPayloadFulu::from_ssz_bytes(bytes) { + return Ok(Self::Fulu(payload)); + } + + if let Ok(payload) = ExecutionPayloadElectra::from_ssz_bytes(bytes) { + return Ok(Self::Electra(payload)); + } + + if let Ok(payload) = ExecutionPayloadDeneb::from_ssz_bytes(bytes) { + return Ok(Self::Deneb(payload)); + } + + if let Ok(payload) = ExecutionPayloadCapella::from_ssz_bytes(bytes) { + return Ok(Self::Capella(payload)); + } + + ExecutionPayloadBellatrix::from_ssz_bytes(bytes) + .map(Self::Bellatrix) .map_err(Into::into) } } diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 8419dde4a2..e2b666e597 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -2,9 +2,9 @@ use crate::errors::HandleUnavailable; use crate::{Error, HotColdDB, ItemStore}; use std::borrow::Cow; use std::marker::PhantomData; +use typenum::Unsigned; use types::{ - typenum::Unsigned, BeaconState, BeaconStateError, BlindedPayload, EthSpec, Hash256, - SignedBeaconBlock, Slot, + BeaconState, BeaconStateError, BlindedPayload, EthSpec, Hash256, SignedBeaconBlock, Slot, }; /// Implemented for types that have ancestors (e.g., blocks, states) that may be iterated over. @@ -384,11 +384,11 @@ fn slot_of_prev_restore_point(current_slot: Slot) -> Slot { #[cfg(test)] mod test { use super::*; - use crate::StoreConfig as Config; + use crate::{MemoryStore, StoreConfig as Config}; use beacon_chain::test_utils::BeaconChainHarness; - use beacon_chain::types::{ChainSpec, MainnetEthSpec}; + use beacon_chain::types::MainnetEthSpec; + use fixed_bytes::FixedBytesExtended; use std::sync::Arc; - use types::FixedBytesExtended; fn get_state() -> BeaconState { let harness = BeaconChainHarness::builder(E::default()) @@ -400,10 +400,31 @@ mod test { harness.get_current_state() } + fn get_store() -> HotColdDB, MemoryStore> { + let store = + HotColdDB::open_ephemeral(Config::default(), Arc::new(E::default_spec())).unwrap(); + // Init achor info so anchor slot is set. Use a random block as it is only used for the + // parent_root + let _ = store + .init_anchor_info(Hash256::ZERO, Slot::new(0), Slot::new(0), false) + .unwrap(); + // Write a state with state root 0 which is the base `put_state` below tries to diff from + { + let harness = BeaconChainHarness::builder(E::default()) + .default_spec() + .deterministic_keypairs(1) + .fresh_ephemeral_store() + .build(); + let genesis_state = harness.get_current_state(); + store.put_state(&Hash256::ZERO, &genesis_state).unwrap(); + } + store + } + #[test] fn block_root_iter() { - let store = - HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap(); + let store = get_store::(); + let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); let mut state_a: BeaconState = get_state(); @@ -449,8 +470,8 @@ mod test { #[test] fn state_root_iter() { - let store = - HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap(); + let store = get_store::(); + let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root(); let mut state_a: BeaconState = get_state(); diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 5b30971fd8..ae5b2e1e57 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -8,8 +8,6 @@ //! Provides a simple API for storing/retrieving all types that sometimes needs type-hints. See //! tests for implementation examples. pub mod blob_sidecar_list_from_root; -pub mod chunked_iter; -pub mod chunked_vector; pub mod config; pub mod consensus_context; pub mod errors; @@ -21,7 +19,6 @@ mod impls; mod memory_store; pub mod metadata; pub mod metrics; -pub mod partial_beacon_state; pub mod reconstruct; pub mod state_cache; @@ -35,10 +32,8 @@ pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::memory_store::MemoryStore; pub use crate::metadata::BlobInfo; pub use errors::Error; -pub use impls::beacon_state::StorageContainer as BeaconStateStorageContainer; pub use metadata::AnchorInfo; pub use metrics::scrape_for_metrics; -use parking_lot::MutexGuard; use std::collections::HashSet; use std::sync::Arc; use strum::{EnumIter, EnumString, IntoStaticStr}; @@ -76,12 +71,6 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { /// Execute either all of the operations in `batch` or none at all, returning an error. fn do_atomically(&self, batch: Vec) -> Result<(), Error>; - /// Return a mutex guard that can be used to synchronize sensitive transactions. - /// - /// This doesn't prevent other threads writing to the DB unless they also use - /// this method. In future we may implement a safer mandatory locking scheme. - fn begin_rw_transaction(&self) -> MutexGuard<()>; - /// Compact a single column in the database, freeing space used by deleted items. fn compact_column(&self, column: DBColumn) -> Result<(), Error>; @@ -91,7 +80,7 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { // i.e. entries being created and deleted. for column in [ DBColumn::BeaconState, - DBColumn::BeaconStateSummary, + DBColumn::BeaconStateHotSummary, DBColumn::BeaconBlock, ] { self.compact_column(column)?; @@ -100,17 +89,17 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { } /// Iterate through all keys and values in a particular column. - fn iter_column(&self, column: DBColumn) -> ColumnIter { + fn iter_column(&self, column: DBColumn) -> ColumnIter<'_, K> { self.iter_column_from(column, &vec![0; column.key_size()]) } /// Iterate through all keys and values in a column from a given starting point that fulfill the given predicate. - fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter; + fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K>; - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter<'_, K>; /// Iterate through all keys in a particular column. - fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter; + fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<'_, K>; fn delete_batch(&self, column: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error>; @@ -130,7 +119,10 @@ impl Key for Hash256 { if key.len() == 32 { Ok(Hash256::from_slice(key)) } else { - Err(Error::InvalidKey) + Err(Error::InvalidKey(format!( + "Hash256 key unexpected len {}", + key.len() + ))) } } } @@ -162,7 +154,10 @@ pub fn get_data_column_key(block_root: &Hash256, column_index: &ColumnIndex) -> pub fn parse_data_column_key(data: Vec) -> Result<(Hash256, ColumnIndex), Error> { if data.len() != DBColumn::BeaconDataColumn.key_size() { - return Err(Error::InvalidKey); + return Err(Error::InvalidKey(format!( + "Unexpected BeaconDataColumn key len {}", + data.len() + ))); } // split_at panics if 32 < 40 which will never happen after the length check above let (block_root_bytes, column_index_bytes) = data.split_at(32); @@ -171,7 +166,7 @@ pub fn parse_data_column_key(data: Vec) -> Result<(Hash256, ColumnIndex), Er let column_index = ColumnIndex::from_le_bytes( column_index_bytes .try_into() - .map_err(|_| Error::InvalidKey)?, + .map_err(|e| Error::InvalidKey(format!("Invalid ColumnIndex {e:?}")))?, ); Ok((block_root, column_index)) } @@ -266,21 +261,43 @@ pub enum DBColumn { BeaconBlob, #[strum(serialize = "bdc")] BeaconDataColumn, + #[strum(serialize = "bdi")] + BeaconDataColumnCustodyInfo, /// For full `BeaconState`s in the hot database (finalized or fork-boundary states). + /// + /// DEPRECATED. #[strum(serialize = "ste")] BeaconState, + /// For compact `BeaconStateDiff`'s in the hot DB. + /// + /// hsd = Hot State Diff. + #[strum(serialize = "hsd")] + BeaconStateHotDiff, + /// For beacon state snapshots in the hot DB. + /// + /// hsn = Hot Snapshot. + #[strum(serialize = "hsn")] + BeaconStateHotSnapshot, /// For beacon state snapshots in the freezer DB. #[strum(serialize = "bsn")] BeaconStateSnapshot, /// For compact `BeaconStateDiff`s in the freezer DB. #[strum(serialize = "bsd")] BeaconStateDiff, - /// Mapping from state root to `HotStateSummary` in the hot DB. + /// DEPRECATED + /// + /// Mapping from state root to `HotStateSummaryV22` in the hot DB. /// /// Previously this column also served a role in the freezer DB, mapping state roots to /// `ColdStateSummary`. However that role is now filled by `BeaconColdStateSummary`. #[strum(serialize = "bss")] BeaconStateSummary, + /// Mapping from state root to `HotStateSummaryV23` in the hot DB. + /// + /// This column is populated after DB schema version 23 superseding `BeaconStateSummary`. The + /// new column is necessary to have a safe migration without data loss. + #[strum(serialize = "bs3")] + BeaconStateHotSummary, /// Mapping from state root to `ColdStateSummary` in the cold DB. #[strum(serialize = "bcs")] BeaconColdStateSummary, @@ -298,6 +315,7 @@ pub enum DBColumn { BeaconChain, #[strum(serialize = "opo")] OpPool, + /// DEPRECATED. #[strum(serialize = "etc")] Eth1Cache, #[strum(serialize = "frk")] @@ -339,6 +357,8 @@ pub enum DBColumn { BeaconRandaoMixes, #[strum(serialize = "dht")] DhtEnrs, + #[strum(serialize = "cus")] + CustodyContext, /// DEPRECATED. For Optimistically Imported Merge Transition Blocks #[strum(serialize = "otb")] OptimisticTransitionBlock, @@ -387,6 +407,9 @@ impl DBColumn { | Self::BeaconState | Self::BeaconBlob | Self::BeaconStateSummary + | Self::BeaconStateHotDiff + | Self::BeaconStateHotSnapshot + | Self::BeaconStateHotSummary | Self::BeaconColdStateSummary | Self::BeaconStateTemporary | Self::ExecPayload @@ -397,8 +420,10 @@ impl DBColumn { | Self::PubkeyCache | Self::BeaconRestorePoint | Self::DhtEnrs + | Self::CustodyContext | Self::OptimisticTransitionBlock => 32, Self::BeaconBlockRoots + | Self::BeaconDataColumnCustodyInfo | Self::BeaconBlockRootsChunked | Self::BeaconStateRoots | Self::BeaconStateRootsChunked diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 6070a2d3f0..6baef61c9d 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -1,8 +1,8 @@ use crate::{ - errors::Error as DBError, get_key_for_col, hot_cold_store::BytesKey, ColumnIter, ColumnKeyIter, - DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp, + ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp, + errors::Error as DBError, get_key_for_col, hot_cold_store::BytesKey, }; -use parking_lot::{Mutex, MutexGuard, RwLock}; +use parking_lot::RwLock; use std::collections::{BTreeMap, HashSet}; use std::marker::PhantomData; use types::*; @@ -12,7 +12,6 @@ type DBMap = BTreeMap>; /// A thread-safe `BTreeMap` wrapper. pub struct MemoryStore { db: RwLock, - transaction_mutex: Mutex<()>, _phantom: PhantomData, } @@ -21,7 +20,6 @@ impl MemoryStore { pub fn open() -> Self { Self { db: RwLock::new(BTreeMap::new()), - transaction_mutex: Mutex::new(()), _phantom: PhantomData, } } @@ -82,7 +80,7 @@ impl KeyValueStore for MemoryStore { Ok(()) } - fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> { // We use this awkward pattern because we can't lock the `self.db` field *and* maintain a // reference to the lock guard across calls to `.next()`. This would be require a // struct with a field (the iterator) which references another field (the lock guard). @@ -103,19 +101,15 @@ impl KeyValueStore for MemoryStore { })) } - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter<'_, K> { Box::new(self.iter_column(column).map(|res| res.map(|(k, _)| k))) } - fn begin_rw_transaction(&self) -> MutexGuard<()> { - self.transaction_mutex.lock() - } - fn compact_column(&self, _column: DBColumn) -> Result<(), Error> { Ok(()) } - fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<'_, K> { // We use this awkward pattern because we can't lock the `self.db` field *and* maintain a // reference to the lock guard across calls to `.next()`. This would be require a // struct with a field (the iterator) which references another field (the lock guard). diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 55c64bf850..cf49468451 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -2,9 +2,9 @@ use crate::{DBColumn, Error, StoreItem}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; -use types::{Checkpoint, Hash256, Slot}; +use types::{Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(23); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(28); // All the keys that get stored under the `BeaconMeta` column. // @@ -12,24 +12,17 @@ pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(23); pub const SCHEMA_VERSION_KEY: Hash256 = Hash256::repeat_byte(0); pub const CONFIG_KEY: Hash256 = Hash256::repeat_byte(1); pub const SPLIT_KEY: Hash256 = Hash256::repeat_byte(2); -pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3); +// DEPRECATED +// pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3); pub const COMPACTION_TIMESTAMP_KEY: Hash256 = Hash256::repeat_byte(4); pub const ANCHOR_INFO_KEY: Hash256 = Hash256::repeat_byte(5); pub const BLOB_INFO_KEY: Hash256 = Hash256::repeat_byte(6); pub const DATA_COLUMN_INFO_KEY: Hash256 = Hash256::repeat_byte(7); +pub const DATA_COLUMN_CUSTODY_INFO_KEY: Hash256 = Hash256::repeat_byte(8); /// State upper limit value used to indicate that a node is not storing historic states. pub const STATE_UPPER_LIMIT_NO_RETAIN: Slot = Slot::new(u64::MAX); -/// The `AnchorInfo` encoding full availability of all historic blocks & states. -pub const ANCHOR_FOR_ARCHIVE_NODE: AnchorInfo = AnchorInfo { - anchor_slot: Slot::new(0), - oldest_block_slot: Slot::new(0), - oldest_block_parent: Hash256::ZERO, - state_upper_limit: Slot::new(0), - state_lower_limit: Slot::new(0), -}; - /// The `AnchorInfo` encoding an uninitialized anchor. /// /// This value should never exist except on initial start-up prior to the anchor being initialised @@ -65,30 +58,6 @@ impl StoreItem for SchemaVersion { } } -/// The checkpoint used for pruning the database. -/// -/// Updated whenever pruning is successful. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PruningCheckpoint { - pub checkpoint: Checkpoint, -} - -impl StoreItem for PruningCheckpoint { - fn db_column() -> DBColumn { - DBColumn::BeaconMeta - } - - fn as_store_bytes(&self) -> Vec { - self.checkpoint.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(PruningCheckpoint { - checkpoint: Checkpoint::from_ssz_bytes(bytes)?, - }) - } -} - /// The last time the database was compacted. pub struct CompactionTimestamp(pub u64); @@ -111,7 +80,8 @@ impl StoreItem for CompactionTimestamp { pub struct AnchorInfo { /// The slot at which the anchor state is present and which we cannot revert. Values on start: /// - Genesis start: 0 - /// - Checkpoint sync: Slot of the finalized checkpoint block + /// - Checkpoint sync: Slot of the finalized state advanced to the checkpoint epoch + /// - Existing DB prior to v23: Finalized state slot at the migration moment /// /// Immutable pub anchor_slot: Slot, @@ -175,6 +145,21 @@ impl AnchorInfo { pub fn full_state_pruning_enabled(&self) -> bool { self.state_lower_limit == 0 && self.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN } + + /// Compute the correct `AnchorInfo` for an archive node created from the current node. + /// + /// This method ensures that the `anchor_slot` which is used for the hot database's diff grid is + /// preserved. + pub fn as_archive_anchor(&self) -> Self { + Self { + // Anchor slot MUST be the same. It is immutable. + anchor_slot: self.anchor_slot, + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: Slot::new(0), + state_lower_limit: Slot::new(0), + } + } } impl StoreItem for AnchorInfo { @@ -220,6 +205,30 @@ impl StoreItem for BlobInfo { } } +/// Database parameter relevant to data column custody sync. There is only at most a single +/// `DataColumnCustodyInfo` stored in the db. `earliest_data_column_slot` is updated when cgc +/// count changes and is updated incrementally during data column custody backfill. Once custody backfill +/// is complete `earliest_data_column_slot` is set to `None`. +#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Default)] +pub struct DataColumnCustodyInfo { + /// The earliest slot for which data columns are available. + pub earliest_data_column_slot: Option, +} + +impl StoreItem for DataColumnCustodyInfo { + fn db_column() -> DBColumn { + DBColumn::BeaconDataColumnCustodyInfo + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(DataColumnCustodyInfo::from_ssz_bytes(bytes)?) + } +} + /// Database parameters relevant to data column sync. #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Default)] pub struct DataColumnInfo { diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 5da73c3cad..93c9840586 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -4,6 +4,10 @@ use directory::size_of_dir; use std::path::Path; use std::sync::LazyLock; +// Labels used for histogram timer vecs that are tracked per DB (hot and cold). +pub const HOT_METRIC: &[&str] = &["hot"]; +pub const COLD_METRIC: &[&str] = &["cold"]; + /* * General */ @@ -64,7 +68,7 @@ pub static DISK_DB_WRITE_COUNT: LazyLock> = LazyLock::new( pub static DISK_DB_READ_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram( "store_disk_db_read_seconds", - "Time taken to write bytes to store.", + "Time taken to read bytes from store.", ) }); pub static DISK_DB_WRITE_TIMES: LazyLock> = LazyLock::new(|| { @@ -142,71 +146,84 @@ pub static BEACON_STATE_HOT_GET_COUNT: LazyLock> = LazyLock:: "Total number of hot beacon states requested from the store (cache or DB)", ) }); -pub static BEACON_STATE_READ_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "store_beacon_state_read_seconds", - "Total time required to read a BeaconState from the database", - ) -}); -pub static BEACON_STATE_READ_OVERHEAD_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "store_beacon_state_read_overhead_seconds", - "Overhead on reading a beacon state from the DB (e.g., decoding)", - ) -}); -pub static BEACON_STATE_READ_COUNT: LazyLock> = LazyLock::new(|| { - try_create_int_counter( - "store_beacon_state_read_total", - "Total number of beacon state reads from the DB", - ) -}); -pub static BEACON_STATE_READ_BYTES: LazyLock> = LazyLock::new(|| { - try_create_int_counter( - "store_beacon_state_read_bytes_total", - "Total number of beacon state bytes read from the DB", - ) -}); -pub static BEACON_STATE_WRITE_OVERHEAD_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "store_beacon_state_write_overhead_seconds", - "Overhead on writing a beacon state to the DB (e.g., encoding)", - ) -}); -pub static BEACON_STATE_WRITE_COUNT: LazyLock> = LazyLock::new(|| { - try_create_int_counter( - "store_beacon_state_write_total", - "Total number of beacon state writes the DB", - ) -}); -pub static BEACON_STATE_WRITE_BYTES: LazyLock> = LazyLock::new(|| { - try_create_int_counter( - "store_beacon_state_write_bytes_total", - "Total number of beacon state bytes written to the DB", - ) -}); -pub static BEACON_HDIFF_READ_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( + +/* + * HDiffs + */ +pub static BEACON_HDIFF_READ_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( "store_hdiff_read_seconds", - "Time required to read the hierarchical diff bytes from the database", + "Time taken to read hdiff bytes from disk", + &["db"], ) }); -pub static BEACON_HDIFF_DECODE_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( +pub static BEACON_HDIFF_DECODE_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( "store_hdiff_decode_seconds", - "Time required to decode hierarchical diff bytes", + "Time taken to decode hdiff bytes", + &["db"], ) }); -pub static BEACON_HDIFF_BUFFER_CLONE_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram( +pub static BEACON_HDIFF_APPLY_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( + "store_hdiff_apply_seconds", + "Time taken to apply an hdiff to a buffer", + &["db"], + ) +}); +pub static BEACON_HDIFF_COMPUTE_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( + "store_hdiff_compute_seconds", + "Time taken to compute an hdiff for a state", + &["db"], + ) +}); +pub static BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( + "store_hdiff_buffer_load_seconds", + "Time taken to load an hdiff buffer for a state", + &["db"], + ) +}); +pub static BEACON_HDIFF_BUFFER_CLONE_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec( "store_hdiff_buffer_clone_seconds", - "Time required to clone hierarchical diff buffer bytes", + "Time taken to clone an hdiff buffer from a cache", + &["db"], ) }); +pub static BEACON_HDIFF_BUFFER_LOAD_BEFORE_STORE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram_vec( + "store_hdiff_buffer_load_before_store_seconds", + "Time taken to load the hdiff buffer required for the storage of a new state", + &["db"], + ) + }); +// This metric is not split hot/cold because it is recorded in a place where that info is not known. pub static BEACON_HDIFF_BUFFER_APPLY_RESIZES: LazyLock> = LazyLock::new(|| { try_create_histogram_with_buckets( "store_hdiff_buffer_apply_resizes", "Number of times during diff application that the output buffer had to be resized before decoding succeeded", - Ok(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) + Ok(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]), + ) +}); +// This metric is not split hot/cold because both databases use the same hierarchy config anyway +// and that's all that affects diff sizes. +pub static BEACON_HDIFF_SIZES: LazyLock> = LazyLock::new(|| { + try_create_histogram_vec_with_buckets( + "store_hdiff_sizes", + "Size of hdiffs in bytes by layer (exponent)", + Ok(vec![ + 500_000.0, + 2_000_000.0, + 5_000_000.0, + 10_000_000.0, + 15_000_000.0, + 20_000_000.0, + 50_000_000.0, + ]), + &["exponent"], ) }); /* @@ -259,17 +276,20 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: LazyLock> = "Current count of states in the historic state cache", ) }); -pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "store_beacon_hdiff_buffer_cache_size", - "Current count of hdiff buffers in the historic state cache", - ) -}); -pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( + try_create_int_gauge_vec( + "store_beacon_hdiff_buffer_cache_size", + "Current count of hdiff buffers cached in memory", + &["db"], + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge_vec( "store_beacon_hdiff_buffer_cache_byte_size", - "Memory consumed by hdiff buffers in the historic state cache", + "Memory consumed by hdiff buffers cached in memory", + &["db"], ) }); pub static STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: LazyLock> = @@ -286,33 +306,6 @@ pub static STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME: LazyLock> = - LazyLock::new(|| { - try_create_histogram( - "store_beacon_hdiff_buffer_apply_seconds", - "Time taken to apply hdiff buffer to a state buffer", - ) - }); -pub static STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME: LazyLock> = - LazyLock::new(|| { - try_create_histogram( - "store_beacon_hdiff_buffer_compute_seconds", - "Time taken to compute hdiff buffer to a state buffer", - ) - }); -pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = LazyLock::new(|| { - try_create_histogram( - "store_beacon_hdiff_buffer_load_seconds", - "Time taken to load an hdiff buffer", - ) -}); -pub static STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME: LazyLock> = - LazyLock::new(|| { - try_create_histogram( - "store_beacon_hdiff_buffer_load_for_store_seconds", - "Time taken to load an hdiff buffer to store another hdiff", - ) - }); pub static STORE_BEACON_HISTORIC_STATE_CACHE_HIT: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -327,18 +320,20 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_MISS: LazyLock> "Total count of historic state cache misses for full states", ) }); -pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = LazyLock::new(|| { - try_create_int_counter( + try_create_int_counter_vec( "store_beacon_hdiff_buffer_cache_hit_total", "Total count of hdiff buffer cache hits", + &["db"], ) }); -pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock> = +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock> = LazyLock::new(|| { - try_create_int_counter( + try_create_int_counter_vec( "store_beacon_hdiff_buffer_cache_miss_total", "Total count of hdiff buffer cache miss", + &["db"], ) }); pub static STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME: LazyLock> = diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 30df552b7b..7aca692ef9 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -1,12 +1,11 @@ //! Implementation of historic state reconstruction (given complete block history). use crate::hot_cold_store::{HotColdDB, HotColdDBError}; -use crate::metadata::ANCHOR_FOR_ARCHIVE_NODE; use crate::metrics; use crate::{Error, ItemStore}; -use itertools::{process_results, Itertools}; +use itertools::{Itertools, process_results}; use state_processing::{ - per_block_processing, per_slot_processing, BlockSignatureStrategy, ConsensusContext, - VerifyBlockRoot, + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing, + per_slot_processing, }; use std::sync::Arc; use tracing::{debug, info}; @@ -48,6 +47,12 @@ where let lower_limit_slot = anchor.state_lower_limit; let upper_limit_slot = std::cmp::min(split.slot, anchor.state_upper_limit); + // If the split is at 0 we can't reconstruct historic states. + if split.slot == 0 { + debug!("No state reconstruction possible"); + return Ok(()); + } + // If `num_blocks` is not specified iterate all blocks. Add 1 so that we end on an epoch // boundary when `num_blocks` is a multiple of an epoch boundary. We want to be *inclusive* // of the state at slot `lower_limit_slot + num_blocks`. @@ -145,10 +150,8 @@ where }); } - self.compare_and_set_anchor_info_with_write( - old_anchor, - ANCHOR_FOR_ARCHIVE_NODE, - )?; + let new_anchor = old_anchor.as_archive_anchor(); + self.compare_and_set_anchor_info_with_write(old_anchor, new_anchor)?; return Ok(()); } else { diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs index 281ecab152..4b0d1ee016 100644 --- a/beacon_node/store/src/state_cache.rs +++ b/beacon_node/store/src/state_cache.rs @@ -1,7 +1,12 @@ -use crate::Error; +use crate::hdiff::HDiffBuffer; +use crate::{ + Error, + metrics::{self, HOT_METRIC}, +}; use lru::LruCache; use std::collections::{BTreeMap, HashMap, HashSet}; use std::num::NonZeroUsize; +use tracing::instrument; use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot}; /// Fraction of the LRU cache to leave intact during culling. @@ -37,26 +42,53 @@ pub struct StateCache { // the state_root states: LruCache)>, block_map: BlockMap, + hdiff_buffers: HotHDiffBufferCache, max_epoch: Epoch, head_block_root: Hash256, headroom: NonZeroUsize, } +/// Cache of hdiff buffers for hot states. +/// +/// This cache only keeps buffers prior to the finalized state, which are required by the +/// hierarchical state diff scheme to construct newer unfinalized states. +/// +/// The cache always retains the hdiff buffer for the most recent snapshot so that even if the +/// cache capacity is 1, this snapshot never needs to be loaded from disk. +#[derive(Debug)] +pub struct HotHDiffBufferCache { + /// Cache of HDiffBuffers for states *prior* to the `finalized_state`. + /// + /// Maps state_root -> (slot, buffer). + hdiff_buffers: LruCache, +} + #[derive(Debug)] pub enum PutStateOutcome { + /// State is prior to the cache's finalized state (lower slot) and was cached as an HDiffBuffer. + PreFinalizedHDiffBuffer, + /// State is equal to the cache's finalized state and was not inserted. Finalized, + /// State was already present in the cache. Duplicate, - /// Includes deleted states as a result of this insertion + /// State is new to the cache and was inserted. + /// + /// Includes deleted states as a result of this insertion. New(Vec), } #[allow(clippy::len_without_is_empty)] impl StateCache { - pub fn new(capacity: NonZeroUsize, headroom: NonZeroUsize) -> Self { + pub fn new( + state_capacity: NonZeroUsize, + headroom: NonZeroUsize, + hdiff_capacity: NonZeroUsize, + ) -> Self { StateCache { finalized_state: None, - states: LruCache::new(capacity), + states: LruCache::new(state_capacity), block_map: BlockMap::default(), + hdiff_buffers: HotHDiffBufferCache::new(hdiff_capacity), max_epoch: Epoch::new(0), head_block_root: Hash256::ZERO, headroom, @@ -71,11 +103,20 @@ impl StateCache { self.states.cap().get() } + pub fn num_hdiff_buffers(&self) -> usize { + self.hdiff_buffers.len() + } + + pub fn hdiff_buffer_mem_usage(&self) -> usize { + self.hdiff_buffers.mem_usage() + } + pub fn update_finalized_state( &mut self, state_root: Hash256, block_root: Hash256, state: BeaconState, + pre_finalized_slots_to_retain: &[Slot], ) -> Result<(), Error> { if state.slot() % E::slots_per_epoch() != 0 { return Err(Error::FinalizedStateUnaligned); @@ -95,9 +136,31 @@ impl StateCache { // Prune block map. let state_roots_to_prune = self.block_map.prune(state.slot()); + // Prune HDiffBuffers that are no longer required by the hdiff grid of the finalized state. + // We need to do this prior to copying in any new hdiff buffers, because the cache + // preferences older slots. + // NOTE: This isn't perfect as it prunes by slot: there could be multiple buffers + // at some slots in the case of long forks without finality. + let new_hdiff_cache = HotHDiffBufferCache::new(self.hdiff_buffers.cap()); + let old_hdiff_cache = std::mem::replace(&mut self.hdiff_buffers, new_hdiff_cache); + for (state_root, (slot, buffer)) in old_hdiff_cache.hdiff_buffers { + if pre_finalized_slots_to_retain.contains(&slot) { + self.hdiff_buffers.put(state_root, slot, buffer); + } + } + // Delete states. for state_root in state_roots_to_prune { - self.states.pop(&state_root); + if let Some((_, state)) = self.states.pop(&state_root) { + // Add the hdiff buffer for this state to the hdiff cache if it is now part of + // the pre-finalized grid. The `put` method will take care of keeping the most + // useful buffers. + let slot = state.slot(); + if pre_finalized_slots_to_retain.contains(&slot) { + let hdiff_buffer = HDiffBuffer::from_state(state); + self.hdiff_buffers.put(state_root, slot, hdiff_buffer); + } + } } // Update finalized state. @@ -123,9 +186,15 @@ impl StateCache { state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - if let Some(finalized_state) = &self.finalized_state { + // Do not attempt to rebase states prior to the finalized state. This method might be called + // with states on the hdiff grid prior to finalization, as part of the reconstruction of + // some later unfinalized state. + if let Some(finalized_state) = &self.finalized_state + && state.slot() >= finalized_state.state.slot() + { state.rebase_on(&finalized_state.state, spec)?; } + Ok(()) } @@ -136,12 +205,19 @@ impl StateCache { block_root: Hash256, state: &BeaconState, ) -> Result { - if self - .finalized_state - .as_ref() - .is_some_and(|finalized_state| finalized_state.state_root == state_root) - { - return Ok(PutStateOutcome::Finalized); + if let Some(ref finalized_state) = self.finalized_state { + if finalized_state.state_root == state_root { + return Ok(PutStateOutcome::Finalized); + } else if state.slot() <= finalized_state.state.slot() { + // We assume any state being inserted into the cache is grid-aligned (it is the + // caller's responsibility to not feed us garbage) as we don't want to thread the + // hierarchy config through here. So any state received is converted to an + // HDiffBuffer and saved. + let hdiff_buffer = HDiffBuffer::from_state(state.clone()); + self.hdiff_buffers + .put(state_root, state.slot(), hdiff_buffer); + return Ok(PutStateOutcome::PreFinalizedHDiffBuffer); + } } if self.states.peek(&state_root).is_some() { @@ -184,14 +260,46 @@ impl StateCache { } pub fn get_by_state_root(&mut self, state_root: Hash256) -> Option> { - if let Some(ref finalized_state) = self.finalized_state { - if state_root == finalized_state.state_root { - return Some(finalized_state.state.clone()); - } + if let Some(ref finalized_state) = self.finalized_state + && state_root == finalized_state.state_root + { + return Some(finalized_state.state.clone()); } self.states.get(&state_root).map(|(_, state)| state.clone()) } + pub fn put_hdiff_buffer(&mut self, state_root: Hash256, slot: Slot, buffer: &HDiffBuffer) { + // Only accept HDiffBuffers prior to finalization. Later states should be stored as proper + // states, not HDiffBuffers. + if let Some(finalized_state) = &self.finalized_state + && slot >= finalized_state.state.slot() + { + return; + } + self.hdiff_buffers.put(state_root, slot, buffer.clone()); + } + + pub fn get_hdiff_buffer_by_state_root(&mut self, state_root: Hash256) -> Option { + if let Some(buffer) = self.hdiff_buffers.get(&state_root) { + metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT, HOT_METRIC); + let timer = + metrics::start_timer_vec(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIME, HOT_METRIC); + let result = Some(buffer.clone()); + drop(timer); + return result; + } + if let Some(buffer) = self + .get_by_state_root(state_root) + .map(HDiffBuffer::from_state) + { + metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT, HOT_METRIC); + return Some(buffer); + } + metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS, HOT_METRIC); + None + } + + #[instrument(skip_all, fields(?block_root, %slot), level = "debug")] pub fn get_by_block_root( &mut self, block_root: Hash256, @@ -245,7 +353,9 @@ impl StateCache { let mut old_boundary_state_roots = vec![]; let mut good_boundary_state_roots = vec![]; - for (&state_root, (_, state)) in self.states.iter().skip(cull_exempt) { + // Skip the `cull_exempt` most-recently used, then reverse the iterator to start at + // least-recently used states. + for (&state_root, (_, state)) in self.states.iter().skip(cull_exempt).rev() { let is_advanced = state.slot() > state.latest_block_header().slot; let is_boundary = state.slot() % E::slots_per_epoch() == 0; let could_finalize = @@ -325,3 +435,80 @@ impl BlockMap { self.blocks.remove(block_root) } } + +impl HotHDiffBufferCache { + pub fn new(capacity: NonZeroUsize) -> Self { + Self { + hdiff_buffers: LruCache::new(capacity), + } + } + + pub fn get(&mut self, state_root: &Hash256) -> Option { + self.hdiff_buffers + .get(state_root) + .map(|(_, buffer)| buffer.clone()) + } + + /// Put a value in the cache, making room for it if necessary. + /// + /// If the value was inserted then `true` is returned. + pub fn put(&mut self, state_root: Hash256, slot: Slot, buffer: HDiffBuffer) -> bool { + // If the cache is not full, simply insert the value. + if self.hdiff_buffers.len() != self.hdiff_buffers.cap().get() { + self.hdiff_buffers.put(state_root, (slot, buffer)); + return true; + } + + // If the cache is full, it has room for this new entry if: + // + // - The capacity is greater than 1: we can retain the snapshot and the new entry, or + // - The capacity is 1 and the slot of the new entry is older than the min_slot in the + // cache. This is a simplified way of retaining the snapshot in the cache. We don't need + // to worry about inserting/retaining states older than the snapshot because these are + // pruned on finalization and never reinserted. + let Some(min_slot) = self.hdiff_buffers.iter().map(|(_, (slot, _))| *slot).min() else { + // Unreachable: cache is full so should have >0 entries. + return false; + }; + + if self.hdiff_buffers.cap().get() > 1 || slot < min_slot { + // Remove LRU value. Cache is now at size `cap - 1`. + let Some((removed_state_root, (removed_slot, removed_buffer))) = + self.hdiff_buffers.pop_lru() + else { + // Unreachable: cache is full so should have at least one entry to pop. + return false; + }; + + // Insert new value. Cache size is now at size `cap`. + self.hdiff_buffers.put(state_root, (slot, buffer)); + + // If the removed value had the min slot and we didn't intend to replace it (cap=1) + // then we reinsert it. + if removed_slot == min_slot && slot >= min_slot { + self.hdiff_buffers + .put(removed_state_root, (removed_slot, removed_buffer)); + } + true + } else { + // No room. + false + } + } + + pub fn cap(&self) -> NonZeroUsize { + self.hdiff_buffers.cap() + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.hdiff_buffers.len() + } + + pub fn mem_usage(&self) -> usize { + self.hdiff_buffers + .iter() + .map(|(_, (_, buffer))| buffer.size()) + .sum() + } +} diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index ab78b65ae9..fa1b902a0f 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -2,9 +2,10 @@ use beacon_chain::StateSkipConfig; use node_test_rig::{ + LocalBeaconNode, environment::{Environment, EnvironmentBuilder}, eth2::types::StateId, - testing_client_config, LocalBeaconNode, + testing_client_config, }; use types::{EthSpec, MinimalEthSpec, Slot}; diff --git a/book/book.toml b/book/book.toml index 7b143710a5..c0d38f6147 100644 --- a/book/book.toml +++ b/book/book.toml @@ -1,7 +1,6 @@ [book] authors = ["Paul Hauner", "Age Manning"] language = "en" -multilingual = false src = "src" title = "Lighthouse Book" diff --git a/book/src/advanced.md b/book/src/advanced.md index 76a7fed202..650b99d456 100644 --- a/book/src/advanced.md +++ b/book/src/advanced.md @@ -19,4 +19,4 @@ tips about how things work under the hood. * [Release Candidates](./advanced_release_candidates.md): latest release of Lighthouse to get feedback from users. * [Maximal Extractable Value](./advanced_builders.md): use external builders for a potential higher rewards during block proposals * [Late Block Re-orgs](./advanced_re-orgs.md): read information about Lighthouse late block re-orgs. -* [Blobs](./advanced_blobs.md): information about blobs in Deneb upgrade +* [Blobs](./advanced_blobs.md): information about blobs diff --git a/book/src/advanced_blobs.md b/book/src/advanced_blobs.md index 524f70219f..e06bdb9fb9 100644 --- a/book/src/advanced_blobs.md +++ b/book/src/advanced_blobs.md @@ -1,4 +1,27 @@ -# Blobs +# Data columns + +With the [Fusaka](https://ethereum.org/roadmap/fusaka) upgrade, the main feature [PeerDAS](https://ethereum.org/roadmap/fusaka#peerdas) allows storing only a portion of blob data, known as data columns, thus reducing the storage and bandwidth requirements of a full node. This however also means that a full node will not be able to serve blobs after Fusaka. To continue serving blobs, run the beacon node with `--semi-supernode` or `--supernode`. Note that this comes at a significant increase in storage and bandwidth requirements, see [this blog post about PeerDAS](https://blog.sigmaprime.io/peerdas-distributed-blob-building.html) and [Fusaka bandwidth estimation](https://ethpandaops.io/posts/fusaka-bandwidth-estimation/) for more details. + +> Note: the above assumes that the beacon node has no attached validators. If the beacon node has attached validators, then it is required to custody (store) a certain number of data columns which increases with the number of staked ETH. For example, if the staked ETH is >= 2048 ETH, then due to custody requirement, it will make the beacon node a semi-supernode ; if >= 4096 ETH, the beacon node will be a supernode without needing the flag. + +Table below summarizes the role of relevant flags in Lighthouse beacon node: + +| | Post-Deneb, Pre-Fulu | | Post-Fulu | | +|---------------------|-------------------------------------------|--------------------------------------------------------------|--------------------------------------------------|----------------------------------------------------------| +| Flag | Usage | Can serve blobs? | Usage | Can serve blobs? | +| --prune-blobs false | Does not prune blobs since using the flag | Yes, for blobs since using the flag and for the past 18 days | Does not prune data columns since using the flag | No | +| --semi-supernode | - | - | Store half data columns | Yes, for blobs since using the flag for a max of 18 days | +| --supernode | - | - | Store all data columns | Yes, for blobs since using the flag for a max of 18 days | + +While both `--supernode` and `--semi-supernode` can serve blobs, a supernode will be faster to respond to blobs queries as it skips the blob reconstruction step. Running a supernode also helps the network by serving the data columns to its peers. + +Combining `--prune-blobs false` and `--supernode` (or `--semi-supernode`) implies that no data columns will be pruned, and the node will be able to serve blobs since using the flag. + +If you want historical blob data beyond the data availability period (18 days), you can backfill blobs or data columns with the experimental flag `--complete-blob-backfill`. However, do note that this is an experimental feature and it only works when the flag is present during a fresh checkpoint sync when the database is initialised. The flag will not backfill blobs if the node is already running (with an existing database). During blob backfill, the feature may cause some issues, e.g., the node may block most of its peers. + +**⚠️ The following section on Blobs is archived and not maintained as blobs are stored in the form of data columns after the Fulu fork ⚠️** + +## Blobs In the Deneb network upgrade, one of the changes is the implementation of EIP-4844, also known as [Proto-danksharding](https://blog.ethereum.org/2024/02/27/dencun-mainnet-announcement). Alongside with this, a new term named `blob` (binary large object) is introduced. Blobs are "side-cars" carrying transaction data in a block. They are mainly used by Ethereum layer 2 operators. As far as stakers are concerned, the main difference with the introduction of blobs is the increased storage requirement. diff --git a/book/src/advanced_builders.md b/book/src/advanced_builders.md index d9468898b4..7202bd7bb9 100644 --- a/book/src/advanced_builders.md +++ b/book/src/advanced_builders.md @@ -60,7 +60,7 @@ relays, run one of the following services and configure lighthouse to use it wit ## Validator Client Configuration In the validator client you can configure gas limit and fee recipient on a per-validator basis. If no gas limit is -configured, Lighthouse will use a default gas limit of 30,000,000, which is the current default value used in execution +configured, Lighthouse will use a default gas limit of 60,000,000, which is the current default value used in execution engines. You can also enable or disable use of external builders on a per-validator basis rather than using `--builder-proposals`, `--builder-boost-factor` or `--prefer-builder-proposals`, which apply builder related preferences for all validators. In order to manage these configurations per-validator, you can either make updates to the `validator_definitions.yml` file @@ -75,7 +75,7 @@ transaction within the block to the fee recipient, so a discrepancy in fee recip is something afoot. > Note: The gas limit configured here is effectively a vote on block size, so the configuration should not be taken lightly. -> 30,000,000 is currently seen as a value balancing block size with how expensive it is for +> 60,000,000 is currently seen as a value balancing block size with how expensive it is for > the network to validate blocks. So if you don't feel comfortable making an informed "vote", using the default value is > encouraged. We will update the default value if the community reaches a rough consensus on a new value. @@ -114,7 +114,7 @@ Each field is optional. ```json { "builder_proposals": true, - "gas_limit": 30000001 + "gas_limit": 45000001 } ``` @@ -127,7 +127,7 @@ curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf4 -H "Content-Type: application/json" \ -d '{ "builder_proposals": true, - "gas_limit": 30000001 + "gas_limit": 45000001 }' | jq ``` @@ -161,7 +161,7 @@ You can also directly configure these fields in the `validator_definitions.yml` voting_keystore_path: /home/paul/.lighthouse/validators/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007/voting-keystore.json voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 suggested_fee_recipient: "0x6cc8dcbca744a6e4ffedb98e1d0df903b10abd21" - gas_limit: 30000001 + gas_limit: 45000001 builder_proposals: true builder_boost_factor: 50 - enabled: false diff --git a/book/src/advanced_checkpoint_sync.md b/book/src/advanced_checkpoint_sync.md index 45aed6ef58..7c30598928 100644 --- a/book/src/advanced_checkpoint_sync.md +++ b/book/src/advanced_checkpoint_sync.md @@ -82,7 +82,7 @@ Once backfill is complete, a `INFO Historical block download complete` log will 1. What if I have an existing database? How can I use checkpoint sync? The existing beacon database needs to be deleted before Lighthouse will attempt checkpoint sync. - You can do this by providing the `--purge-db` flag, or by manually deleting `/beacon`. + You can do this by providing the `--purge-db-force` flag, or by manually deleting `/beacon`. 1. Why is checkpoint sync faster? @@ -92,7 +92,7 @@ Once backfill is complete, a `INFO Historical block download complete` log will No, in fact it is more secure! Checkpoint sync guards against long-range attacks that genesis sync does not. This is due to a property of Proof of Stake consensus known as [Weak Subjectivity][weak-subj]. -## Reconstructing States +## How to run an archived node > This section is only relevant if you are interested in running an archival node for analysis > purposes. @@ -101,7 +101,7 @@ After completing backfill sync the node's database will differ from a genesis-sy lack of historic states. _You do not need these states to run a staking node_, but they are required for historical API calls (as used by block explorers and researchers). -You can opt-in to reconstructing all of the historic states by providing the +To run an archived node, you can opt-in to reconstructing all of the historic states by providing the `--reconstruct-historic-states` flag to the beacon node at any point (before, during or after sync). The database keeps track of three markers to determine the availability of historic blocks and @@ -155,12 +155,12 @@ The command is as following: ```bash curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v2/debug/beacon/states/$SLOT" > state.ssz curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v2/beacon/blocks/$SLOT" > block.ssz -curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v1/beacon/blob_sidecars/$SLOT" > blobs.ssz +curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v1/beacon/blobs/$SLOT" > blobs.ssz ``` where `$SLOT` is the slot number. A slot which is an epoch boundary slot (i.e., first slot of an epoch) should always be used for manual checkpoint sync. -If the block contains blobs, all state, block and blobs must be provided and must point to the same slot. The +If the block contains blobs, all state, block and blobs must be provided and must point to the same slot (only applies for slots before Fulu). The state may be from the same slot as the block (unadvanced), or advanced to an epoch boundary, in which case it will be assumed to be finalized at that epoch. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 4e77046c2d..1643736794 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -63,11 +63,11 @@ that we have observed are: The following table lists the data for different configurations. Note that the disk space requirement is for the `chain_db` and `freezer_db`, excluding the `blobs_db`. -| Hierarchy Exponents | Storage Requirement | Sequential Slot Query | Uncached Query | Time to Sync | -|---|---|---|---|---| -| 5,9,11,13,16,18,21 (default) | 418 GiB | 250-700 ms | up to 10 s | 1 week | -| 5,7,11 (frequent snapshots) | 589 GiB | 250-700 ms | up to 6 s | 1 week | -| 0,5,7,11 (per-slot diffs) | 2500 GiB | 250-700 ms | up to 4 s | 7 weeks | +| Hierarchy Exponents | Storage Requirement | Sequential Slot Query | Uncached Query | Time to Sync | +|------------------------------|---------------------|-----------------------|----------------|--------------| +| 5,9,11,13,16,18,21 (default) | 418 GiB | 250-700 ms | up to 10 s | 1 week | +| 5,7,11 (frequent snapshots) | 589 GiB | 250-700 ms | up to 6 s | 1 week | +| 0,5,7,11 (per-slot diffs) | 2500 GiB | 250-700 ms | up to 4 s | 7 weeks | [Jim](https://github.com/mcdee) has done some experiments to study the response time of querying random slots (uncached query) for `--hierarchy-exponents 0,5,7,11` (per-slot diffs) and `--hierarchy-exponents 5,9,11,13,17,21` (per-epoch diffs), as show in the figures below. From the figures, two points can be concluded: diff --git a/book/src/advanced_database_migrations.md b/book/src/advanced_database_migrations.md index e9954e2ad9..115a885878 100644 --- a/book/src/advanced_database_migrations.md +++ b/book/src/advanced_database_migrations.md @@ -17,6 +17,9 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| +| v8.0.0 | Nov 2025 | v28 | yes before Fulu | +| v8.0.0-rc.0 | Sep 2025 | v28 | yes before Fulu | +| v7.1.0 | Jul 2025 | v26 | yes | | v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | @@ -206,6 +209,8 @@ Here are the steps to prune historic states: | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|-------------------------------------| +| v8.0.0-rc.0 | Sep 2025 | v28 | yes before Fulu | +| v7.1.0 | Jul 2025 | v26 | yes | | v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | | v5.3.0 | Aug 2024 | v21 | yes before Electra using <= v7.0.0 | diff --git a/book/src/advanced_proposer_only.md b/book/src/advanced_proposer_only.md index f55e51606c..1ef7a06655 100644 --- a/book/src/advanced_proposer_only.md +++ b/book/src/advanced_proposer_only.md @@ -23,9 +23,7 @@ normal activities such as performing attestations, but it will make the node harder to identify as a potential node to attack and will also consume less resources. -Specifically, this flag reduces the default peer count (to a safe minimal -number as maintaining peers on attestation subnets do not need to be considered), -prevents the node from subscribing to any attestation-subnets or +Specifically, this flag prevents the node from subscribing to any attestation-subnets or sync-committees which is a primary way for attackers to de-anonymize validators. diff --git a/book/src/advanced_re-orgs.md b/book/src/advanced_re-orgs.md index fca156bda3..3a31778786 100644 --- a/book/src/advanced_re-orgs.md +++ b/book/src/advanced_re-orgs.md @@ -2,6 +2,9 @@ Since v3.4.0 Lighthouse will opportunistically re-org late blocks when proposing. +When Lighthouse is about to propose a new block, it quickly checks whether the block from the previous slot landed so late that hardly anyone attested to it. +If that late block looks weak enough, Lighthouse may decide to “re-org” it away: instead of building on it, Lighthouse builds its new block on the grand-parent block, turning the late block into an orphan. + This feature is intended to disincentivise late blocks and improve network health. Proposing a re-orging block is also more profitable for the proposer because it increases the number of attestations and transactions that can be included. diff --git a/book/src/api_lighthouse.md b/book/src/api_lighthouse.md index b65bef4762..0442bf4ec0 100644 --- a/book/src/api_lighthouse.md +++ b/book/src/api_lighthouse.md @@ -353,126 +353,6 @@ See [Validator Inclusion APIs](./api_validator_inclusion.md). See [Validator Inclusion APIs](./api_validator_inclusion.md). -## `/lighthouse/eth1/syncing` - -Returns information regarding execution layer, as it is required for use in -consensus layer - -### Fields - -- `head_block_number`, `head_block_timestamp`: the block number and timestamp -from the very head of the execution chain. Useful for understanding the immediate -health of the execution node that the beacon node is connected to. -- `latest_cached_block_number` & `latest_cached_block_timestamp`: the block -number and timestamp of the latest block we have in our block cache. - - For correct execution client voting this timestamp should be later than the -`voting_target_timestamp`. - -- `voting_target_timestamp`: The latest timestamp allowed for an execution layer block in this voting period. -- `eth1_node_sync_status_percentage` (float): An estimate of how far the head of the - execution node is from the head of the execution chain. - - `100.0` indicates a fully synced execution node. - - `0.0` indicates an execution node that has not verified any blocks past the - genesis block. -- `lighthouse_is_cached_and_ready`: Is set to `true` if the caches in the - beacon node are ready for block production. - - This value might be set to - `false` whilst `eth1_node_sync_status_percentage == 100.0` if the beacon - node is still building its internal cache. - - This value might be set to `true` whilst - `eth1_node_sync_status_percentage < 100.0` since the cache only cares - about blocks a certain distance behind the head. - -### Example - -```bash -curl -X GET "http://localhost:5052/lighthouse/eth1/syncing" -H "accept: application/json" | jq -``` - -```json -{ - "data": { - "head_block_number": 3611806, - "head_block_timestamp": 1603249317, - "latest_cached_block_number": 3610758, - "latest_cached_block_timestamp": 1603233597, - "voting_target_timestamp": 1603228632, - "eth1_node_sync_status_percentage": 100, - "lighthouse_is_cached_and_ready": true - } -} -``` - -## `/lighthouse/eth1/block_cache` - -Returns a list of all the execution layer blocks in the execution client voting cache. - -### Example - -```bash -curl -X GET "http://localhost:5052/lighthouse/eth1/block_cache" -H "accept: application/json" | jq -``` - -```json -{ - "data": [ - { - "hash": "0x3a17f4b7ae4ee57ef793c49ebc9c06ff85207a5e15a1d0bd37b68c5ef5710d7f", - "timestamp": 1603173338, - "number": 3606741, - "deposit_root": "0xd24920d936e8fb9b67e93fd126ce1d9e14058b6d82dcf7d35aea46879fae6dee", - "deposit_count": 88911 - }, - { - "hash": "0x78852954ea4904e5f81038f175b2adefbede74fbb2338212964405443431c1e7", - "timestamp": 1603173353, - "number": 3606742, - "deposit_root": "0xd24920d936e8fb9b67e93fd126ce1d9e14058b6d82dcf7d35aea46879fae6dee", - "deposit_count": 88911 - } - ] -} -``` - -## `/lighthouse/eth1/deposit_cache` - -Returns a list of all cached logs from the deposit contract. - -### Example - -```bash -curl -X GET "http://localhost:5052/lighthouse/eth1/deposit_cache" -H "accept: application/json" | jq -``` - -```json -{ - "data": [ - { - "deposit_data": { - "pubkey": "0xae9e6a550ac71490cdf134533b1688fcbdb16f113d7190eacf4f2e9ca6e013d5bd08c37cb2bde9bbdec8ffb8edbd495b", - "withdrawal_credentials": "0x0062a90ebe71c4c01c4e057d7d13b944d9705f524ebfa24290c22477ab0517e4", - "amount": "32000000000", - "signature": "0xa87a4874d276982c471e981a113f8af74a31ffa7d18898a02df2419de2a7f02084065784aa2f743d9ddf80952986ea0b012190cd866f1f2d9c633a7a33c2725d0b181906d413c82e2c18323154a2f7c7ae6f72686782ed9e423070daa00db05b" - }, - "block_number": 3086571, - "index": 0, - "signature_is_valid": false - }, - { - "deposit_data": { - "pubkey": "0xb1d0ec8f907e023ea7b8cb1236be8a74d02ba3f13aba162da4a68e9ffa2e395134658d150ef884bcfaeecdf35c286496", - "withdrawal_credentials": "0x00a6aa2a632a6c4847cf87ef96d789058eb65bfaa4cc4e0ebc39237421c22e54", - "amount": "32000000000", - "signature": "0x8d0f8ec11935010202d6dde9ab437f8d835b9cfd5052c001be5af9304f650ada90c5363022e1f9ef2392dd222cfe55b40dfd52578468d2b2092588d4ad3745775ea4d8199216f3f90e57c9435c501946c030f7bfc8dbd715a55effa6674fd5a4" - }, - "block_number": 3086579, - "index": 1, - "signature_is_valid": false - } - ] -} -``` - ## `/lighthouse/liveness` POST request that checks if any of the given validators have attested in the given epoch. Returns a list @@ -565,7 +445,38 @@ For archive nodes, the `anchor` will be: indicating that all states with slots `>= 0` are available, i.e., full state history. For more information on the specific meanings of these fields see the docs on [Checkpoint -Sync](./advanced_checkpoint_sync.md#reconstructing-states). +Sync](./advanced_checkpoint_sync.md#how-to-run-an-archived-node). + +## `/lighthouse/custody/info` + +Information about data columns custody info. + +```bash +curl "http://localhost:5052/lighthouse/custody/info" | jq +``` + +```json +{ + "earliest_custodied_data_column_slot": "8823040", + "custody_group_count": "4", + "custody_columns": [ + "117", + "72", + "31", + "79" + ] +} +``` + +## `/lighthouse/custody/backfill` + +Starts a custody backfill sync from the next epoch with the node's latest custody requirements. The sync won't begin immediately, it waits until the next epoch is finalized before triggering. + +This endpoint should only be used to fix nodes that may have partial custody columns due to a prior backfill bug (present in v8.0.0-rc.2). Use with caution as it re-downloads all historic custody data columns and may consume significant bandwidth. + +```bash +curl -X POST "http://localhost:5052/lighthouse/custody/backfill" +``` ## `/lighthouse/merge_readiness` diff --git a/book/src/api_vc_endpoints.md b/book/src/api_vc_endpoints.md index 87c9a517a5..d128b13b2f 100644 --- a/book/src/api_vc_endpoints.md +++ b/book/src/api_vc_endpoints.md @@ -19,6 +19,7 @@ | [`POST /lighthouse/validators/web3signer`](#post-lighthousevalidatorsweb3signer) | Add web3signer validators. | | [`GET /lighthouse/logs`](#get-lighthouselogs) | Get logs | | [`GET /lighthouse/beacon/health`](#get-lighthousebeaconhealth) | Get health information for each connected beacon node. | +| [`POST /lighthouse/beacon/update`](#post-lighthousebeaconupdate) | Update the `--beacon-nodes` list. | The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api_vc_auth_header.md). @@ -131,7 +132,7 @@ Returns information regarding the health of the host machine. | Property | Specification | |-------------------|--------------------------------------------| -| Path | `/lighthouse/ui/health` | +| Path | `/lighthouse/ui/health` | | Method | GET | | Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | @@ -177,7 +178,7 @@ Returns the graffiti that will be used for the next block proposal of each valid | Property | Specification | |-------------------|--------------------------------------------| -| Path | `/lighthouse/ui/graffiti` | +| Path | `/lighthouse/ui/graffiti` | | Method | GET | | Required Headers | [`Authorization`](./api_vc_auth_header.md) | | Typical Responses | 200 | @@ -926,3 +927,57 @@ curl -X GET http://localhost:5062/lighthouse/beacon/health \ } } ``` + +## `POST /lighthouse/beacon/update` + +Updates the list of beacon nodes originally specified by the `--beacon-nodes` CLI flag. +Use this endpoint when you don't want to restart the VC to add, remove or reorder beacon nodes. + +### HTTP Specification + +| Property | Specification | +|-------------------|--------------------------------------------| +| Path | `/lighthouse/beacon/update` | +| Method | POST | +| Required Headers | [`Authorization`](./api_vc_auth_header.md) | +| Typical Responses | 200, 400 | + +### Example Request Body + +```json +{ + "beacon_nodes": [ + "http://beacon-node1:5052", + "http://beacon-node2:5052", + "http://beacon-node3:5052" + ] +} +``` + +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/beacon/update \ + -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ + -H "Content-Type: application/json" \ + -d "{\"beacon_nodes\":[\"http://beacon-node1:5052\",\"http://beacon-node2:5052\",\"http://beacon-node3:5052\"]}" +``` + +### Example Response Body + +```json +{ + "data": { + "new_beacon_nodes_list": [ + "http://beacon-node1:5052", + "http://beacon-node2:5052", + "http://beacon-node3:5052" + ] + } +} +``` + +If successful, the response will be a copy of the new list included in the request. +If unsuccessful, an error will be shown and the beacon nodes list will not be updated. +You can verify the results of the endpoint by using the `/lighthouse/beacon/health` endpoint. diff --git a/book/src/archived_key_management.md b/book/src/archived_key_management.md index d8b00e8352..ad285ac4ec 100644 --- a/book/src/archived_key_management.md +++ b/book/src/archived_key_management.md @@ -21,7 +21,7 @@ using Lighthouse. Rather than continuing to read this page, we recommend users visit either: - The [Staking Launchpad][launchpad] for detailed, beginner-friendly instructions. -- The [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) for a CLI tool used by the [Staking Launchpad][launchpad]. +- The [ethstaker-deposit-cli](https://github.com/eth-educators/ethstaker-deposit-cli/releases) for a CLI tool used by the [Staking Launchpad][launchpad]. - The [validator-manager documentation](./validator_manager.md) for a Lighthouse-specific tool for streamlined validator management tools. ## The `lighthouse account-manager` diff --git a/book/src/archived_merge_migration.md b/book/src/archived_merge_migration.md index ac9c78c5e3..b983db23ae 100644 --- a/book/src/archived_merge_migration.md +++ b/book/src/archived_merge_migration.md @@ -25,14 +25,14 @@ All networks (**Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln
-| Network | Bellatrix | The Merge | Remark | -|---------|-------------------------------|-------------------------------| -----------| -| Ropsten | 2nd June 2022 | 8th June 2022 | Deprecated | -| Sepolia | 20th June 2022 | 6th July 2022 | | -| Goerli | 4th August 2022 | 10th August 2022 | Previously named `Prater`| -| Mainnet | 6th September 2022| 15th September 2022| | -| Chiado | 10th October 2022 | 4th November 2022 | | -| Gnosis | 30th November 2022| 8th December 2022 | | +| Network | Bellatrix | The Merge | Remark | +|---------|-------------------------------|--------------------------------|---------------------------| +| Ropsten | 2nd June 2022 | 8th June 2022 | Deprecated | +| Sepolia | 20th June 2022 | 6th July 2022 | | +| Goerli | 4th August 2022 | 10th August 2022 | Previously named `Prater` | +| Mainnet | 6th September 2022 | 15th September 2022 | | +| Chiado | 10th October 2022 | 4th November 2022 | | +| Gnosis | 30th November 2022 | 8th December 2022 | |
diff --git a/book/src/contributing_setup.md b/book/src/contributing_setup.md index 7143c8f0fb..958e8f71f6 100644 --- a/book/src/contributing_setup.md +++ b/book/src/contributing_setup.md @@ -26,7 +26,7 @@ you can run them locally and avoid CI failures: - `$ make cargo-fmt`: (fast) runs a Rust code formatting check. - `$ make lint`: (fast) runs a Rust code linter. -- `$ make test`: (medium) runs unit tests across the whole project. +- `$ make test`: (medium) runs unit tests across the whole project using nextest. - `$ make test-ef`: (medium) runs the Ethereum Foundation test vectors. - `$ make test-full`: (slow) runs the full test suite (including all previous commands). This is approximately everything @@ -36,88 +36,80 @@ _The lighthouse test suite is quite extensive, running the whole suite may take ## Testing -As with most other Rust projects, Lighthouse uses `cargo test` for unit and -integration tests. For example, to test the `ssz` crate run: +Lighthouse uses `cargo nextest` for unit and integration tests. Nextest provides better parallelization and is used by CI. For example, to test the `safe_arith` crate run: ```bash -$ cd consensus/ssz -$ cargo test - Finished test [unoptimized + debuginfo] target(s) in 7.69s - Running unittests (target/debug/deps/ssz-61fc26760142b3c4) - -running 27 tests -test decode::impls::tests::awkward_fixed_length_portion ... ok -test decode::impls::tests::invalid_h256 ... ok - -test encode::tests::test_encode_length ... ok -test encode::impls::tests::vec_of_vec_of_u8 ... ok -test encode::tests::test_encode_length_above_max_debug_panics - should panic ... ok - -test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - Running tests/tests.rs (target/debug/deps/tests-f8fb1f9ccb197bf4) - -running 20 tests -test round_trip::bool ... ok -test round_trip::first_offset_skips_byte ... ok -test round_trip::fixed_len_excess_bytes ... ok - -test round_trip::vec_u16 ... ok -test round_trip::vec_of_vec_u16 ... ok - -test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s - - Doc-tests ssz - -running 3 tests -test src/decode.rs - decode::SszDecoder (line 258) ... ok -test src/encode.rs - encode::SszEncoder (line 57) ... ok -test src/lib.rs - (line 10) ... ok - -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s$ cargo test -p eth2_ssz +$ cd consensus/safe_arith +$ cargo nextest run + Finished test [unoptimized + debuginfo] target(s) in 0.43s + ------------ + Nextest run ID: 01234567-89ab-cdef-0123-456789abcdef + Starting 8 tests across 1 binary + PASS [ 0.001s] safe_arith tests::test_safe_add_u64 + PASS [ 0.001s] safe_arith tests::test_safe_mul_u64 + + ------------ + Summary [ 0.012s] 8 tests run: 8 passed, 0 skipped ``` -Alternatively, since `lighthouse` is a cargo workspace you can use `-p eth2_ssz` where -`eth2_ssz` is the package name as defined `/consensus/ssz/Cargo.toml` +Alternatively, since `lighthouse` is a cargo workspace you can use `-p safe_arith` where +`safe_arith` is the package name as defined in `/consensus/safe_arith/Cargo.toml`: ```bash -$ head -2 consensus/ssz/Cargo.toml +$ head -2 consensus/safe_arith/Cargo.toml [package] -name = "eth2_ssz" -$ cargo test -p eth2_ssz - Finished test [unoptimized + debuginfo] target(s) in 7.69s - Running unittests (target/debug/deps/ssz-61fc26760142b3c4) +name = "safe_arith" +$ cargo nextest run -p safe_arith + Finished test [unoptimized + debuginfo] target(s) in 0.43s + ------------ + Nextest run ID: 01234567-89ab-cdef-0123-456789abcdef + Starting 8 tests across 1 binary + PASS [ 0.001s] safe_arith tests::test_safe_add_u64 + PASS [ 0.001s] safe_arith tests::test_safe_mul_u64 + + ------------ + Summary [ 0.012s] 8 tests run: 8 passed, 0 skipped +``` -running 27 tests -test decode::impls::tests::awkward_fixed_length_portion ... ok -test decode::impls::tests::invalid_h256 ... ok - -test encode::tests::test_encode_length ... ok -test encode::impls::tests::vec_of_vec_of_u8 ... ok -test encode::tests::test_encode_length_above_max_debug_panics - should panic ... ok +### Integration tests -test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +Due to the size and complexity of the test suite, Lighthouse uses a pattern that differs from how +[integration tests are usually defined](https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html). +This pattern helps manage large test suites more effectively and ensures tests only run in release +mode to avoid stack overflow issues. - Running tests/tests.rs (target/debug/deps/tests-f8fb1f9ccb197bf4) +#### The "main pattern" -running 20 tests -test round_trip::bool ... ok -test round_trip::first_offset_skips_byte ... ok -test round_trip::fixed_len_excess_bytes ... ok - -test round_trip::vec_u16 ... ok -test round_trip::vec_of_vec_u16 ... ok +For packages with integration tests that require more than one file, Lighthouse uses the following +structure: -test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +- A `main.rs` file is defined at `package/tests/main.rs` that declares other test files as modules +- In `package/Cargo.toml`, integration tests are explicitly configured: - Doc-tests ssz + ```toml + [package] + autotests = false -running 3 tests -test src/decode.rs - decode::SszDecoder (line 258) ... ok -test src/encode.rs - encode::SszEncoder (line 57) ... ok -test src/lib.rs - (line 10) ... ok + [[test]] + name = "package_tests" + path = "tests/main.rs" + ``` -test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s$ cargo test -p eth2_ssz +#### Rust Analyzer configuration + +This pattern, combined with `#![cfg(not(debug_assertions))]` directives in test files (which +prevent tests from running in debug mode), causes Rust Analyzer to not provide IDE services like +autocomplete and error checking in integration test files by default. + +To enable IDE support for these test files, configure Rust Analyzer to disable debug assertions. +For VSCode users, this is already configured in the repository's `.vscode/settings.json` file: + +```json +{ + "rust-analyzer.cargo.cfgs": [ + "!debug_assertions" + ] +} ``` ### test_logger @@ -129,7 +121,7 @@ testing the logs are displayed. This can be very helpful while debugging tests. Example: ``` -$ cargo test -p beacon_chain validator_pubkey_cache::test::basic_operation --features 'logging/test_logger' +$ cargo nextest run -p beacon_chain -E 'test(validator_pubkey_cache::test::basic_operation)' --features 'logging/test_logger' Finished test [unoptimized + debuginfo] target(s) in 0.20s Running unittests (target/debug/deps/beacon_chain-975363824f1143bc) diff --git a/book/src/faq.md b/book/src/faq.md index b97a82fcca..c9bc53533f 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -2,7 +2,6 @@ ## [Beacon Node](#beacon-node-1) -- [I see a warning about "Syncing deposit contract block cache" or an error about "updating deposit contract cache", what should I do?](#bn-deposit-contract) - [I see beacon logs showing `WARN: Execution engine called failed`, what should I do?](#bn-ee) - [I see beacon logs showing `Error during execution engine upcheck`, what should I do?](#bn-upcheck) - [My beacon node is stuck at downloading historical block using checkpoint sync. What should I do?](#bn-download-historical) @@ -14,6 +13,7 @@ - [My beacon node logs `WARN Error signalling fork choice waiter`, what should I do?](#bn-fork-choice) - [My beacon node logs `ERRO Aggregate attestation queue full`, what should I do?](#bn-queue-full) - [My beacon node logs `WARN Failed to finalize deposit cache`, what should I do?](#bn-deposit-cache) +- [How can I construct only partial state history?](#bn-partial-history) ## [Validator](#validator-1) @@ -50,31 +50,6 @@ ## Beacon Node -### I see a warning about "Syncing deposit contract block cache" or an error about "updating deposit contract cache", what should I do? - -The error can be a warning: - -```text -Nov 30 21:04:28.268 WARN Syncing deposit contract block cache est_blocks_remaining: initializing deposits, service: slot_notifier -``` - -or an error: - -```text -ERRO Error updating deposit contract cache error: Failed to get remote head and new block ranges: EndpointError(FarBehind), retry_millis: 60000, service: deposit_contract_rpc -``` - -This log indicates that your beacon node is downloading blocks and deposits -from your execution node. When the `est_blocks_remaining` is -`initializing_deposits`, your node is downloading deposit logs. It may stay in -this stage for several minutes. Once the deposits logs are finished -downloading, the `est_blocks_remaining` value will start decreasing. - -It is perfectly normal to see this log when starting a node for the first time -or after being off for more than several minutes. - -If this log continues appearing during operation, it means your execution client is still syncing and it cannot provide Lighthouse the information about the deposit contract yet. What you need to do is to make sure that the execution client is up and syncing. Once the execution client is synced, the error will disappear. - ### I see beacon logs showing `WARN: Execution engine called failed`, what should I do? The `WARN Execution engine called failed` log is shown when the beacon node cannot reach the execution engine. When this warning occurs, it will be followed by a detailed message. A frequently encountered example of the error message is: @@ -190,6 +165,40 @@ If the node is syncing or downloading historical blocks, the error should disapp This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will fix by itself. +### How can I construct only partial state history? + +Lighthouse prunes finalized states by default. Nevertheless, it is quite often that users may be interested in the state history of a few epochs before finalization. To have access to these pruned states, Lighthouse typically requires a full reconstruction of states using the flag `--reconstruct-historic-states` (which will usually take a week). Partial state history can be achieved with some "tricks". Here are the general steps: + + 1. Delete the current database. You can do so with `--purge-db-force` or manually deleting the database from the data directory: `$datadir/beacon`. + + 1. If you are interested in the states from the current slot and beyond, perform a checkpoint sync with the flag `--reconstruct-historic-states`, then you can skip the following and jump straight to Step 5 to check the database. + + If you are interested in the states before the current slot, identify the slot to perform a manual checkpoint sync. With the default configuration, this slot should be divisible by 221, as this is where a full state snapshot is stored. With the flag `--reconstruct-historic-states`, the state upper limit will be adjusted to the next full snapshot slot, a slot that satisfies: `slot % 2**21 == 0`. In other words, to have the state history available before the current slot, we have to checkpoint sync 221 slots before the next full snapshot slot. + + Example: Say the current mainnet is at slot `12000000`. As the next full state snapshot is at slot `12582912`, the slot that we want is slot `10485760`. You can calculate this (in Python) using `12000000 // 2**21 * 2**21`. + + 1. [Export](./advanced_checkpoint_sync.md#manual-checkpoint-sync) the blobs, block and state data for the slot identified in Step 2. This can be done from another beacon node that you have access to, or you could use any available public beacon API, e.g., [QuickNode](https://www.quicknode.com/docs/ethereum). + + 1. Perform a [manual checkpoint sync](./advanced_checkpoint_sync.md#manual-checkpoint-sync) using the data from the previous step, and provide the flag `--reconstruct-historic-states`. + + 1. Check the database: + + ```bash + curl "http://localhost:5052/lighthouse/database/info" | jq '.anchor' + ``` + + and look for the field `state_upper_limit`. It should show the slot of the snapshot: + + ```json + "state_upper_limit": "10485760", + ``` + +Lighthouse will now start to reconstruct historic states from slot `10485760`. At this point, if you do not want a full state reconstruction, you may remove the flag `--reconstruct-historic-states` (and restart). When the process is completed, you will have the state data from slot `10485760`. Going forward, Lighthouse will continue retaining all historical states newer than the snapshot. Eventually this can lead to increased disk usage, which presently can only be reduced by repeating the process starting from a more recent snapshot. + +> Note: You may only be interested in very recent historic states. To do so, you may configure the full snapshot to be, for example, every 211 slots, see [database configuration](./advanced_database.md#hierarchical-state-diffs) for more details. This can be configured with the flag `--hierarchy-exponents 5,7,11` together with the flag `--reconstruct-historic-states`. This will affect the slot number in Step 2, while other steps remain the same. Note that this comes at the expense of a higher storage requirement. + +> With `--hierarchy-exponents 5,7,11`, using the same example as above, the next full state snapshot is at slot `12001280`. So the slot to checkpoint sync from is: slot `11999232`. + ## Validator ### Can I use redundancy in my staking setup? @@ -209,7 +218,7 @@ The first thing is to ensure both consensus and execution clients are synced wit - the internet is working well - you have sufficient peers -You can see more information on the [Ethstaker KB](https://ethstaker.gitbook.io/ethstaker-knowledge-base/help/missed-attestations). +You can see more information on the [EthStaker KB](https://ethstaker.gitbook.io/ethstaker-knowledge-base/help/missed-attestations). Another cause for missing attestations is the block arriving late, or there are delays during block processing. @@ -300,7 +309,7 @@ expect, there are a few things to check on: If you have incoming peers, it should return a lot of data containing information of peers. If the response is empty, it means that you have no incoming peers and there the ports are not open. You may want to double check if the port forward was correctly set up. -1. Check that you do not lower the number of peers using the flag `--target-peers`. The default is 100. A lower value set will lower the maximum number of peers your node can connect to, which may potentially interrupt the validator performance. We recommend users to leave the `--target peers` untouched to keep a diverse set of peers. +1. Check that you do not lower the number of peers using the flag `--target-peers`. The default is 200. A lower value set will lower the maximum number of peers your node can connect to, which may potentially interrupt the validator performance. We recommend users to leave the `--target peers` untouched to keep a diverse set of peers. 1. Ensure that you have a quality router for the internet connection. For example, if you connect the router to many devices including the node, it may be possible that the router cannot handle all routing tasks, hence struggling to keep up the number of peers. Therefore, using a quality router for the node is important to keep a healthy number of peers. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 35ad020b74..5f3c43a7e4 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -22,7 +22,7 @@ Options: Data directory for the blobs database. --block-cache-size Specifies how many blocks the database should cache in memory - [default: 5] + [default: 0] --boot-nodes One or more comma-delimited base64-encoded ENR's to bootstrap the p2p network. Multiaddr is also supported. @@ -122,15 +122,6 @@ Options: The number of epochs to wait between running the migration of data from the hot DB to the cold DB. Less frequent runs can be useful for minimizing disk writes [default: 1] - --eth1-blocks-per-log-query - Specifies the number of blocks that a deposit log query should span. - This will reduce the size of responses from the Eth1 endpoint. - [default: 1000] - --eth1-cache-follow-distance - Specifies the distance between the Eth1 chain head and the last block - which should be imported into the cache. Setting this value lower can - help compensate for irregular Proof-of-Work block times, but setting - it too low can make the node vulnerable to re-orgs. --execution-endpoint Server endpoint for an execution layer JWT-authenticated HTTP JSON-RPC connection. Uses the same endpoint to populate the deposit cache. @@ -171,10 +162,10 @@ Options: Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated to fit in 32 bytes. --hdiff-buffer-cache-size - Number of hierarchical diff (hdiff) buffers to cache in memory. Each - buffer is around the size of a BeaconState so you should be cautious - about setting this value too high. This flag is irrelevant for most - nodes, which run with state pruning enabled. [default: 16] + Number of cold hierarchical diff (hdiff) buffers to cache in memory. + Each buffer is around the size of a BeaconState so you should be + cautious about setting this value too high. This flag is irrelevant + for most nodes, which run with state pruning enabled. [default: 16] --hierarchy-exponents Specifies the frequency for storing full state snapshots and hierarchical diffs in the freezer DB. Accepts a comma-separated list @@ -187,6 +178,12 @@ Options: --historic-state-cache-size Specifies how many states from the freezer database should be cached in memory [default: 1] + --hot-hdiff-buffer-cache-size + Number of hot hierarchical diff (hdiff) buffers to cache in memory. + Each buffer is around the size of a BeaconState so you should be + cautious about setting this value too high. Setting this value higher + can reduce the time taken to store new states on disk at the cost of + higher memory usage. [default: 1] --http-address
Set the listen address for the RESTful HTTP API server. --http-allow-origin @@ -384,7 +381,7 @@ Options: Minimum number of states to cull from the state cache when it gets full [default: 1] --state-cache-size - Specifies the size of the state cache [default: 32] + Specifies the size of the state cache [default: 128] --suggested-fee-recipient Emergency fallback fee recipient for use in case the validator client does not have one configured. You should set this flag on the @@ -395,6 +392,13 @@ Options: database. --target-peers The target number of peers. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. --trusted-peers One or more comma-delimited trusted peer ids which always have the highest score according to the peer scoring system. @@ -448,15 +452,13 @@ Flags: resource contention which degrades staking performance. Stakers should generally choose to avoid this flag since backfill sync is not required for staking. - --disable-deposit-contract-sync - Explicitly disables syncing of deposit logs from the execution node. - This overrides any previous option that depends on it. Useful if you - intend to run a non-validating beacon node. --disable-enr-auto-update Discovery automatically updates the nodes local ENR with an external IP address and port as seen by other peers on the network. This disables this feature, fixing the ENR's IP/PORT to those specified on boot. + --disable-get-blobs + Disables the getBlobs optimisation to fetch blobs from the EL mempool --disable-inbound-rate-limiter Disables the inbound rate limiter (requests received by this node). --disable-light-client-server @@ -493,8 +495,6 @@ Flags: --enable-private-discovery Lighthouse by default does not discover private IP addresses. Set this flag to enable connection attempts to local addresses. - --eth1-purge-cache - Purges the eth1 block and deposit caches --genesis-backfill Attempts to download blocks all the way back to genesis when checkpoint syncing. @@ -513,8 +513,6 @@ Flags: subscriptions. This will only import attestations from already-subscribed subnets, use with --subscribe-all-subnets to ensure all attestations are received for import. - --light-client-server - DEPRECATED --log-color [] Enables/Disables colors for logs in terminal. Set it to false to disable colors. [default: true] [possible values: true, false] @@ -554,6 +552,12 @@ Flags: When present, Lighthouse will forget the payload statuses of any already-imported blocks. This can assist in the recovery from a consensus failure caused by the execution layer. + --semi-supernode + Run in minimal reconstruction mode. This node will subscribe to and + custody half of the data columns (enough for reconstruction), enabling + efficient data availability with lower bandwidth and storage + requirements compared to a supernode, while still supporting full blob + reconstruction. --shutdown-after-sync Shutdown beacon node as soon as sync is completed. Backfill sync will not be performed before shutdown. @@ -571,6 +575,13 @@ Flags: Subscribe to all subnets regardless of validator count. This will also advertise the beacon node as being long-lived subscribed to all subnets. + --supernode + Run as a voluntary supernode. This node will subscribe to all data + column subnets, custody all data columns, and perform reconstruction + and cross-seeding. This requires significantly more bandwidth, + storage, and computation requirements but the node will have direct + access to all blobs via the beacon API and it helps network resilience + by serving all data columns to syncing peers. --validator-monitor-auto Enables the automatic detection and monitoring of validators connected to the HTTP API and using the subnet subscription endpoint. This diff --git a/book/src/help_general.md b/book/src/help_general.md index fbc3ca2557..56e4aebdb5 100644 --- a/book/src/help_general.md +++ b/book/src/help_general.md @@ -76,6 +76,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. -V, --version Print version diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 15b5c209a7..2a9936d1d2 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -40,7 +40,7 @@ Options: The gas limit to be used in all builder proposals for all validators managed by this validator client. Note this will not necessarily be used if the gas limit set here moves too far from the previous block's - gas limit. [default: 36000000] + gas limit. [default: 60000000] --genesis-state-url A URL of a beacon-API compatible server from which to download the genesis state. Checkpoint sync server URLs can generally be used with @@ -134,6 +134,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. --validator-registration-batch-size Defines the number of validators per validator/register_validator request sent to the BN. This value can be reduced to avoid timeouts @@ -214,6 +221,10 @@ Flags: automatically enabled for <= 64 validators. Enabling this metric for higher validator counts will lead to higher volume of prometheus metrics being collected. + --graffiti-append + When used, client version info will be prepended to user custom + graffiti, with a space in between. This should only be used with a + Lighthouse beacon node. -h, --help Prints help information --http diff --git a/book/src/help_vm.md b/book/src/help_vm.md index 85e1a1168f..409c56a74d 100644 --- a/book/src/help_vm.md +++ b/book/src/help_vm.md @@ -12,7 +12,7 @@ Commands: data. This file can then be imported to a validator client using the "import-validators" command. Another, optional JSON file is created which contains a list of validator deposits in the same format as the - "ethereum/staking-deposit-cli" tool. + "ethstaker-deposit-cli" tool. import Uploads validators to a validator client using the HTTP API. The validators are defined in a JSON file which can be generated using the @@ -28,6 +28,10 @@ Commands: delete Deletes one or more validators from a validator client using the HTTP API. + exit + Exits one or more validators using the HTTP API. It can also be used + to generate a presigned voluntary exit message for a particular future + epoch. help Print this message or the help of the given subcommand(s) @@ -73,6 +77,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. Flags: --disable-log-timestamp diff --git a/book/src/help_vm_create.md b/book/src/help_vm_create.md index 3b88206397..a438c075dc 100644 --- a/book/src/help_vm_create.md +++ b/book/src/help_vm_create.md @@ -5,7 +5,7 @@ Creates new validators from BIP-39 mnemonic. A JSON file will be created which contains all the validator keystores and other validator data. This file can then be imported to a validator client using the "import-validators" command. Another, optional JSON file is created which contains a list of validator -deposits in the same format as the "ethereum/staking-deposit-cli" tool. +deposits in the same format as the "ethstaker-deposit-cli" tool. Usage: lighthouse validator_manager create [OPTIONS] --output-path @@ -93,6 +93,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. Flags: --disable-deposits diff --git a/book/src/help_vm_import.md b/book/src/help_vm_import.md index 63cca91ee5..3c768f6705 100644 --- a/book/src/help_vm_import.md +++ b/book/src/help_vm_import.md @@ -39,8 +39,7 @@ Options: [default: 300] --keystore-file The path to a keystore JSON file to be imported to the validator - client. This file is usually created using staking-deposit-cli or - ethstaker-deposit-cli + client. This file is usually created using ethstaker-deposit-cli --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] @@ -74,6 +73,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. --validators-file The path to a JSON file containing a list of validators to be imported to the validator client. This file is usually named "validators.json". diff --git a/book/src/help_vm_move.md b/book/src/help_vm_move.md index b7320ca290..cd139449b3 100644 --- a/book/src/help_vm_move.md +++ b/book/src/help_vm_move.md @@ -82,6 +82,13 @@ Options: Path to directory containing eth2_testnet specs. Defaults to a hard-coded Lighthouse testnet. Only effective if there is no existing database. + --telemetry-collector-url + URL of the OpenTelemetry collector to export tracing spans (e.g., + http://localhost:4317). If not set, tracing export is disabled. + --telemetry-service-name + Override the OpenTelemetry service name. Defaults to 'lighthouse-bn' + for beacon node, 'lighthouse-vc' for validator client, or 'lighthouse' + for other subcommands. --validators The validators to be moved. Either a list of 0x-prefixed validator pubkeys or the keyword "all". diff --git a/book/src/installation_binaries.md b/book/src/installation_binaries.md index e3a2bfb8a0..67a629e5c3 100644 --- a/book/src/installation_binaries.md +++ b/book/src/installation_binaries.md @@ -6,11 +6,11 @@ on Github](https://github.com/sigp/lighthouse/releases). ## Platforms -Binaries are supplied for four platforms: +Binaries are supplied for the following platforms: - `x86_64-unknown-linux-gnu`: AMD/Intel 64-bit processors (most desktops, laptops, servers) - `aarch64-unknown-linux-gnu`: 64-bit ARM processors (Raspberry Pi 4) -- `x86_64-apple-darwin`: macOS with Intel chips +- `aarch64-apple-darwin`: macOS with ARM chips - `x86_64-windows`: Windows with 64-bit processors ## Usage diff --git a/book/src/installation_source.md b/book/src/installation_source.md index 0aa8a99a5e..f035d1d843 100644 --- a/book/src/installation_source.md +++ b/book/src/installation_source.md @@ -166,8 +166,8 @@ Commonly used features include: - `slasher-lmdb`: support for the LMDB slasher backend. Enabled by default. - `slasher-mdbx`: support for the MDBX slasher backend. - `beacon-node-leveldb`: support for the leveldb backend. Enabled by default. -- `jemalloc`: use [`jemalloc`][jemalloc] to allocate memory. Enabled by default on Linux and macOS. - Not supported on Windows. +- `sysmalloc`: use the system memory allocator rather than jemalloc. This is always enabled on + Windows. - `spec-minimal`: support for the minimal preset (useful for testing). Default features (e.g. `slasher-lmdb`, `beacon-node-leveldb`) may be opted out of using the `--no-default-features` @@ -178,8 +178,6 @@ E.g. CARGO_INSTALL_EXTRA_FLAGS="--no-default-features" make ``` -[jemalloc]: https://jemalloc.net/ - ## Compilation Profiles You can customise the compiler settings used to compile Lighthouse via diff --git a/book/src/mainnet_validator.md b/book/src/mainnet_validator.md index 8da8b98f89..106461aa9b 100644 --- a/book/src/mainnet_validator.md +++ b/book/src/mainnet_validator.md @@ -42,7 +42,7 @@ hardware. 32 ETH is a significant outlay and joining a testnet is a great way to ### Step 1. Create validator keys -The Ethereum Foundation provides the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli/releases) for creating validator keys. Download and run the `staking-deposit-cli` with the command: +EthStaker provides the [ethstaker-deposit-cli](https://github.com/eth-educators/ethstaker-deposit-cli/releases) for creating validator keys. Download and run the `ethstaker-deposit-cli` with the command: ```bash ./deposit new-mnemonic @@ -52,7 +52,7 @@ and follow the instructions to generate the keys. When prompted for a network, s > **Important note:** A mnemonic (or seed phrase) is a 24-word string randomly generated in the process. It is highly recommended to write down the mnemonic and keep it safe offline. It is important to ensure that the mnemonic is never stored in any digital form (computers, mobile phones, etc) connected to the internet. Please also make one or more backups of the mnemonic to ensure your ETH is not lost in the case of data loss. It is very important to keep your mnemonic private as it represents the ultimate control of your ETH. -Upon completing this step, the files `deposit_data-*.json` and `keystore-m_*.json` will be created. The keys that are generated from staking-deposit-cli can be easily loaded into a Lighthouse validator client (`lighthouse vc`) in [Step 3](#step-3-import-validator-keys-to-lighthouse). In fact, both of these programs are designed to work with each other. +Upon completing this step, the files `deposit_data-*.json` and `keystore-m_*.json` will be created. The keys that are generated from `ethstaker-deposit-cli` can be easily loaded into a Lighthouse validator client (`lighthouse vc`) in [Step 3](#step-3-import-validator-keys-to-lighthouse). In fact, both of these programs are designed to work with each other. > Lighthouse also supports creating validator keys, see [Validator Manager Create](./validator_manager_create.md) for more info. @@ -62,19 +62,19 @@ Start an execution client and Lighthouse beacon node according to the [Run a Nod ### Step 3. Import validator keys to Lighthouse -In [Step 1](#step-1-create-validator-keys), the staking-deposit-cli will generate the validator keys into a `validator_keys` directory. Let's assume that -this directory is `$HOME/staking-deposit-cli/validator_keys`. Using the default `validators` directory in Lighthouse (`~/.lighthouse/mainnet/validators`), run the following command to import validator keys: +In [Step 1](#step-1-create-validator-keys), the `ethstaker-deposit-cli` will generate the validator keys into a `validator_keys` directory. Let's assume that +this directory is `$HOME/ethstaker-deposit-cli/validator_keys`. Using the default `validators` directory in Lighthouse (`~/.lighthouse/mainnet/validators`), run the following command to import validator keys: Mainnet: ```bash -lighthouse --network mainnet account validator import --directory $HOME/staking-deposit-cli/validator_keys +lighthouse --network mainnet account validator import --directory $HOME/ethstaker-deposit-cli/validator_keys ``` Hoodi testnet: ```bash -lighthouse --network hoodi account validator import --directory $HOME/staking-deposit-cli/validator_keys +lighthouse --network hoodi account validator import --directory $HOME/ethstaker-deposit-cli/validator_keys ``` > Note: The user must specify the consensus client network that they are importing the keys by using the `--network` flag. @@ -88,7 +88,7 @@ lighthouse --network hoodi account validator import --directory $HOME/staking-de The user will be prompted for a password for each keystore discovered: ``` -Keystore found at "/home/{username}/staking-deposit-cli/validator_keys/keystore-m_12381_3600_0_0_0-1595406747.json": +Keystore found at "/home/{username}/ethstaker-deposit-cli/validator_keys/keystore-m_12381_3600_0_0_0-1595406747.json": - Public key: 0xa5e8702533f6d66422e042a0bf3471ab9b302ce115633fa6fdc5643f804b6b4f1c33baf95f125ec21969a3b1e0dd9e56 - UUID: 8ea4cf99-8719-43c5-9eda-e97b8a4e074f diff --git a/book/src/run_a_node.md b/book/src/run_a_node.md index 6c43ef5e32..4d1f917fcb 100644 --- a/book/src/run_a_node.md +++ b/book/src/run_a_node.md @@ -78,12 +78,9 @@ lighthouse bn \ --network mainnet \ --execution-endpoint http://localhost:8551 \ --execution-jwt /secrets/jwt.hex \ - --checkpoint-sync-url https://mainnet.checkpoint.sigp.io \ - --disable-deposit-contract-sync + --checkpoint-sync-url https://mainnet.checkpoint.sigp.io ``` -Since we are not staking, we can use the `--disable-deposit-contract-sync` flag to disable syncing of deposit logs from the execution node. - Once Lighthouse runs, we can monitor the logs to see if it is syncing correctly. ## Step 4: Check logs for sync status @@ -109,7 +106,7 @@ Once the checkpoint is loaded, Lighthouse will sync forwards to the head of the If a validator client is connected to the beacon node it will be able to start its duties as soon as forwards sync completes, which typically takes 1-2 minutes. -> Note: If you have an existing Lighthouse database, you will need to delete the database by using the `--purge-db` flag or manually delete the database with `sudo rm -r /path_to_database/beacon`. If you do use a `--purge-db` flag, once checkpoint sync is complete, you can remove the flag upon a restart. +> Note: If you have an existing Lighthouse database, you will need to delete the database by using the `--purge-db-force` flag or manually delete the database with `sudo rm -r /path_to_database/beacon`. If you do use a `--purge-db-force` flag, once checkpoint sync is complete, you can remove the flag upon a restart. > **Security Note**: You should cross-reference the `block_root` and `slot` of the loaded checkpoint > against a trusted source like another [public endpoint](https://eth-clients.github.io/checkpoint-sync-endpoints/), diff --git a/book/src/ui_faqs.md b/book/src/ui_faqs.md index cbfaa2c430..d6b93e6012 100644 --- a/book/src/ui_faqs.md +++ b/book/src/ui_faqs.md @@ -30,9 +30,9 @@ Yes, if you need to access your beacon or validator from an address such as `htt If your graph is not showing data, it usually means your validator node is still caching data. The application must wait at least 3 epochs before it can render any graphical visualizations. This could take up to 20min. -## 8. How can I connect to Siren using Wallet Connect? +## 8. How can I connect to Siren using Reown (previously WalletConnect)? -Depending on your configuration, building with Docker or Local, you will need to include the `NEXT_PUBLIC_WALLET_CONNECT_ID` variable in your `.env` file. To obtain your Wallet Connect project ID, please follow the instructions on their [website](https://cloud.walletconnect.com/sign-in). After providing a valid project ID, the Wallet Connect option should appear in the wallet connector dropdown. +Depending on your configuration, building with Docker or Local, you will need to include the `NEXT_PUBLIC_WALLET_CONNECT_ID` variable in your `.env` file. To obtain your Wallet Connect project ID, please follow the instructions on their [website](https://dashboard.reown.com/sign-in). After providing a valid project ID, the Wallet Connect option should appear in the wallet connector dropdown. ## 9. I can't log in to Siren even with correct credentials? diff --git a/book/src/ui_installation.md b/book/src/ui_installation.md index df0522f07a..82f5d755bc 100644 --- a/book/src/ui_installation.md +++ b/book/src/ui_installation.md @@ -13,7 +13,7 @@ Siren requires a connection to both a Lighthouse Validator Client and a Lighthou Both the Beacon node and the Validator client need to have their HTTP APIs enabled. These ports should be accessible from Siren. This means adding the flag `--http` on both beacon node and validator client. -To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This action ensures that the HTTP API can be accessed by other software on the same machine. +To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This action ensures that the HTTP API can be accessed by other software on the same machine. It also enables the validator monitoring. > The Beacon Node must be run with the `--gui` flag set. @@ -138,13 +138,13 @@ Navigate to the backend directory `cd backend`. Install all required Node packag After initializing the backend, return to the root directory. Install all frontend dependencies by executing `yarn`. Build the frontend using `yarn build`. Start the frontend production server with `yarn start`. -This will allow you to access siren at `http://localhost:3000` by default. +This will allow you to access siren at `http://localhost:3300` by default. ## Advanced configuration ### About self-signed SSL certificates -By default, internally, Siren is running on port 80 (plain, behind nginx), port 3000 (plain, direct) and port 443 (with SSL, behind nginx)). Siren will generate and use a self-signed certificate on startup. This will generate a security warning when you try to access the interface. We recommend to only disable SSL if you would access Siren over a local LAN or otherwise highly trusted or encrypted network (i.e. VPN). +By default, internally, Siren is running on port 80 (plain, behind nginx), port 3300 (plain, direct) and port 443 (with SSL, behind nginx)). Siren will generate and use a self-signed certificate on startup. This will generate a security warning when you try to access the interface. We recommend to only disable SSL if you would access Siren over a local LAN or otherwise highly trusted or encrypted network (i.e. VPN). #### Generating persistent SSL certificates and installing them to your system diff --git a/book/src/validator_manager.md b/book/src/validator_manager.md index c610340b39..609f176901 100644 --- a/book/src/validator_manager.md +++ b/book/src/validator_manager.md @@ -15,7 +15,7 @@ except the latter creates files that will be read by the VC next time it starts whilst the former makes instant changes to a live VC. The `account-manager` is ideal for importing keys created with the -[staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli). On the +[ethstaker-deposit-cli](https://github.com/eth-educators/ethstaker-deposit-cli). On the other hand, the `validator-manager` is ideal for moving existing validators between two VCs or for advanced users to create validators at scale with less downtime. @@ -32,4 +32,4 @@ The `validator-manager` boasts the following features: - [Creating and importing validators using the `create` and `import` commands.](./validator_manager_create.md) - [Moving validators between two VCs using the `move` command.](./validator_manager_move.md) -- [Managing validators such as delete, import and list validators.](./validator_manager_api.md) +- [Managing validators such as exit, delete, import and list validators.](./validator_manager_api.md) diff --git a/book/src/validator_manager_api.md b/book/src/validator_manager_api.md index a5fc69fd5a..0542008463 100644 --- a/book/src/validator_manager_api.md +++ b/book/src/validator_manager_api.md @@ -1,6 +1,54 @@ # Managing Validators -The `lighthouse validator-manager` uses the [Keymanager API](https://ethereum.github.io/keymanager-APIs/#/) to list, import and delete keystores via the HTTP API. This requires the validator client running with the flag `--http`. +The `lighthouse validator-manager` uses the [Keymanager API](https://ethereum.github.io/keymanager-APIs/#/) to list, import and delete keystores via the HTTP API. This requires the validator client running with the flag `--http`. By default, the validator client HTTP address is `http://localhost:5062`. If a different IP address or port is used, add the flag `--vc-url http://IP:port_number` to the command below. + +## Exit + +The `exit` command exits one or more validators from the validator client. To `exit`: + +> **Important note: Once the --beacon-node flag is used, it will publish the voluntary exit to the network. This action is irreversible.** + +```bash +lighthouse vm exit --vc-token --validators pubkey1,pubkey2 --beacon-node http://beacon-node-url:5052 +``` + +Example: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f --beacon-node http://localhost:5052 +``` + +If successful, the following log will be returned: + +```text +Successfully validated and published voluntary exit for validator 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4 +Successfully validated and published voluntary exit for validator +0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f +``` + +To exit all validators on the validator client, use the keyword `all`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --beacon-node http://localhost:5052 +``` + +To check the voluntary exit status, refer to [the list command](./validator_manager_api.md#list). + +The following command will only generate a presigned voluntary exit message and save it to a file named `{validator_pubkey}.json`. It **will not** publish the voluntary exit to the network. + +To generate a presigned exit message and save it to a file, use the flag `--presign`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign +``` + +To generate a presigned exit message for a particular (future) epoch, use the flag `--exit-epoch`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign --exit-epoch 1234567 +``` + +The generated presigned exit message will only be valid at or after the specified exit-epoch, in this case, epoch 1234567. ## Delete @@ -16,9 +64,15 @@ Example: lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f ``` +To delete all validators on the validator client, use the keyword `all`: + +```bash +lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all +``` + ## Import -The `import` command imports validator keystores generated by the staking-deposit-cli/ethstaker-deposit-cli. To import a validator keystore: +The `import` command imports validator keystores generated by the `ethstaker-deposit-cli`. To import a validator keystore: ```bash lighthouse vm import --vc-token --keystore-file /path/to/json --password keystore_password @@ -37,3 +91,26 @@ To list the validators running on the validator client: ```bash lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt ``` + +The `list` command can also be used to check the voluntary exit status of validators. To do so, use both `--beacon-node` and `--validators` flags. The `--validators` flag accepts a comma-separated list of validator public keys, or the keyword `all` to check the voluntary exit status of all validators attached to the validator client. + +```bash +lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 --beacon-node http://localhost:5052 +``` + +If the validator voluntary exit has been accepted by the chain, the following log will be returned: + +```text +Voluntary exit for validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has been accepted into the beacon chain, but not yet finalized. Finalization may take several minutes or longer. Before finalization there is a low probability that the exit may be reverted. +Current epoch: 2, Exit epoch: 7, Withdrawable epoch: 263 +Please keep your validator running till exit epoch +Exit epoch in approximately 480 secs +``` + +When the exit epoch is reached, querying the status will return: + +```text +Validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has exited at epoch: 7 +``` + +You can safely shut down the validator client at this point. diff --git a/book/src/validator_manager_create.md b/book/src/validator_manager_create.md index 458907bc65..ae40910d5c 100644 --- a/book/src/validator_manager_create.md +++ b/book/src/validator_manager_create.md @@ -8,7 +8,7 @@ mnemonic and produces two files: - `validators.json`: the keystores and passwords for the newly generated validators, in JSON format. - `deposits.json`: a JSON file of the same format as - [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) which can + [ethstaker-deposit-cli](https://github.com/eth-educators/ethstaker-deposit-cli) which can be used for deposit submission via the [Ethereum Staking Launchpad][]. @@ -69,7 +69,7 @@ lighthouse \ > Be sure to remove `./validators.json` after the import is successful since it > contains unencrypted validator keystores. -> Note: To import validators with validator-manager using keystore files created using the staking deposit CLI, refer to [Managing Validators](./validator_manager_api.md#import). +> Note: To import validators with validator-manager using keystore files created using the `ethstaker-deposit-cli`, refer to [Managing Validators](./validator_manager_api.md#import). ## Detailed Guide diff --git a/book/src/validator_slashing_protection.md b/book/src/validator_slashing_protection.md index 3e0fe184e5..03e54e5827 100644 --- a/book/src/validator_slashing_protection.md +++ b/book/src/validator_slashing_protection.md @@ -21,7 +21,7 @@ and carefully to keep your validators safe. See the [Troubleshooting](#troublesh The database will be automatically created, and your validators registered with it when: -* Importing keys from another source (e.g. [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli/releases), Lodestar, Nimbus, Prysm, Teku, [ethdo](https://github.com/wealdtech/ethdo)). +* Importing keys from another source (e.g. [ethstaker-deposit-cli](https://github.com/eth-educators/ethstaker-deposit-cli), Lodestar, Nimbus, Prysm, Teku, [ethdo](https://github.com/wealdtech/ethdo)). See [import validator keys](./mainnet_validator.md#step-3-import-validator-keys-to-lighthouse). * Creating keys using Lighthouse itself (`lighthouse account validator create`) * Creating keys via the [validator client API](./api_vc.md). diff --git a/book/src/validator_voluntary_exit.md b/book/src/validator_voluntary_exit.md index d5d1722d59..3b660efe70 100644 --- a/book/src/validator_voluntary_exit.md +++ b/book/src/validator_voluntary_exit.md @@ -10,6 +10,8 @@ A validator can initiate a voluntary exit provided that the validator is current It takes at a minimum 5 epochs (32 minutes) for a validator to exit after initiating a voluntary exit. This number can be much higher depending on how many other validators are queued to exit. +You can also perform voluntary exit for one or more validators using the validator manager, see [Managing Validators](./validator_manager_api.md#exit) for more details. + ## Initiating a voluntary exit In order to initiate an exit, users can use the `lighthouse account validator exit` command. @@ -94,7 +96,7 @@ After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12 There are two types of withdrawal credentials, `0x00` and `0x01`. To check which type your validator has, go to [Staking launchpad](https://launchpad.ethereum.org/en/withdrawals), enter your validator index and click `verify on mainnet`: - `withdrawals enabled` means your validator is of type `0x01`, and you will automatically receive the full withdrawal to the withdrawal address that you set. -- `withdrawals not enabled` means your validator is of type `0x00`, and will need to update your withdrawal credentials from `0x00` type to `0x01` type (also known as BLS-to-execution-change, or BTEC) to receive the staked funds. The common way to do this is using `Staking deposit CLI` or `ethdo`, with the instructions available [here](https://launchpad.ethereum.org/en/withdrawals#update-your-keys). +- `withdrawals not enabled` means your validator is of type `0x00`, and will need to update your withdrawal credentials from `0x00` type to `0x01` type (also known as BLS-to-execution-change, or BTEC) to receive the staked funds. The common way to do this is using `ethstaker-deposit-cli` or `ethdo`, with the instructions available [here](https://launchpad.ethereum.org/en/withdrawals#update-your-keys). ### 2. What if my validator is of type `0x00` and I do not update my withdrawal credentials after I initiated a voluntary exit? @@ -118,26 +120,9 @@ There are two types of withdrawal credentials, `0x00` and `0x01`. To check which - A fixed waiting period of 256 epochs (27.3 hours) for the validator's status to become withdrawable. -- A varying time of "validator sweep" that can take up to _n_ days with _n_ listed in the table below. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. +- A varying time of "validator sweep" that take a few days. The "validator sweep" is the process of skimming through all eligible validators by index number for withdrawals (those with type `0x01` and balance above 32ETH). Once the "validator sweep" reaches your validator's index, your staked fund will be fully withdrawn to the withdrawal address set. -
- -| Number of eligible validators | Ideal scenario _n_ | Practical scenario _n_ | -|:----------------:|:---------------------:|:----:| -| 300000 | 2.60 | 2.63 | -| 400000 | 3.47 | 3.51 | -| 500000 | 4.34 | 4.38 | -| 600000 | 5.21 | 5.26 | -| 700000 | 6.08 | 6.14 | -| 800000 | 6.94 | 7.01 | -| 900000 | 7.81 | 7.89 | -| 1000000 | 8.68 | 8.77 | - -
- -> Note: Ideal scenario assumes no block proposals are missed. This means a total of withdrawals of 7200 blocks/day * 16 withdrawals/block = 115200 withdrawals/day. Practical scenario assumes 1% of blocks are missed per day. As an example, if there are 700000 eligible validators, one would expect a waiting time of slightly more than 6 days. - - The total time taken is the summation of the above 3 waiting periods. After these waiting periods, you will receive the staked funds in your withdrawal address. +The total time taken is the summation of the above 3 waiting periods. After these waiting periods, you will receive the staked funds in your withdrawal address. The voluntary exit and full withdrawal process is summarized in the Figure below. diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index d1b059f3b2..0df18017f0 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "boot_node" -version = "7.1.0-beta.0" +version = { workspace = true } authors = ["Sigma Prime "] edition = { workspace = true } @@ -15,6 +15,7 @@ hex = { workspace = true } lighthouse_network = { workspace = true } log = { workspace = true } logging = { workspace = true } +network_utils = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index 0f274885d1..301363afe8 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -1,7 +1,7 @@ //! Simple logic for spawning a Lighthouse BootNode. use clap::{Arg, ArgAction, Command}; -use clap_utils::{get_color_style, FLAG_HEADER}; +use clap_utils::{FLAG_HEADER, get_color_style}; // TODO: Add DOS prevention CLI params pub fn cli_app() -> Command { diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index c43a8b397b..fb0daf5264 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -2,11 +2,13 @@ use beacon_node::{get_data_dir, set_network_config}; use bytes::Bytes; use clap::ArgMatches; use eth2_network_config::Eth2NetworkConfig; -use lighthouse_network::discv5::{self, enr::CombinedKey, Enr}; +use lighthouse_network::discv5::{self, Enr, enr::CombinedKey}; use lighthouse_network::{ + NetworkConfig, discovery::{load_enr_from_disk, use_or_load_enr}, - load_private_key, CombinedKeyExt, NetworkConfig, + load_private_key, }; +use network_utils::enr_ext::CombinedKeyExt; use serde::{Deserialize, Serialize}; use ssz::Encode; use std::net::{SocketAddrV4, SocketAddrV6}; @@ -56,26 +58,30 @@ impl BootNodeConfig { set_network_config(&mut network_config, matches, &data_dir)?; // Set the Enr Discovery ports to the listening ports if not present. - if let Some(listening_addr_v4) = network_config.listen_addrs().v4() { - if network_config.enr_udp4_port.is_none() { - network_config.enr_udp4_port = - Some(network_config.enr_udp4_port.unwrap_or( - listening_addr_v4.disc_port.try_into().map_err(|_| { - "boot node enr-udp-port not set and listening port is zero" - })?, - )) - } + if let Some(listening_addr_v4) = network_config.listen_addrs().v4() + && network_config.enr_udp4_port.is_none() + { + network_config.enr_udp4_port = Some( + network_config.enr_udp4_port.unwrap_or( + listening_addr_v4 + .disc_port + .try_into() + .map_err(|_| "boot node enr-udp-port not set and listening port is zero")?, + ), + ) }; - if let Some(listening_addr_v6) = network_config.listen_addrs().v6() { - if network_config.enr_udp6_port.is_none() { - network_config.enr_udp6_port = - Some(network_config.enr_udp6_port.unwrap_or( - listening_addr_v6.disc_port.try_into().map_err(|_| { - "boot node enr-udp-port not set and listening port is zero" - })?, - )) - } + if let Some(listening_addr_v6) = network_config.listen_addrs().v6() + && network_config.enr_udp6_port.is_none() + { + network_config.enr_udp6_port = Some( + network_config.enr_udp6_port.unwrap_or( + listening_addr_v6 + .disc_port + .try_into() + .map_err(|_| "boot node enr-udp-port not set and listening port is zero")?, + ), + ) }; // By default this is enabled. If it is not set, revert to false. diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index d96ac0c726..fce734bd70 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -5,9 +5,10 @@ use crate::config::BootNodeConfigSerialization; use clap::ArgMatches; use eth2_network_config::Eth2NetworkConfig; use lighthouse_network::{ - discv5::{self, enr::NodeId, Discv5}, - EnrExt, Eth2Enr, + Eth2Enr, + discv5::{self, Discv5, enr::NodeId}, }; +use network_utils::enr_ext::EnrExt; use tracing::{info, warn}; use types::EthSpec; @@ -77,10 +78,10 @@ pub async fn run( node_id = ?enr.node_id(), "Adding bootnode" ); - if enr != local_enr { - if let Err(e) = discv5.add_enr(enr) { - warn!(error = ?e, "Failed adding ENR"); - } + if enr != local_enr + && let Err(e) = discv5.add_enr(enr) + { + warn!(error = ?e, "Failed adding ENR"); } } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000000..dabcbe8bf5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,7 @@ +# Disallow preliminary slashing checks, +disallowed-methods = [ + { path = "slashing_protection::slashing_database::SlashingDatabase::preliminary_check_block_proposal", reason = "not safe for slashing checks", replacement = "slashing_protection::slashing_database::SlashingDatabase::check_and_insert_block_proposal" }, + { path = "slashing_protection::slashing_database::SlashingDatabase::preliminary_check_block_signing_root", reason = "not safe for slashing checks", replacement = "slashing_protection::slashing_database::SlashingDatabase::check_and_insert_block_signing_root" }, + { path = "slashing_protection::slashing_database::SlashingDatabase::preliminary_check_attestation", reason = "not safe for slashing checks", replacement = "slashing_protection::slashing_database::SlashingDatabase::check_and_insert_attestation" }, + { path = "slashing_protection::slashing_database::SlashingDatabase::preliminary_check_attestation_signing_root", reason = "not safe for slashing checks", replacement = "slashing_protection::slashing_database::SlashingDatabase::check_and_insert_attestation_signing_root" }, +] diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index 00c74a1303..d0a3e487c4 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bls = { workspace = true } eth2_keystore = { workspace = true } eth2_wallet = { workspace = true } filesystem = { workspace = true } diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 0f576efb3a..806c9338d5 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -3,11 +3,11 @@ use eth2_keystore::Keystore; use eth2_wallet::{ - bip39::{Language, Mnemonic, MnemonicType}, Wallet, + bip39::{Language, Mnemonic, MnemonicType}, }; -use filesystem::{create_with_600_perms, Error as FsError}; -use rand::{distributions::Alphanumeric, Rng}; +use filesystem::{Error as FsError, create_with_600_perms}; +use rand::{Rng, distr::Alphanumeric}; use std::fs::{self, File}; use std::io; use std::io::prelude::*; @@ -115,7 +115,7 @@ pub fn random_password_string() -> Zeroizing { /// Common implementation for `random_password` and `random_password_string`. fn random_password_raw_string() -> String { - rand::thread_rng() + rand::rng() .sample_iter(&Alphanumeric) .take(DEFAULT_PASSWORD_LEN) .map(char::from) diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 4c253283fe..bffdfcc38b 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -4,15 +4,16 @@ //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. use crate::{default_keystore_password_path, read_password_string, write_file_via_temporary}; +use bls::PublicKey; use eth2_keystore::Keystore; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::fs::{self, create_dir_all, File}; +use std::fs::{self, File, create_dir_all}; use std::io; use std::path::{Path, PathBuf}; use tracing::error; -use types::{graffiti::GraffitiString, Address, PublicKey}; +use types::{Address, graffiti::GraffitiString}; use validator_dir::VOTING_KEYSTORE_FILE; use zeroize::Zeroizing; @@ -450,11 +451,11 @@ pub fn is_voting_keystore(file_name: &str) -> bool { return true; } - // The format exported by the `eth2.0-deposit-cli` library. + // The format exported by the `ethstaker-deposit-cli` library. // // Reference to function that generates keystores: // - // https://github.com/ethereum/eth2.0-deposit-cli/blob/7cebff15eac299b3b1b090c896dd3410c8463450/eth2deposit/credentials.py#L58-L62 + // https://github.com/eth-educators/ethstaker-deposit-cli/blob/80d536374de838ccae142974ed0e747b46beb030/ethstaker_deposit/credentials.py#L186-L190 // // Since we include the key derivation path of `m/12381/3600/x/0/0` this should only ever match // with a voting keystore and never a withdrawal keystore. diff --git a/common/clap_utils/src/lib.rs b/common/clap_utils/src/lib.rs index a4b5f4dc1c..bc904c78e3 100644 --- a/common/clap_utils/src/lib.rs +++ b/common/clap_utils/src/lib.rs @@ -1,8 +1,8 @@ //! A helper library for parsing values from `clap::ArgMatches`. -use clap::builder::styling::*; use clap::ArgMatches; -use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK}; +use clap::builder::styling::*; +use eth2_network_config::{DEFAULT_HARDCODED_NETWORK, Eth2NetworkConfig}; use ssz::Decode; use std::path::PathBuf; use std::str::FromStr; diff --git a/common/compare_fields/Cargo.toml b/common/compare_fields/Cargo.toml deleted file mode 100644 index 9972ca75ca..0000000000 --- a/common/compare_fields/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "compare_fields" -version = "0.2.0" -authors = ["Paul Hauner "] -edition = { workspace = true } - -[dependencies] -itertools = { workspace = true } - -[dev-dependencies] -compare_fields_derive = { workspace = true } - -[package.metadata.cargo-udeps.ignore] -development = ["compare_fields_derive"] # used in doc-tests diff --git a/common/compare_fields/src/lib.rs b/common/compare_fields/src/lib.rs deleted file mode 100644 index 27baf14806..0000000000 --- a/common/compare_fields/src/lib.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Provides field-by-field comparisons for structs and vecs. -//! -//! Returns comparisons as data, without making assumptions about the desired equality (e.g., -//! does not `panic!` on inequality). -//! -//! Note: `compare_fields_derive` requires `PartialEq` and `Debug` implementations. -//! -//! ## Example -//! -//! ```rust -//! use compare_fields::{CompareFields, Comparison, FieldComparison}; -//! use compare_fields_derive::CompareFields; -//! -//! #[derive(PartialEq, Debug, CompareFields)] -//! pub struct Bar { -//! a: u64, -//! b: u16, -//! #[compare_fields(as_slice)] -//! c: Vec -//! } -//! -//! #[derive(Clone, PartialEq, Debug, CompareFields)] -//! pub struct Foo { -//! d: String -//! } -//! -//! let cat = Foo {d: "cat".to_string()}; -//! let dog = Foo {d: "dog".to_string()}; -//! let chicken = Foo {d: "chicken".to_string()}; -//! -//! let mut bar_a = Bar { -//! a: 42, -//! b: 12, -//! c: vec![ cat.clone(), dog.clone() ], -//! }; -//! -//! let mut bar_b = Bar { -//! a: 42, -//! b: 99, -//! c: vec![ chicken.clone(), dog.clone()] -//! }; -//! -//! let cat_dog = Comparison::Child(FieldComparison { -//! field_name: "d".to_string(), -//! equal: false, -//! a: "\"cat\"".to_string(), -//! b: "\"dog\"".to_string(), -//! }); -//! assert_eq!(cat.compare_fields(&dog), vec![cat_dog]); -//! -//! let bar_a_b = vec![ -//! Comparison::Child(FieldComparison { -//! field_name: "a".to_string(), -//! equal: true, -//! a: "42".to_string(), -//! b: "42".to_string(), -//! }), -//! Comparison::Child(FieldComparison { -//! field_name: "b".to_string(), -//! equal: false, -//! a: "12".to_string(), -//! b: "99".to_string(), -//! }), -//! Comparison::Parent{ -//! field_name: "c".to_string(), -//! equal: false, -//! children: vec![ -//! FieldComparison { -//! field_name: "0".to_string(), -//! equal: false, -//! a: "Some(Foo { d: \"cat\" })".to_string(), -//! b: "Some(Foo { d: \"chicken\" })".to_string(), -//! }, -//! FieldComparison { -//! field_name: "1".to_string(), -//! equal: true, -//! a: "Some(Foo { d: \"dog\" })".to_string(), -//! b: "Some(Foo { d: \"dog\" })".to_string(), -//! } -//! ] -//! } -//! ]; -//! assert_eq!(bar_a.compare_fields(&bar_b), bar_a_b); -//! ``` -use itertools::{EitherOrBoth, Itertools}; -use std::fmt::Debug; - -#[derive(Debug, PartialEq, Clone)] -pub enum Comparison { - Child(FieldComparison), - Parent { - field_name: String, - equal: bool, - children: Vec, - }, -} - -impl Comparison { - pub fn child>(field_name: String, a: &T, b: &T) -> Self { - Comparison::Child(FieldComparison::new(field_name, a, b)) - } - - pub fn parent(field_name: String, equal: bool, children: Vec) -> Self { - Comparison::Parent { - field_name, - equal, - children, - } - } - - pub fn from_slice>(field_name: String, a: &[T], b: &[T]) -> Self { - Self::from_iter(field_name, a.iter(), b.iter()) - } - - pub fn from_into_iter<'a, T: Debug + PartialEq + 'a>( - field_name: String, - a: impl IntoIterator, - b: impl IntoIterator, - ) -> Self { - Self::from_iter(field_name, a.into_iter(), b.into_iter()) - } - - pub fn from_iter<'a, T: Debug + PartialEq + 'a>( - field_name: String, - a: impl Iterator, - b: impl Iterator, - ) -> Self { - let mut children = vec![]; - let mut all_equal = true; - - for (i, entry) in a.zip_longest(b).enumerate() { - let comparison = match entry { - EitherOrBoth::Both(x, y) => { - FieldComparison::new(format!("{i}"), &Some(x), &Some(y)) - } - EitherOrBoth::Left(x) => FieldComparison::new(format!("{i}"), &Some(x), &None), - EitherOrBoth::Right(y) => FieldComparison::new(format!("{i}"), &None, &Some(y)), - }; - all_equal = all_equal && comparison.equal(); - children.push(comparison); - } - - Self::parent(field_name, all_equal, children) - } - - pub fn retain_children(&mut self, f: F) - where - F: FnMut(&FieldComparison) -> bool, - { - match self { - Comparison::Child(_) => (), - Comparison::Parent { children, .. } => children.retain(f), - } - } - - pub fn equal(&self) -> bool { - match self { - Comparison::Child(fc) => fc.equal, - Comparison::Parent { equal, .. } => *equal, - } - } - - pub fn not_equal(&self) -> bool { - !self.equal() - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct FieldComparison { - pub field_name: String, - pub equal: bool, - pub a: String, - pub b: String, -} - -pub trait CompareFields { - fn compare_fields(&self, b: &Self) -> Vec; -} - -impl FieldComparison { - pub fn new>(field_name: String, a: &T, b: &T) -> Self { - Self { - field_name, - equal: a == b, - a: format!("{a:?}"), - b: format!("{b:?}"), - } - } - - pub fn equal(&self) -> bool { - self.equal - } - - pub fn not_equal(&self) -> bool { - !self.equal() - } -} diff --git a/common/compare_fields_derive/Cargo.toml b/common/compare_fields_derive/Cargo.toml deleted file mode 100644 index 19682bf367..0000000000 --- a/common/compare_fields_derive/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "compare_fields_derive" -version = "0.2.0" -authors = ["Paul Hauner "] -edition = { workspace = true } - -[lib] -proc-macro = true - -[dependencies] -quote = { workspace = true } -syn = { workspace = true } diff --git a/common/compare_fields_derive/src/lib.rs b/common/compare_fields_derive/src/lib.rs deleted file mode 100644 index 1a89ccf4fd..0000000000 --- a/common/compare_fields_derive/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -fn is_iter(field: &syn::Field) -> bool { - field.attrs.iter().any(|attr| { - attr.path.is_ident("compare_fields") - && (attr.tokens.to_string().replace(' ', "") == "(as_slice)" - || attr.tokens.to_string().replace(' ', "") == "(as_iter)") - }) -} - -#[proc_macro_derive(CompareFields, attributes(compare_fields))] -pub fn compare_fields_derive(input: TokenStream) -> TokenStream { - let item = parse_macro_input!(input as DeriveInput); - - let name = &item.ident; - let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl(); - - let syn::Data::Struct(struct_data) = &item.data else { - panic!("compare_fields_derive only supports structs."); - }; - - let mut quotes = vec![]; - - for field in struct_data.fields.iter() { - let Some(ident_a) = &field.ident else { - panic!("compare_fields_derive only supports named struct fields."); - }; - let field_name = ident_a.to_string(); - let ident_b = ident_a.clone(); - - let quote = if is_iter(field) { - quote! { - comparisons.push(compare_fields::Comparison::from_into_iter( - #field_name.to_string(), - &self.#ident_a, - &b.#ident_b - )); - } - } else { - quote! { - comparisons.push( - compare_fields::Comparison::child( - #field_name.to_string(), - &self.#ident_a, - &b.#ident_b - ) - ); - } - }; - - quotes.push(quote); - } - - let output = quote! { - impl #impl_generics compare_fields::CompareFields for #name #ty_generics #where_clause { - fn compare_fields(&self, b: &Self) -> Vec { - let mut comparisons = vec![]; - - #( - #quotes - )* - - comparisons - } - } - }; - output.into() -} diff --git a/common/deposit_contract/Cargo.toml b/common/deposit_contract/Cargo.toml index 953fde1af7..76c18ef242 100644 --- a/common/deposit_contract/Cargo.toml +++ b/common/deposit_contract/Cargo.toml @@ -6,14 +6,18 @@ edition = { workspace = true } build = "build.rs" +[dependencies] +alloy-dyn-abi = { workspace = true } +alloy-json-abi = { workspace = true } +alloy-primitives = { workspace = true } +bls = { workspace = true } +ethereum_ssz = { workspace = true } +serde_json = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } + [build-dependencies] hex = { workspace = true } reqwest = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } - -[dependencies] -ethabi = "16.0.0" -ethereum_ssz = { workspace = true } -tree_hash = { workspace = true } -types = { workspace = true } diff --git a/common/deposit_contract/build.rs b/common/deposit_contract/build.rs index cae1d480c8..2061d13c24 100644 --- a/common/deposit_contract/build.rs +++ b/common/deposit_contract/build.rs @@ -153,14 +153,13 @@ fn verify_checksum(bytes: &[u8], expected_checksum: &str) { /// Returns the directory that will be used to store the deposit contract ABI. fn abi_dir() -> PathBuf { - let base = env::var("CARGO_MANIFEST_DIR") - .expect("should know manifest dir") + let base = env::var("OUT_DIR") + .expect("should know out dir") .parse::() - .expect("should parse manifest dir as path") - .join("contracts"); + .expect("should parse out dir as path"); std::fs::create_dir_all(base.clone()) - .expect("should be able to create abi directory in manifest"); + .expect("should be able to create abi directory in out dir"); base } diff --git a/common/deposit_contract/src/lib.rs b/common/deposit_contract/src/lib.rs index 5b54a05396..6200a4ca15 100644 --- a/common/deposit_contract/src/lib.rs +++ b/common/deposit_contract/src/lib.rs @@ -1,82 +1,124 @@ -use ethabi::{Contract, Token}; +use alloy_dyn_abi::{DynSolValue, JsonAbiExt}; +use alloy_json_abi::JsonAbi; +use alloy_primitives::FixedBytes; +use bls::{PublicKeyBytes, SignatureBytes}; use ssz::{Decode, DecodeError as SszDecodeError, Encode}; use tree_hash::TreeHash; -use types::{DepositData, Hash256, PublicKeyBytes, SignatureBytes}; - -pub use ethabi::Error; +use types::{DepositData, Hash256}; #[derive(Debug)] -pub enum DecodeError { - EthabiError(ethabi::Error), +pub enum Error { + AlloyCoreError(alloy_json_abi::Error), + SerdeJsonError(serde_json::Error), + DynAbiError(alloy_dyn_abi::Error), SszDecodeError(SszDecodeError), + FunctionNotFound, MissingField, UnableToGetBytes, MissingToken, InadequateBytes, } -impl From for DecodeError { - fn from(e: ethabi::Error) -> DecodeError { - DecodeError::EthabiError(e) +impl From for Error { + fn from(e: alloy_json_abi::Error) -> Error { + Error::AlloyCoreError(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { + Error::SerdeJsonError(e) + } +} + +impl From for Error { + fn from(e: alloy_dyn_abi::Error) -> Error { + Error::DynAbiError(e) + } +} + +impl From for Error { + fn from(e: SszDecodeError) -> Error { + Error::SszDecodeError(e) } } pub const CONTRACT_DEPLOY_GAS: usize = 4_000_000; pub const DEPOSIT_GAS: usize = 400_000; -pub const ABI: &[u8] = include_bytes!("../contracts/v0.12.1_validator_registration.json"); -pub const BYTECODE: &[u8] = include_bytes!("../contracts/v0.12.1_validator_registration.bytecode"); +pub const ABI: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/v0.12.1_validator_registration.json" +)); +pub const BYTECODE: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/v0.12.1_validator_registration.bytecode" +)); pub const DEPOSIT_DATA_LEN: usize = 420; // lol pub mod testnet { - pub const ABI: &[u8] = - include_bytes!("../contracts/v0.12.1_testnet_validator_registration.json"); - pub const BYTECODE: &[u8] = - include_bytes!("../contracts/v0.12.1_testnet_validator_registration.bytecode"); + pub const ABI: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/v0.12.1_testnet_validator_registration.json" + )); + pub const BYTECODE: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/v0.12.1_testnet_validator_registration.bytecode" + )); } pub fn encode_eth1_tx_data(deposit_data: &DepositData) -> Result, Error> { let params = vec![ - Token::Bytes(deposit_data.pubkey.as_ssz_bytes()), - Token::Bytes(deposit_data.withdrawal_credentials.as_ssz_bytes()), - Token::Bytes(deposit_data.signature.as_ssz_bytes()), - Token::FixedBytes(deposit_data.tree_hash_root().as_ssz_bytes()), + DynSolValue::Bytes(deposit_data.pubkey.as_ssz_bytes()), + DynSolValue::Bytes(deposit_data.withdrawal_credentials.as_ssz_bytes()), + DynSolValue::Bytes(deposit_data.signature.as_ssz_bytes()), + DynSolValue::FixedBytes( + FixedBytes::<32>::from_slice(&deposit_data.tree_hash_root().as_ssz_bytes()), + 32, + ), ]; // Here we make an assumption that the `crate::testnet::ABI` has a superset of the features of // the crate::ABI`. - let abi = Contract::load(ABI)?; - let function = abi.function("deposit")?; - function.encode_input(¶ms) + let abi: JsonAbi = serde_json::from_slice(ABI)?; + let function = abi + .function("deposit") + .and_then(|functions| functions.first()) + .ok_or(Error::FunctionNotFound)?; + + function + .abi_encode_input(¶ms) + .map_err(Error::DynAbiError) } -pub fn decode_eth1_tx_data( - bytes: &[u8], - amount: u64, -) -> Result<(DepositData, Hash256), DecodeError> { - let abi = Contract::load(ABI)?; - let function = abi.function("deposit")?; - let mut tokens = function.decode_input(bytes.get(4..).ok_or(DecodeError::InadequateBytes)?)?; +pub fn decode_eth1_tx_data(bytes: &[u8], amount: u64) -> Result<(DepositData, Hash256), Error> { + let abi: JsonAbi = serde_json::from_slice(ABI)?; + let function = abi + .function("deposit") + .and_then(|functions| functions.first()) + .ok_or(Error::FunctionNotFound)?; + + let input_data = bytes.get(4..).ok_or(Error::InadequateBytes)?; + let mut tokens = function.abi_decode_input(input_data)?; macro_rules! decode_token { - ($type: ty, $to_fn: ident) => { - <$type>::from_ssz_bytes( - &tokens - .pop() - .ok_or_else(|| DecodeError::MissingToken)? - .$to_fn() - .ok_or_else(|| DecodeError::UnableToGetBytes)?, - ) - .map_err(DecodeError::SszDecodeError)? - }; + ($type: ty) => {{ + let token = tokens.pop().ok_or(Error::MissingToken)?; + let bytes_data = match token { + DynSolValue::Bytes(b) => b, + DynSolValue::FixedBytes(b, _) => b.to_vec(), + _ => return Err(Error::UnableToGetBytes), + }; + <$type>::from_ssz_bytes(&bytes_data)? + }}; } - let root = decode_token!(Hash256, into_fixed_bytes); + let root = decode_token!(Hash256); let deposit_data = DepositData { amount, - signature: decode_token!(SignatureBytes, into_bytes), - withdrawal_credentials: decode_token!(Hash256, into_bytes), - pubkey: decode_token!(PublicKeyBytes, into_bytes), + signature: decode_token!(SignatureBytes), + withdrawal_credentials: decode_token!(Hash256), + pubkey: decode_token!(PublicKeyBytes), }; Ok((deposit_data, root)) @@ -85,10 +127,8 @@ pub fn decode_eth1_tx_data( #[cfg(test)] mod tests { use super::*; - use types::{ - test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Keypair, MinimalEthSpec, - Signature, - }; + use bls::{Keypair, Signature}; + use types::{ChainSpec, EthSpec, MinimalEthSpec, test_utils::generate_deterministic_keypair}; type E = MinimalEthSpec; diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml new file mode 100644 index 0000000000..058e1fd1a0 --- /dev/null +++ b/common/eip_3076/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "eip_3076" +version = "0.1.0" +authors = ["Sigma Prime "] +edition = { workspace = true } + +[features] +default = [] +arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary"] +json = ["dep:serde_json"] + +[dependencies] +arbitrary = { workspace = true, features = ["derive"], optional = true } +bls = { workspace = true } +ethereum_serde_utils = { workspace = true } +fixed_bytes = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true, optional = true } +types = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/validator_client/slashing_protection/src/interchange.rs b/common/eip_3076/src/lib.rs similarity index 63% rename from validator_client/slashing_protection/src/interchange.rs rename to common/eip_3076/src/lib.rs index 95a39c50e4..cdd05d7b1e 100644 --- a/validator_client/slashing_protection/src/interchange.rs +++ b/common/eip_3076/src/lib.rs @@ -1,9 +1,15 @@ -use crate::InterchangeError; +use bls::PublicKeyBytes; use serde::{Deserialize, Serialize}; use std::cmp::max; use std::collections::{HashMap, HashSet}; +#[cfg(feature = "json")] use std::io; -use types::{Epoch, Hash256, PublicKeyBytes, Slot}; +use types::{Epoch, Hash256, Slot}; + +#[derive(Debug)] +pub enum Error { + MaxInconsistent, +} #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] @@ -53,10 +59,12 @@ pub struct Interchange { } impl Interchange { + #[cfg(feature = "json")] pub fn from_json_str(json: &str) -> Result { serde_json::from_str(json) } + #[cfg(feature = "json")] pub fn from_json_reader(mut reader: impl std::io::Read) -> Result { // We read the entire file into memory first, as this is *a lot* faster than using // `serde_json::from_reader`. See https://github.com/serde-rs/json/issues/160 @@ -65,6 +73,7 @@ impl Interchange { Ok(Interchange::from_json_str(&json_str)?) } + #[cfg(feature = "json")] pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> { serde_json::to_writer(writer, self) } @@ -87,7 +96,7 @@ impl Interchange { } /// Minify an interchange by constructing a synthetic block & attestation for each validator. - pub fn minify(&self) -> Result { + pub fn minify(&self) -> Result { // Map from pubkey to optional max block and max attestation. let mut validator_data = HashMap::, Option)>::new(); @@ -124,7 +133,7 @@ impl Interchange { } } (None, None) => {} - _ => return Err(InterchangeError::MaxInconsistent), + _ => return Err(Error::MaxInconsistent), }; // Find maximum block slot. @@ -157,3 +166,96 @@ impl Interchange { }) } } + +#[cfg(feature = "json")] +#[cfg(test)] +mod tests { + use super::*; + use fixed_bytes::FixedBytesExtended; + use std::fs::File; + use tempfile::tempdir; + + fn get_interchange() -> Interchange { + Interchange { + metadata: InterchangeMetadata { + interchange_format_version: 5, + genesis_validators_root: Hash256::from_low_u64_be(555), + }, + data: vec![ + InterchangeData { + pubkey: PublicKeyBytes::deserialize(&[1u8; 48]).unwrap(), + signed_blocks: vec![SignedBlock { + slot: Slot::new(100), + signing_root: Some(Hash256::from_low_u64_be(1)), + }], + signed_attestations: vec![SignedAttestation { + source_epoch: Epoch::new(0), + target_epoch: Epoch::new(5), + signing_root: Some(Hash256::from_low_u64_be(2)), + }], + }, + InterchangeData { + pubkey: PublicKeyBytes::deserialize(&[2u8; 48]).unwrap(), + signed_blocks: vec![], + signed_attestations: vec![], + }, + ], + } + } + + #[test] + fn test_roundtrip() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("interchange.json"); + + let interchange = get_interchange(); + + let mut file = File::create(&file_path).unwrap(); + interchange.write_to(&mut file).unwrap(); + + let file = File::open(&file_path).unwrap(); + let from_file = Interchange::from_json_reader(file).unwrap(); + + assert_eq!(interchange, from_file); + } + + #[test] + fn test_empty_roundtrip() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("empty.json"); + + let empty = Interchange { + metadata: InterchangeMetadata { + interchange_format_version: 5, + genesis_validators_root: Hash256::zero(), + }, + data: vec![], + }; + + let mut file = File::create(&file_path).unwrap(); + empty.write_to(&mut file).unwrap(); + + let file = File::open(&file_path).unwrap(); + let from_file = Interchange::from_json_reader(file).unwrap(); + + assert_eq!(empty, from_file); + } + + #[test] + fn test_minify_roundtrip() { + let interchange = get_interchange(); + + let minified = interchange.minify().unwrap(); + + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("minified.json"); + + let mut file = File::create(&file_path).unwrap(); + minified.write_to(&mut file).unwrap(); + + let file = File::open(&file_path).unwrap(); + let from_file = Interchange::from_json_reader(file).unwrap(); + + assert_eq!(minified, from_file); + } +} diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 5d0ad1f45e..da8aba5ded 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -4,36 +4,36 @@ version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } +[features] +default = [] +lighthouse = ["proto_array", "eth2_keystore", "eip_3076", "zeroize"] +events = ["reqwest-eventsource", "futures", "futures-util"] + [dependencies] -derivative = { workspace = true } -either = { workspace = true } -enr = { version = "0.13.0", features = ["ed25519"] } -eth2_keystore = { workspace = true } +bls = { workspace = true } +context_deserialize = { workspace = true } +educe = { workspace = true } +eip_3076 = { workspace = true, optional = true } +eth2_keystore = { workspace = true, optional = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -futures = { workspace = true } -futures-util = "0.3.8" -libp2p-identity = { version = "0.2", features = ["peerid"] } +futures = { workspace = true, optional = true } +futures-util = { version = "0.3.8", optional = true } mediatype = "0.19.13" -multiaddr = "0.18.2" pretty_reqwest_error = { workspace = true } -proto_array = { workspace = true } -rand = { workspace = true } +proto_array = { workspace = true, optional = true } reqwest = { workspace = true } -reqwest-eventsource = "0.5.0" +reqwest-eventsource = { version = "0.6.0", optional = true } sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -slashing_protection = { workspace = true } ssz_types = { workspace = true } -test_random_derive = { path = "../../common/test_random_derive" } +superstruct = { workspace = true } types = { workspace = true } -zeroize = { workspace = true } +zeroize = { workspace = true, optional = true } [dev-dependencies] +rand = { workspace = true } +test_random_derive = { path = "../../common/test_random_derive" } tokio = { workspace = true } - -[features] -default = ["lighthouse"] -lighthouse = [] diff --git a/consensus/types/src/beacon_response.rs b/common/eth2/src/beacon_response.rs similarity index 88% rename from consensus/types/src/beacon_response.rs rename to common/eth2/src/beacon_response.rs index 2e45854364..d58734997c 100644 --- a/consensus/types/src/beacon_response.rs +++ b/common/eth2/src/beacon_response.rs @@ -1,12 +1,8 @@ -use crate::{ContextDeserialize, ForkName}; +use context_deserialize::ContextDeserialize; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::Value; - -pub trait ForkVersionDecode: Sized { - /// SSZ decode with explicit fork variant. - fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; -} +use types::ForkName; /// The metadata of type M should be set to `EmptyMetadata` if you don't care about adding fields other than /// version. If you *do* care about adding other fields you can mix in any type that implements @@ -25,6 +21,7 @@ pub struct ForkVersionedResponse { /// `Deserialize`. #[derive(Debug, PartialEq, Clone, Serialize)] pub struct UnversionedResponse { + #[serde(flatten)] pub metadata: M, pub data: T, } @@ -195,9 +192,10 @@ impl From> for BeaconResponse { #[cfg(test)] mod fork_version_response_tests { + use crate::beacon_response::ExecutionOptimisticFinalizedMetadata; use crate::{ ExecutionPayload, ExecutionPayloadBellatrix, ForkName, ForkVersionedResponse, - MainnetEthSpec, + MainnetEthSpec, UnversionedResponse, }; use serde_json::json; @@ -236,4 +234,24 @@ mod fork_version_response_tests { assert!(result.is_err()); } + + // The following test should only pass by having the attribute #[serde(flatten)] on the metadata + #[test] + fn unversioned_response_serialize_dezerialize_round_trip_test() { + // Create an UnversionedResponse with some data + let data = UnversionedResponse { + metadata: ExecutionOptimisticFinalizedMetadata { + execution_optimistic: Some(false), + finalized: Some(false), + }, + data: "some_test_data".to_string(), + }; + + let serialized = serde_json::to_string(&data); + + let deserialized = + serde_json::from_str(&serialized.unwrap()).expect("Failed to deserialize"); + + assert_eq!(data, deserialized); + } } diff --git a/common/eth2/src/error.rs b/common/eth2/src/error.rs new file mode 100644 index 0000000000..1f21220b79 --- /dev/null +++ b/common/eth2/src/error.rs @@ -0,0 +1,167 @@ +//! Centralized error handling for eth2 API clients +//! +//! This module consolidates all error types, response processing, +//! and recovery logic for both beacon node and validator client APIs. + +use pretty_reqwest_error::PrettyReqwestError; +use reqwest::{Response, StatusCode}; +use sensitive_url::SensitiveUrl; +use serde::{Deserialize, Serialize}; +use std::{fmt, path::PathBuf}; + +/// Main error type for eth2 API clients +#[derive(Debug)] +pub enum Error { + /// The `reqwest` client raised an error. + HttpClient(PrettyReqwestError), + #[cfg(feature = "events")] + /// The `reqwest_eventsource` client raised an error. + SseClient(Box), + /// The server returned an error message where the body was able to be parsed. + ServerMessage(ErrorMessage), + /// The server returned an error message with an array of errors. + ServerIndexedMessage(IndexedErrorMessage), + /// The server returned an error message where the body was unable to be parsed. + StatusCode(StatusCode), + /// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`. + InvalidUrl(SensitiveUrl), + /// The supplied validator client secret is invalid. + InvalidSecret(String), + /// The server returned a response with an invalid signature. It may be an impostor. + InvalidSignatureHeader, + /// The server returned a response without a signature header. It may be an impostor. + MissingSignatureHeader, + /// The server returned an invalid JSON response. + InvalidJson(serde_json::Error), + /// The server returned an invalid server-sent event. + InvalidServerSentEvent(String), + /// The server sent invalid response headers. + InvalidHeaders(String), + /// The server returned an invalid SSZ response. + InvalidSsz(ssz::DecodeError), + /// An I/O error occurred while loading an API token from disk. + TokenReadError(PathBuf, std::io::Error), + /// The client has been configured without a server pubkey, but requires one for this request. + NoServerPubkey, + /// The client has been configured without an API token, but requires one for this request. + NoToken, +} + +/// An API error serializable to JSON. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ErrorMessage { + pub code: u16, + pub message: String, + #[serde(default)] + pub stacktraces: Vec, +} + +/// An indexed API error serializable to JSON. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct IndexedErrorMessage { + pub code: u16, + pub message: String, + pub failures: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Failure { + pub index: u64, + pub message: String, +} + +impl Failure { + pub fn new(index: usize, message: String) -> Self { + Self { + index: index as u64, + message, + } + } +} + +/// Server error response variants +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ResponseError { + Indexed(IndexedErrorMessage), + Message(ErrorMessage), +} + +impl Error { + /// If the error has a HTTP status code, return it. + pub fn status(&self) -> Option { + match self { + Error::HttpClient(error) => error.inner().status(), + #[cfg(feature = "events")] + Error::SseClient(error) => { + if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() { + Some(*status) + } else { + None + } + } + Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), + Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), + Error::StatusCode(status) => Some(*status), + Error::InvalidUrl(_) => None, + Error::InvalidSecret(_) => None, + Error::InvalidSignatureHeader => None, + Error::MissingSignatureHeader => None, + Error::InvalidJson(_) => None, + Error::InvalidSsz(_) => None, + Error::InvalidServerSentEvent(_) => None, + Error::InvalidHeaders(_) => None, + Error::TokenReadError(..) => None, + Error::NoServerPubkey | Error::NoToken => None, + } + } +} + +impl From for Error { + fn from(error: reqwest::Error) -> Self { + Error::HttpClient(error.into()) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Returns `Ok(response)` if the response is a `200 OK`, `202 ACCEPTED`, or `204 NO_CONTENT` +/// Otherwise, creates an appropriate error message. +pub async fn ok_or_error(response: Response) -> Result { + let status = response.status(); + + if status == StatusCode::OK + || status == StatusCode::ACCEPTED + || status == StatusCode::NO_CONTENT + { + Ok(response) + } else if let Ok(message) = response.json::().await { + match message { + ResponseError::Message(message) => Err(Error::ServerMessage(message)), + ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)), + } + } else { + Err(Error::StatusCode(status)) + } +} + +/// Returns `Ok(response)` if the response is a success (2xx) response. Otherwise, creates an +/// appropriate error message. +pub async fn success_or_error(response: Response) -> Result { + let status = response.status(); + + if status.is_success() { + Ok(response) + } else if let Ok(message) = response.json().await { + match message { + ResponseError::Message(message) => Err(Error::ServerMessage(message)), + ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)), + } + } else { + Err(Error::StatusCode(status)) + } +} diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 323f548eea..2b542f4024 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -7,6 +7,8 @@ //! Eventually it would be ideal to publish this crate on crates.io, however we have some local //! dependencies preventing this presently. +pub mod beacon_response; +pub mod error; #[cfg(feature = "lighthouse")] pub mod lighthouse; #[cfg(feature = "lighthouse")] @@ -14,28 +16,35 @@ pub mod lighthouse_vc; pub mod mixin; pub mod types; -use self::mixin::{RequestAccept, ResponseOptional}; -use self::types::{Error as ResponseError, *}; -use ::types::beacon_response::ExecutionOptimisticFinalizedBeaconResponse; -use derivative::Derivative; -use either::Either; -use futures::Stream; -use futures_util::StreamExt; -use libp2p_identity::PeerId; -use pretty_reqwest_error::PrettyReqwestError; -pub use reqwest; -use reqwest::{ - header::{HeaderMap, HeaderValue}, - Body, IntoUrl, RequestBuilder, Response, +pub use beacon_response::{ + BeaconResponse, EmptyMetadata, ExecutionOptimisticFinalizedBeaconResponse, + ExecutionOptimisticFinalizedMetadata, ForkVersionedResponse, UnversionedResponse, }; + +pub use self::error::{Error, ok_or_error, success_or_error}; +pub use reqwest; pub use reqwest::{StatusCode, Url}; +pub use sensitive_url::SensitiveUrl; + +use self::mixin::{RequestAccept, ResponseOptional}; +use self::types::*; +use bls::SignatureBytes; +use context_deserialize::ContextDeserialize; +use educe::Educe; +#[cfg(feature = "events")] +use futures::Stream; +#[cfg(feature = "events")] +use futures_util::StreamExt; +use reqwest::{ + Body, IntoUrl, RequestBuilder, Response, + header::{HeaderMap, HeaderValue}, +}; +#[cfg(feature = "events")] use reqwest_eventsource::{Event, EventSource}; -pub use sensitive_url::{SensitiveError, SensitiveUrl}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use ssz::Encode; use std::fmt; use std::future::Future; -use std::path::PathBuf; use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); @@ -51,82 +60,23 @@ pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream"; pub const JSON_CONTENT_TYPE_HEADER: &str = "application/json"; -#[derive(Debug)] -pub enum Error { - /// The `reqwest` client raised an error. - HttpClient(PrettyReqwestError), - /// The `reqwest_eventsource` client raised an error. - SseClient(Box), - /// The server returned an error message where the body was able to be parsed. - ServerMessage(ErrorMessage), - /// The server returned an error message with an array of errors. - ServerIndexedMessage(IndexedErrorMessage), - /// The server returned an error message where the body was unable to be parsed. - StatusCode(StatusCode), - /// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`. - InvalidUrl(SensitiveUrl), - /// The supplied validator client secret is invalid. - InvalidSecret(String), - /// The server returned a response with an invalid signature. It may be an impostor. - InvalidSignatureHeader, - /// The server returned a response without a signature header. It may be an impostor. - MissingSignatureHeader, - /// The server returned an invalid JSON response. - InvalidJson(serde_json::Error), - /// The server returned an invalid server-sent event. - InvalidServerSentEvent(String), - /// The server sent invalid response headers. - InvalidHeaders(String), - /// The server returned an invalid SSZ response. - InvalidSsz(ssz::DecodeError), - /// An I/O error occurred while loading an API token from disk. - TokenReadError(PathBuf, std::io::Error), - /// The client has been configured without a server pubkey, but requires one for this request. - NoServerPubkey, - /// The client has been configured without an API token, but requires one for this request. - NoToken, -} - -impl From for Error { - fn from(error: reqwest::Error) -> Self { - Error::HttpClient(error.into()) - } -} - -impl Error { - /// If the error has a HTTP status code, return it. - pub fn status(&self) -> Option { - match self { - Error::HttpClient(error) => error.inner().status(), - Error::SseClient(error) => { - if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() { - Some(*status) - } else { - None - } - } - Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), - Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), - Error::StatusCode(status) => Some(*status), - Error::InvalidUrl(_) => None, - Error::InvalidSecret(_) => None, - Error::InvalidSignatureHeader => None, - Error::MissingSignatureHeader => None, - Error::InvalidJson(_) => None, - Error::InvalidSsz(_) => None, - Error::InvalidServerSentEvent(_) => None, - Error::InvalidHeaders(_) => None, - Error::TokenReadError(..) => None, - Error::NoServerPubkey | Error::NoToken => None, - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} +/// Specific optimized timeout constants for HTTP requests involved in different validator duties. +/// This can help ensure that proper endpoint fallback occurs. +const HTTP_ATTESTATION_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT: u32 = 24; +const HTTP_ATTESTATION_AGGREGATOR_TIMEOUT_QUOTIENT: u32 = 24; // For DVT involving middleware only +const HTTP_LIVENESS_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_PROPOSAL_TIMEOUT_QUOTIENT: u32 = 2; +const HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_SYNC_COMMITTEE_CONTRIBUTION_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT: u32 = 24; // For DVT involving middleware only +const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; +const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; +const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; /// A struct to define a variety of different timeouts for different validator tasks to ensure /// proper fallback behaviour. @@ -135,11 +85,13 @@ pub struct Timeouts { pub attestation: Duration, pub attester_duties: Duration, pub attestation_subscriptions: Duration, + pub attestation_aggregators: Duration, pub liveness: Duration, pub proposal: Duration, pub proposer_duties: Duration, pub sync_committee_contribution: Duration, pub sync_duties: Duration, + pub sync_aggregators: Duration, pub inclusion_list: Duration, pub inclusion_list_duties: Duration, pub get_beacon_blocks_ssz: Duration, @@ -155,11 +107,13 @@ impl Timeouts { attestation: timeout, attester_duties: timeout, attestation_subscriptions: timeout, + attestation_aggregators: timeout, liveness: timeout, proposal: timeout, proposer_duties: timeout, sync_committee_contribution: timeout, sync_duties: timeout, + sync_aggregators: timeout, inclusion_list: timeout, inclusion_list_duties: timeout, get_beacon_blocks_ssz: timeout, @@ -169,14 +123,39 @@ impl Timeouts { default: timeout, } } + + pub fn use_optimized_timeouts(base_timeout: Duration) -> Self { + Timeouts { + attestation: base_timeout / HTTP_ATTESTATION_TIMEOUT_QUOTIENT, + attester_duties: base_timeout / HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT, + attestation_subscriptions: base_timeout + / HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT, + attestation_aggregators: base_timeout / HTTP_ATTESTATION_AGGREGATOR_TIMEOUT_QUOTIENT, + liveness: base_timeout / HTTP_LIVENESS_TIMEOUT_QUOTIENT, + proposal: base_timeout / HTTP_PROPOSAL_TIMEOUT_QUOTIENT, + proposer_duties: base_timeout / HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT, + sync_committee_contribution: base_timeout + / HTTP_SYNC_COMMITTEE_CONTRIBUTION_TIMEOUT_QUOTIENT, + sync_duties: base_timeout / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, + // TODO(EIP7805) check timeouts + inclusion_list_duties: base_timeout, + inclusion_list: base_timeout, + sync_aggregators: base_timeout / HTTP_SYNC_AGGREGATOR_TIMEOUT_QUOTIENT, + get_beacon_blocks_ssz: base_timeout / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, + get_debug_beacon_states: base_timeout / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, + get_deposit_snapshot: base_timeout / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, + get_validator_block: base_timeout / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, + default: base_timeout / HTTP_DEFAULT_TIMEOUT_QUOTIENT, + } + } } /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Beacon Node HTTP server (`http_api`). -#[derive(Clone, Debug, Derivative)] -#[derivative(PartialEq)] +#[derive(Clone, Debug, Educe)] +#[educe(PartialEq)] pub struct BeaconNodeHttpClient { - #[derivative(PartialEq = "ignore")] + #[educe(PartialEq(ignore))] client: reqwest::Client, server: SensitiveUrl, timeouts: Timeouts, @@ -190,12 +169,6 @@ impl fmt::Display for BeaconNodeHttpClient { } } -impl AsRef for BeaconNodeHttpClient { - fn as_ref(&self) -> &str { - self.server.as_ref() - } -} - impl BeaconNodeHttpClient { pub fn new(server: SensitiveUrl, timeouts: Timeouts) -> Self { Self { @@ -216,10 +189,14 @@ impl BeaconNodeHttpClient { timeouts, } } + // Returns a reference to the `SensitiveUrl` of the server. + pub fn server(&self) -> &SensitiveUrl { + &self.server + } /// Return the path with the standard `/eth/vX` prefix applied. fn eth_path(&self, version: EndpointVersion) -> Result { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -459,7 +436,7 @@ impl BeaconNodeHttpClient { .post(url) .timeout(timeout.unwrap_or(self.timeouts.default)); let response = builder.json(body).send().await?; - ok_or_error(response).await + success_or_error(response).await } /// Generic POST function supporting arbitrary responses and timeouts. @@ -479,7 +456,7 @@ impl BeaconNodeHttpClient { .json(body) .send() .await?; - ok_or_error(response).await + success_or_error(response).await } /// Generic POST function that includes octet-stream content type header. @@ -496,7 +473,7 @@ impl BeaconNodeHttpClient { HeaderValue::from_static("application/octet-stream"), ); let response = builder.headers(headers).json(body).send().await?; - ok_or_error(response).await + success_or_error(response).await } /// Generic POST function supporting arbitrary responses and timeouts. @@ -521,7 +498,7 @@ impl BeaconNodeHttpClient { HeaderValue::from_static("application/octet-stream"), ); let response = builder.headers(headers).body(body).send().await?; - ok_or_error(response).await + success_or_error(response).await } /// `GET beacon/genesis` @@ -670,6 +647,29 @@ impl BeaconNodeHttpClient { self.post_with_opt_response(path, &request).await } + /// `POST beacon/states/{state_id}/validator_identities` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_states_validator_identities( + &self, + state_id: StateId, + ids: Vec, + ) -> Result>>, Error> + { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("validator_identities"); + + let request = ValidatorIdentitiesRequestBody { ids }; + + self.post_with_opt_response(path, &request).await + } + /// `GET beacon/states/{state_id}/validators?id,status` /// /// Returns `Ok(None)` on a 404 error. @@ -842,7 +842,8 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_states_pending_deposits( &self, state_id: StateId, - ) -> Result>>, Error> { + ) -> Result>>, Error> + { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -852,7 +853,9 @@ impl BeaconNodeHttpClient { .push(&state_id.to_string()) .push("pending_deposits"); - self.get_opt(path).await + self.get_fork_contextual(path, |fork| fork) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/states/{state_id}/pending_partial_withdrawals` @@ -861,8 +864,10 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_states_pending_partial_withdrawals( &self, state_id: StateId, - ) -> Result>>, Error> - { + ) -> Result< + Option>>, + Error, + > { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -872,7 +877,9 @@ impl BeaconNodeHttpClient { .push(&state_id.to_string()) .push("pending_partial_withdrawals"); - self.get_opt(path).await + self.get_fork_contextual(path, |fork| fork) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/states/{state_id}/pending_consolidations` @@ -881,7 +888,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_states_pending_consolidations( &self, state_id: StateId, - ) -> Result>>, Error> + ) -> Result>>, Error> { let mut path = self.eth_path(V1)?; @@ -892,7 +899,9 @@ impl BeaconNodeHttpClient { .push(&state_id.to_string()) .push("pending_consolidations"); - self.get_opt(path).await + self.get_fork_contextual(path, |fork| fork) + .await + .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } /// `GET beacon/light_client/updates` @@ -927,6 +936,32 @@ impl BeaconNodeHttpClient { }) } + /// `GET beacon/light_client/updates` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_light_client_updates_ssz( + &self, + start_period: u64, + count: u64, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("light_client") + .push("updates"); + + path.query_pairs_mut() + .append_pair("start_period", &start_period.to_string()); + + path.query_pairs_mut() + .append_pair("count", &count.to_string()); + + self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.default) + .await + } + /// `GET beacon/light_client/bootstrap` /// /// Returns `Ok(None)` on a 404 error. @@ -1169,16 +1204,17 @@ impl BeaconNodeHttpClient { &self, block_contents: &PublishBlockRequest, validation_level: Option, - ) -> Result<(), Error> { - self.post_generic_with_consensus_version( - self.post_beacon_blocks_v2_path(validation_level)?, - block_contents, - Some(self.timeouts.proposal), - block_contents.signed_block().message().body().fork_name(), - ) - .await?; + ) -> Result { + let response = self + .post_generic_with_consensus_version( + self.post_beacon_blocks_v2_path(validation_level)?, + block_contents, + Some(self.timeouts.proposal), + block_contents.signed_block().message().body().fork_name(), + ) + .await?; - Ok(()) + Ok(response) } /// `POST v2/beacon/blocks` @@ -1186,16 +1222,17 @@ impl BeaconNodeHttpClient { &self, block_contents: &PublishBlockRequest, validation_level: Option, - ) -> Result<(), Error> { - self.post_generic_with_consensus_version_and_ssz_body( - self.post_beacon_blocks_v2_path(validation_level)?, - block_contents.as_ssz_bytes(), - Some(self.timeouts.proposal), - block_contents.signed_block().message().body().fork_name(), - ) - .await?; + ) -> Result { + let response = self + .post_generic_with_consensus_version_and_ssz_body( + self.post_beacon_blocks_v2_path(validation_level)?, + block_contents.as_ssz_bytes(), + Some(self.timeouts.proposal), + block_contents.signed_block().message().body().fork_name(), + ) + .await?; - Ok(()) + Ok(response) } /// `POST v2/beacon/blinded_blocks` @@ -1203,16 +1240,17 @@ impl BeaconNodeHttpClient { &self, signed_block: &SignedBlindedBeaconBlock, validation_level: Option, - ) -> Result<(), Error> { - self.post_generic_with_consensus_version( - self.post_beacon_blinded_blocks_v2_path(validation_level)?, - signed_block, - Some(self.timeouts.proposal), - signed_block.message().body().fork_name(), - ) - .await?; + ) -> Result { + let response = self + .post_generic_with_consensus_version( + self.post_beacon_blinded_blocks_v2_path(validation_level)?, + signed_block, + Some(self.timeouts.proposal), + signed_block.message().body().fork_name(), + ) + .await?; - Ok(()) + Ok(response) } /// `POST v2/beacon/blinded_blocks` @@ -1220,16 +1258,17 @@ impl BeaconNodeHttpClient { &self, signed_block: &SignedBlindedBeaconBlock, validation_level: Option, - ) -> Result<(), Error> { - self.post_generic_with_consensus_version_and_ssz_body( - self.post_beacon_blinded_blocks_v2_path(validation_level)?, - signed_block.as_ssz_bytes(), - Some(self.timeouts.proposal), - signed_block.message().body().fork_name(), - ) - .await?; + ) -> Result { + let response = self + .post_generic_with_consensus_version_and_ssz_body( + self.post_beacon_blinded_blocks_v2_path(validation_level)?, + signed_block.as_ssz_bytes(), + Some(self.timeouts.proposal), + signed_block.message().body().fork_name(), + ) + .await?; - Ok(()) + Ok(response) } /// Path for `v2/beacon/blocks` @@ -1244,7 +1283,7 @@ impl BeaconNodeHttpClient { } /// Path for `v1/beacon/blob_sidecars/{block_id}` - pub fn get_blobs_path(&self, block_id: BlockId) -> Result { + pub fn get_blob_sidecars_path(&self, block_id: BlockId) -> Result { let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1254,6 +1293,17 @@ impl BeaconNodeHttpClient { Ok(path) } + /// Path for `v1/beacon/blobs/{blob_id}` + pub fn get_blobs_path(&self, block_id: BlockId) -> Result { + let mut path = self.eth_path(V1)?; + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blobs") + .push(&block_id.to_string()); + Ok(path) + } + /// Path for `v1/beacon/blinded_blocks/{block_id}` pub fn get_beacon_blinded_blocks_path(&self, block_id: BlockId) -> Result { let mut path = self.eth_path(V1)?; @@ -1282,13 +1332,13 @@ impl BeaconNodeHttpClient { /// `GET v1/beacon/blob_sidecars/{block_id}` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_blobs( + pub async fn get_blob_sidecars( &self, block_id: BlockId, indices: Option<&[u64]>, spec: &ChainSpec, ) -> Result>>, Error> { - let mut path = self.get_blobs_path(block_id)?; + let mut path = self.get_blob_sidecars_path(block_id)?; if let Some(indices) = indices { let indices_string = indices .iter() @@ -1300,12 +1350,39 @@ impl BeaconNodeHttpClient { } self.get_fork_contextual(path, |fork| { - (fork, spec.max_blobs_per_block_by_fork(fork) as usize) + // TODO(EIP-7892): this will overestimate the max number of blobs + // It would be better if we could get an epoch passed into this function + (fork, spec.max_blobs_per_block_within_fork(fork) as usize) }) .await .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } + /// `GET v1/beacon/blobs/{block_id}` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_blobs( + &self, + block_id: BlockId, + versioned_hashes: Option<&[Hash256]>, + ) -> Result>>>, Error> + { + let mut path = self.get_blobs_path(block_id)?; + if let Some(hashes) = versioned_hashes { + let hashes_string = hashes + .iter() + .map(|hash| hash.to_string()) + .collect::>() + .join(","); + path.query_pairs_mut() + .append_pair("versioned_hashes", &hashes_string); + } + + self.get_opt(path) + .await + .map(|opt| opt.map(BeaconResponse::Unversioned)) + } + /// `GET v1/beacon/blinded_blocks/{block_id}` /// /// Returns `Ok(None)` on a 404 error. @@ -1436,29 +1513,10 @@ impl BeaconNodeHttpClient { .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } - /// `POST v1/beacon/pool/attestations` - pub async fn post_beacon_pool_attestations_v1( - &self, - attestations: &[Attestation], - ) -> Result<(), Error> { - let mut path = self.eth_path(V1)?; - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("beacon") - .push("pool") - .push("attestations"); - - self.post_with_timeout(path, &attestations, self.timeouts.attestation) - .await?; - - Ok(()) - } - /// `POST v2/beacon/pool/attestations` pub async fn post_beacon_pool_attestations_v2( &self, - attestations: Either>, Vec>, + attestations: Vec, fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V2)?; @@ -1469,26 +1527,13 @@ impl BeaconNodeHttpClient { .push("pool") .push("attestations"); - match attestations { - Either::Right(attestations) => { - self.post_with_timeout_and_consensus_header( - path, - &attestations, - self.timeouts.attestation, - fork_name, - ) - .await?; - } - Either::Left(attestations) => { - self.post_with_timeout_and_consensus_header( - path, - &attestations, - self.timeouts.attestation, - fork_name, - ) - .await?; - } - }; + self.post_with_timeout_and_consensus_header( + path, + &attestations, + self.timeouts.attestation, + fork_name, + ) + .await?; Ok(()) } @@ -1736,18 +1781,6 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `GET beacon/deposit_snapshot` - pub async fn get_deposit_snapshot(&self) -> Result, Error> { - let mut path = self.eth_path(V1)?; - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("beacon") - .push("deposit_snapshot"); - self.get_opt_with_timeout::, _>(path, self.timeouts.get_deposit_snapshot) - .await - .map(|opt| opt.map(|r| r.data)) - } - /// `POST beacon/rewards/sync_committee` pub async fn post_beacon_rewards_sync_committee( &self, @@ -1976,7 +2009,7 @@ impl BeaconNodeHttpClient { /// `GET node/peers/{peer_id}` pub async fn get_node_peers_by_id( &self, - peer_id: PeerId, + peer_id: &str, ) -> Result, Error> { let mut path = self.eth_path(V1)?; @@ -1984,7 +2017,7 @@ impl BeaconNodeHttpClient { .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("node") .push("peers") - .push(&peer_id.to_string()); + .push(peer_id); self.get(path).await } @@ -2200,6 +2233,7 @@ impl BeaconNodeHttpClient { graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, + graffiti_policy: Option, ) -> Result { let mut path = self.eth_path(V3)?; @@ -2227,6 +2261,14 @@ impl BeaconNodeHttpClient { .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); } + // Only append the HTTP URL request if the graffiti_policy is to AppendClientVersions + // If PreserveUserGraffiti (default), then the HTTP URL request does not contain graffiti_policy + // so that the default case is compliant to the spec + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + Ok(path) } @@ -2237,6 +2279,7 @@ impl BeaconNodeHttpClient { randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, builder_booster_factor: Option, + graffiti_policy: Option, ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { self.get_validator_blocks_v3_modular( slot, @@ -2244,6 +2287,7 @@ impl BeaconNodeHttpClient { graffiti, SkipRandaoVerification::No, builder_booster_factor, + graffiti_policy, ) .await } @@ -2256,6 +2300,7 @@ impl BeaconNodeHttpClient { graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, + graffiti_policy: Option, ) -> Result<(JsonProduceBlockV3Response, ProduceBlockV3Metadata), Error> { let path = self .get_validator_blocks_v3_path( @@ -2264,6 +2309,7 @@ impl BeaconNodeHttpClient { graffiti, skip_randao_verification, builder_booster_factor, + graffiti_policy, ) .await?; @@ -2306,6 +2352,7 @@ impl BeaconNodeHttpClient { randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, builder_booster_factor: Option, + graffiti_policy: Option, ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { self.get_validator_blocks_v3_modular_ssz::( slot, @@ -2313,6 +2360,7 @@ impl BeaconNodeHttpClient { graffiti, SkipRandaoVerification::No, builder_booster_factor, + graffiti_policy, ) .await } @@ -2325,6 +2373,7 @@ impl BeaconNodeHttpClient { graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, + graffiti_policy: Option, ) -> Result<(ProduceBlockV3Response, ProduceBlockV3Metadata), Error> { let path = self .get_validator_blocks_v3_path( @@ -2333,6 +2382,7 @@ impl BeaconNodeHttpClient { graffiti, skip_randao_verification, builder_booster_factor, + graffiti_policy, ) .await?; @@ -2633,7 +2683,7 @@ impl BeaconNodeHttpClient { ids: &[u64], epoch: Epoch, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -2801,10 +2851,11 @@ impl BeaconNodeHttpClient { } /// `GET events?topics` + #[cfg(feature = "events")] pub async fn get_events( &self, topic: &[EventTopic], - ) -> Result, Error>>, Error> { + ) -> Result, Error>> + use, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -2864,21 +2915,40 @@ impl BeaconNodeHttpClient { ) .await } -} -/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an -/// appropriate error message. -pub async fn ok_or_error(response: Response) -> Result { - let status = response.status(); + /// `POST validator/beacon_committee_selections` + pub async fn post_validator_beacon_committee_selections( + &self, + selections: &[BeaconCommitteeSelection], + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; - if status == StatusCode::OK { - Ok(response) - } else if let Ok(message) = response.json().await { - match message { - ResponseError::Message(message) => Err(Error::ServerMessage(message)), - ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)), - } - } else { - Err(Error::StatusCode(status)) + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("beacon_committee_selections"); + + self.post_with_timeout_and_response( + path, + &selections, + self.timeouts.attestation_aggregators, + ) + .await + } + + /// `POST validator/sync_committee_selections` + pub async fn post_validator_sync_committee_selections( + &self, + selections: &[SyncCommitteeSelection], + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("sync_committee_selections"); + + self.post_with_timeout_and_response(path, &selections, self.timeouts.sync_aggregators) + .await } } diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 9a5d9100cf..993c263cbf 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -3,15 +3,13 @@ mod attestation_performance; mod block_packing_efficiency; mod block_rewards; +mod custody; pub mod sync_state; use crate::{ + BeaconNodeHttpClient, DepositData, Error, Hash256, Slot, lighthouse::sync_state::SyncState, - types::{ - AdminPeer, DepositTreeSnapshot, Epoch, FinalizedExecutionBlock, GenericResponse, - ValidatorId, - }, - BeaconNodeHttpClient, DepositData, Error, Eth1Data, Hash256, Slot, + types::{AdminPeer, Epoch, GenericResponse, ValidatorId}, }; use proto_array::core::ProtoArray; use serde::{Deserialize, Serialize}; @@ -25,6 +23,7 @@ pub use block_packing_efficiency::{ BlockPackingEfficiency, BlockPackingEfficiencyQuery, ProposerInfo, UniqueAttestation, }; pub use block_rewards::{AttestationRewards, BlockReward, BlockRewardMeta, BlockRewardsQuery}; +pub use custody::CustodyInfo; // Define "legacy" implementations of `Option` which use four bytes for encoding the union // selector. @@ -159,18 +158,6 @@ pub struct ProcessHealth { pub pid_process_seconds_total: u64, } -/// Indicates how up-to-date the Eth1 caches are. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Eth1SyncStatusData { - pub head_block_number: Option, - pub head_block_timestamp: Option, - pub latest_cached_block_number: Option, - pub latest_cached_block_timestamp: Option, - pub voting_target_timestamp: u64, - pub eth1_node_sync_status_percentage: f64, - pub lighthouse_is_cached_and_ready: bool, -} - /// A fully parsed eth1 deposit contract log. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)] pub struct DepositLog { @@ -183,45 +170,10 @@ pub struct DepositLog { pub signature_is_valid: bool, } -/// A block of the eth1 chain. -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)] -pub struct Eth1Block { - pub hash: Hash256, - pub timestamp: u64, - pub number: u64, - #[ssz(with = "four_byte_option_hash256")] - pub deposit_root: Option, - #[ssz(with = "four_byte_option_u64")] - pub deposit_count: Option, -} - -impl Eth1Block { - pub fn eth1_data(self) -> Option { - Some(Eth1Data { - deposit_root: self.deposit_root?, - deposit_count: self.deposit_count?, - block_hash: self.hash, - }) - } -} - -impl From for FinalizedExecutionBlock { - fn from(eth1_block: Eth1Block) -> Self { - Self { - deposit_count: eth1_block.deposit_count.unwrap_or(0), - deposit_root: eth1_block - .deposit_root - .unwrap_or_else(|| DepositTreeSnapshot::default().deposit_root), - block_hash: eth1_block.hash, - block_height: eth1_block.number, - } - } -} - impl BeaconNodeHttpClient { /// `GET lighthouse/health` pub async fn get_lighthouse_health(&self) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -233,7 +185,7 @@ impl BeaconNodeHttpClient { /// `GET lighthouse/syncing` pub async fn get_lighthouse_syncing(&self) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -243,6 +195,32 @@ impl BeaconNodeHttpClient { self.get(path).await } + /// `GET lighthouse/custody/info` + pub async fn get_lighthouse_custody_info(&self) -> Result { + let mut path = self.server.expose_full().clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("custody") + .push("info"); + + self.get(path).await + } + + /// `POST lighthouse/custody/backfill` + pub async fn post_lighthouse_custody_backfill(&self) -> Result<(), Error> { + let mut path = self.server.expose_full().clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("custody") + .push("backfill"); + + self.post(path, &()).await + } + /* * Note: * @@ -253,7 +231,7 @@ impl BeaconNodeHttpClient { /// `GET lighthouse/proto_array` pub async fn get_lighthouse_proto_array(&self) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -268,7 +246,7 @@ impl BeaconNodeHttpClient { &self, epoch: Epoch, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -286,7 +264,7 @@ impl BeaconNodeHttpClient { epoch: Epoch, validator_id: ValidatorId, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -298,66 +276,9 @@ impl BeaconNodeHttpClient { self.get(path).await } - /// `GET lighthouse/eth1/syncing` - pub async fn get_lighthouse_eth1_syncing( - &self, - ) -> Result, Error> { - let mut path = self.server.full.clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("eth1") - .push("syncing"); - - self.get(path).await - } - - /// `GET lighthouse/eth1/block_cache` - pub async fn get_lighthouse_eth1_block_cache( - &self, - ) -> Result>, Error> { - let mut path = self.server.full.clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("eth1") - .push("block_cache"); - - self.get(path).await - } - - /// `GET lighthouse/eth1/deposit_cache` - pub async fn get_lighthouse_eth1_deposit_cache( - &self, - ) -> Result>, Error> { - let mut path = self.server.full.clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("eth1") - .push("deposit_cache"); - - self.get(path).await - } - - /// `GET lighthouse/staking` - pub async fn get_lighthouse_staking(&self) -> Result { - let mut path = self.server.full.clone(); - - path.path_segments_mut() - .map_err(|()| Error::InvalidUrl(self.server.clone()))? - .push("lighthouse") - .push("staking"); - - self.get_opt::<(), _>(path).await.map(|opt| opt.is_some()) - } - /// `POST lighthouse/database/reconstruct` pub async fn post_lighthouse_database_reconstruct(&self) -> Result { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -370,7 +291,7 @@ impl BeaconNodeHttpClient { /// `POST lighthouse/add_peer` pub async fn post_lighthouse_add_peer(&self, req: AdminPeer) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -382,7 +303,7 @@ impl BeaconNodeHttpClient { /// `POST lighthouse/remove_peer` pub async fn post_lighthouse_remove_peer(&self, req: AdminPeer) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -402,7 +323,7 @@ impl BeaconNodeHttpClient { start_slot: Slot, end_slot: Slot, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -423,7 +344,7 @@ impl BeaconNodeHttpClient { start_epoch: Epoch, end_epoch: Epoch, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -445,7 +366,7 @@ impl BeaconNodeHttpClient { end_epoch: Epoch, target: String, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? diff --git a/common/eth2/src/lighthouse/custody.rs b/common/eth2/src/lighthouse/custody.rs new file mode 100644 index 0000000000..c9f9c16520 --- /dev/null +++ b/common/eth2/src/lighthouse/custody.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use types::Slot; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct CustodyInfo { + pub earliest_custodied_data_column_slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + pub custody_group_count: u64, + #[serde(with = "serde_utils::quoted_u64_vec")] + pub custody_columns: Vec, +} diff --git a/common/eth2/src/lighthouse/sync_state.rs b/common/eth2/src/lighthouse/sync_state.rs index 0327f7073f..9f6f3b52e0 100644 --- a/common/eth2/src/lighthouse/sync_state.rs +++ b/common/eth2/src/lighthouse/sync_state.rs @@ -15,6 +15,10 @@ pub enum SyncState { /// specified by its peers. Once completed, the node enters this sync state and attempts to /// download all required historical blocks. BackFillSyncing { completed: usize, remaining: usize }, + /// The node is undertaking a custody backfill sync. This occurs for a node that has completed forward and + /// backfill sync and has undergone a custody count change. During custody backfill sync the node attempts + /// to backfill its new column custody requirements up to the data availability window. + CustodyBackFillSyncing { completed: usize, remaining: usize }, /// The node has completed syncing a finalized chain and is in the process of re-evaluating /// which sync state to progress to. SyncTransition, @@ -39,6 +43,17 @@ pub enum BackFillState { Failed, } +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +/// The state of the custody backfill sync. +pub enum CustodyBackFillState { + /// We are currently backfilling custody columns. + Syncing, + /// A custody backfill sync has completed. + Completed, + /// A custody sync should is set to Pending for various reasons. + Pending(String), +} + impl PartialEq for SyncState { fn eq(&self, other: &Self) -> bool { matches!( @@ -54,6 +69,10 @@ impl PartialEq for SyncState { SyncState::BackFillSyncing { .. }, SyncState::BackFillSyncing { .. } ) + | ( + SyncState::CustodyBackFillSyncing { .. }, + SyncState::CustodyBackFillSyncing { .. } + ) ) } } @@ -65,8 +84,8 @@ impl SyncState { SyncState::SyncingFinalized { .. } => true, SyncState::SyncingHead { .. } => true, SyncState::SyncTransition => true, - // Backfill doesn't effect any logic, we consider this state, not syncing. - SyncState::BackFillSyncing { .. } => false, + // Both backfill and custody backfill don't effect any logic, we consider this state, not syncing. + SyncState::BackFillSyncing { .. } | SyncState::CustodyBackFillSyncing { .. } => false, SyncState::Synced => false, SyncState::Stalled => false, } @@ -77,7 +96,7 @@ impl SyncState { SyncState::SyncingFinalized { .. } => true, SyncState::SyncingHead { .. } => false, SyncState::SyncTransition => false, - SyncState::BackFillSyncing { .. } => false, + SyncState::BackFillSyncing { .. } | SyncState::CustodyBackFillSyncing { .. } => false, SyncState::Synced => false, SyncState::Stalled => false, } @@ -87,7 +106,12 @@ impl SyncState { /// /// NOTE: We consider the node synced if it is fetching old historical blocks. pub fn is_synced(&self) -> bool { - matches!(self, SyncState::Synced | SyncState::BackFillSyncing { .. }) + matches!( + self, + SyncState::Synced + | SyncState::BackFillSyncing { .. } + | SyncState::CustodyBackFillSyncing { .. } + ) } /// Returns true if the node is *stalled*, i.e. has no synced peers. @@ -108,6 +132,9 @@ impl std::fmt::Display for SyncState { SyncState::Stalled => write!(f, "Stalled"), SyncState::SyncTransition => write!(f, "Evaluating known peers"), SyncState::BackFillSyncing { .. } => write!(f, "Syncing Historical Blocks"), + SyncState::CustodyBackFillSyncing { .. } => { + write!(f, "Syncing Historical Data Columns") + } } } } diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 1d1abcac79..3c850fcb05 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -1,11 +1,12 @@ use super::types::*; -use crate::Error; +use crate::{Error, success_or_error}; +use bls::PublicKeyBytes; use reqwest::{ - header::{HeaderMap, HeaderValue}, IntoUrl, + header::{HeaderMap, HeaderValue}, }; use sensitive_url::SensitiveUrl; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use std::fmt::{self, Display}; use std::fs; use std::path::Path; @@ -145,7 +146,7 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } /// Perform a HTTP DELETE request, returning the `Response` for further processing. @@ -157,7 +158,7 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } async fn get(&self, url: U) -> Result { @@ -218,7 +219,7 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } async fn post( @@ -250,7 +251,7 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await?; + success_or_error(response).await?; Ok(()) } @@ -268,7 +269,7 @@ impl ValidatorClientHttpClient { .send() .await .map_err(Error::from)?; - ok_or_error(response).await + success_or_error(response).await } /// Perform a HTTP DELETE request. @@ -283,7 +284,7 @@ impl ValidatorClientHttpClient { /// `GET lighthouse/version` pub async fn get_lighthouse_version(&self) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -295,7 +296,7 @@ impl ValidatorClientHttpClient { /// `GET lighthouse/health` pub async fn get_lighthouse_health(&self) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -309,7 +310,7 @@ impl ValidatorClientHttpClient { pub async fn get_lighthouse_spec( &self, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -323,7 +324,7 @@ impl ValidatorClientHttpClient { pub async fn get_lighthouse_validators( &self, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -338,7 +339,7 @@ impl ValidatorClientHttpClient { &self, validator_pubkey: &PublicKeyBytes, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -354,7 +355,7 @@ impl ValidatorClientHttpClient { &self, validators: Vec, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -369,7 +370,7 @@ impl ValidatorClientHttpClient { &self, request: &CreateValidatorsMnemonicRequest, ) -> Result>, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -385,7 +386,7 @@ impl ValidatorClientHttpClient { &self, request: &KeystoreValidatorsPostRequest, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -401,7 +402,7 @@ impl ValidatorClientHttpClient { &self, request: &[Web3SignerValidatorRequest], ) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -424,7 +425,7 @@ impl ValidatorClientHttpClient { prefer_builder_proposals: Option, graffiti: Option, ) -> Result<(), Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -451,7 +452,7 @@ impl ValidatorClientHttpClient { &self, req: &DeleteKeystoresRequest, ) -> Result { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -462,7 +463,7 @@ impl ValidatorClientHttpClient { } fn make_keystores_url(&self) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") @@ -472,7 +473,7 @@ impl ValidatorClientHttpClient { } fn make_remotekeys_url(&self) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") @@ -482,7 +483,7 @@ impl ValidatorClientHttpClient { } fn make_fee_recipient_url(&self, pubkey: &PublicKeyBytes) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") @@ -494,7 +495,7 @@ impl ValidatorClientHttpClient { } fn make_graffiti_url(&self, pubkey: &PublicKeyBytes) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") @@ -506,7 +507,7 @@ impl ValidatorClientHttpClient { } fn make_gas_limit_url(&self, pubkey: &PublicKeyBytes) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") @@ -519,7 +520,7 @@ impl ValidatorClientHttpClient { /// `GET lighthouse/auth` pub async fn get_auth(&self) -> Result { - let mut url = self.server.full.clone(); + let mut url = self.server.expose_full().clone(); url.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("lighthouse") @@ -635,7 +636,7 @@ impl ValidatorClientHttpClient { pubkey: &PublicKeyBytes, epoch: Option, ) -> Result, Error> { - let mut path = self.server.full.clone(); + let mut path = self.server.expose_full().clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -681,20 +682,3 @@ impl ValidatorClientHttpClient { self.delete(url).await } } - -/// Returns `Ok(response)` if the response is a `200 OK` response or a -/// `202 Accepted` response. Otherwise, creates an appropriate error message. -async fn ok_or_error(response: Response) -> Result { - let status = response.status(); - - if status == StatusCode::OK - || status == StatusCode::ACCEPTED - || status == StatusCode::NO_CONTENT - { - Ok(response) - } else if let Ok(message) = response.json().await { - Err(Error::ServerMessage(message)) - } else { - Err(Error::StatusCode(status)) - } -} diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index ae192312bd..c54252b9e3 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -1,9 +1,10 @@ +use bls::PublicKeyBytes; use eth2_keystore::Keystore; use serde::{Deserialize, Serialize}; -use types::{Address, Graffiti, PublicKeyBytes}; +use types::{Address, Graffiti}; use zeroize::Zeroizing; -pub use slashing_protection::interchange::Interchange; +pub use eip_3076::Interchange; #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct GetFeeRecipientResponse { diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index d7d5a00df5..07f8421dc5 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -1,8 +1,8 @@ pub use crate::lighthouse::Health; pub use crate::lighthouse_vc::std_types::*; pub use crate::types::{GenericResponse, VersionData}; +use bls::{PublicKey, PublicKeyBytes}; use eth2_keystore::Keystore; -use graffiti::GraffitiString; use serde::{Deserialize, Serialize}; use std::path::PathBuf; pub use types::*; @@ -197,3 +197,13 @@ pub struct SingleExportKeystoresResponse { pub struct SetGraffitiRequest { pub graffiti: GraffitiString, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct UpdateCandidatesRequest { + pub beacon_nodes: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct UpdateCandidatesResponse { + pub new_beacon_nodes_list: Vec, +} diff --git a/common/eth2/src/mixin.rs b/common/eth2/src/mixin.rs index a33cf8a40c..c26f4f15b6 100644 --- a/common/eth2/src/mixin.rs +++ b/common/eth2/src/mixin.rs @@ -1,5 +1,5 @@ -use crate::{types::Accept, Error, CONSENSUS_VERSION_HEADER}; -use reqwest::{header::ACCEPT, RequestBuilder, Response, StatusCode}; +use crate::{CONSENSUS_VERSION_HEADER, Error, types::Accept}; +use reqwest::{RequestBuilder, Response, StatusCode, header::ACCEPT}; use std::str::FromStr; use types::ForkName; diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index dc7dcc120d..52f3983869 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1,70 +1,43 @@ //! This module exposes a superset of the `types` crate. It adds additional types that are only //! required for the HTTP API. +pub use types::*; + use crate::{ - Error as ServerError, CONSENSUS_BLOCK_VALUE_HEADER, CONSENSUS_VERSION_HEADER, - EXECUTION_PAYLOAD_BLINDED_HEADER, EXECUTION_PAYLOAD_VALUE_HEADER, + CONSENSUS_BLOCK_VALUE_HEADER, CONSENSUS_VERSION_HEADER, EXECUTION_PAYLOAD_BLINDED_HEADER, + EXECUTION_PAYLOAD_VALUE_HEADER, Error as ServerError, }; -use enr::{CombinedKey, Enr}; -use mediatype::{names, MediaType, MediaTypeList}; -use multiaddr::Multiaddr; +use bls::{PublicKeyBytes, SecretKey, Signature, SignatureBytes}; +use context_deserialize::ContextDeserialize; +use mediatype::{MediaType, MediaTypeList, names}; use reqwest::header::HeaderMap; use serde::{Deserialize, Deserializer, Serialize}; use serde_utils::quoted_u64::Quoted; +use ssz::Encode; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; use std::fmt::{self, Display}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use superstruct::superstruct; + +#[cfg(test)] use test_random_derive::TestRandom; -use types::beacon_block_body::KzgCommitments; +#[cfg(test)] use types::test_utils::TestRandom; -pub use types::*; + +// TODO(mac): Temporary module and re-export hack to expose old `consensus/types` via `eth2/types`. +pub use crate::beacon_response::*; +pub mod beacon_response { + pub use crate::beacon_response::*; +} #[cfg(feature = "lighthouse")] use crate::lighthouse::BlockReward; -/// An API error serializable to JSON. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Error { - Indexed(IndexedErrorMessage), - Message(ErrorMessage), -} - -/// An API error serializable to JSON. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ErrorMessage { - pub code: u16, - pub message: String, - #[serde(default)] - pub stacktraces: Vec, -} - -/// An indexed API error serializable to JSON. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IndexedErrorMessage { - pub code: u16, - pub message: String, - pub failures: Vec, -} - -/// A single failure in an index of API errors, serializable to JSON. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Failure { - pub index: u64, - pub message: String, -} - -impl Failure { - pub fn new(index: usize, message: String) -> Self { - Self { - index: index as u64, - message, - } - } -} +// Re-export error types from the unified error module +pub use crate::error::{ErrorMessage, Failure, IndexedErrorMessage, ResponseError as Error}; /// The version of a single API endpoint, e.g. the `v1` in `/eth/v1/beacon/blocks`. #[derive(Debug, Clone, Copy, PartialEq)] @@ -349,6 +322,14 @@ pub struct ValidatorBalanceData { pub balance: u64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorIdentityData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub pubkey: PublicKeyBytes, + pub activation_epoch: Epoch, +} + // Implemented according to what is described here: // // https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ @@ -357,7 +338,7 @@ pub struct ValidatorBalanceData { // this proposal: // // https://hackmd.io/bQxMDRt1RbS1TLno8K4NPg?view -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ValidatorStatus { PendingInitialized, @@ -581,9 +562,9 @@ pub struct ChainHeadData { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IdentityData { pub peer_id: String, - pub enr: Enr, - pub p2p_addresses: Vec, - pub discovery_addresses: Vec, + pub enr: String, + pub p2p_addresses: Vec, + pub discovery_addresses: Vec, pub metadata: MetaData, } @@ -694,6 +675,12 @@ pub struct ValidatorBalancesRequestBody { pub ids: Vec, } +#[derive(Clone, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ValidatorIdentitiesRequestBody { + pub ids: Vec, +} + #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct BlobIndicesQuery { @@ -701,6 +688,20 @@ pub struct BlobIndicesQuery { pub indices: Option>, } +#[derive(Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BlobsVersionedHashesQuery { + #[serde(default, deserialize_with = "option_query_vec")] + pub versioned_hashes: Option>, +} + +#[derive(Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DataColumnIndicesQuery { + #[serde(default, deserialize_with = "option_query_vec")] + pub indices: Option>, +} + #[derive(Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct ValidatorIndexData(#[serde(with = "serde_utils::quoted_u64_vec")] pub Vec); @@ -751,12 +752,20 @@ pub struct ProposerData { pub slot: Slot, } +#[derive(Clone, Copy, Serialize, Deserialize, Default, Debug)] +pub enum GraffitiPolicy { + #[default] + PreserveUserGraffiti, + AppendClientVersions, +} + #[derive(Clone, Deserialize)] pub struct ValidatorBlocksQuery { pub randao_reveal: SignatureBytes, pub graffiti: Option, pub skip_randao_verification: SkipRandaoVerification, pub builder_boost_factor: Option, + pub graffiti_policy: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)] @@ -807,16 +816,32 @@ pub struct LightClientUpdatesQuery { pub count: u64, } -#[derive(Encode, Decode)] -pub struct LightClientUpdateResponseChunk { +pub struct LightClientUpdateResponseChunk { pub response_chunk_len: u64, - pub response_chunk: LightClientUpdateResponseChunkInner, + pub response_chunk: LightClientUpdateResponseChunkInner, } -#[derive(Encode, Decode)] -pub struct LightClientUpdateResponseChunkInner { +impl Encode for LightClientUpdateResponseChunk { + fn is_ssz_fixed_len() -> bool { + false + } + + fn ssz_bytes_len(&self) -> usize { + 0_u64.ssz_bytes_len() + + self.response_chunk.context.len() + + self.response_chunk.payload.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.response_chunk_len.to_le_bytes()); + buf.extend_from_slice(&self.response_chunk.context); + self.response_chunk.payload.ssz_append(buf); + } +} + +pub struct LightClientUpdateResponseChunkInner { pub context: [u8; 4], - pub payload: Vec, + pub payload: LightClientUpdate, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -934,6 +959,23 @@ pub struct PeerCount { pub disconnecting: u64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BeaconCommitteeSelection { + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub slot: Slot, + pub selection_proof: Signature, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SyncCommitteeSelection { + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + pub subcommittee_index: u64, + pub selection_proof: Signature, +} // --------- Server Sent Event Types ----------- #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] @@ -965,6 +1007,35 @@ impl SseBlobSidecar { } } +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +pub struct SseDataColumnSidecar { + pub block_root: Hash256, + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub slot: Slot, + pub kzg_commitments: Vec, + pub versioned_hashes: Vec, +} + +impl SseDataColumnSidecar { + pub fn from_data_column_sidecar( + data_column_sidecar: &DataColumnSidecar, + ) -> SseDataColumnSidecar { + let kzg_commitments = data_column_sidecar.kzg_commitments.to_vec(); + let versioned_hashes = kzg_commitments + .iter() + .map(|c| c.calculate_versioned_hash()) + .collect(); + SseDataColumnSidecar { + block_root: data_column_sidecar.block_root(), + index: data_column_sidecar.index, + slot: data_column_sidecar.slot(), + kzg_commitments, + versioned_hashes, + } + } +} + #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct SseFinalizedCheckpoint { pub block: Hash256, @@ -1077,7 +1148,7 @@ impl<'de> ContextDeserialize<'de, ForkName> for SsePayloadAttributes { return Err(serde::de::Error::custom(format!( "SsePayloadAttributes failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Bellatrix => { Self::V1(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -1085,7 +1156,11 @@ impl<'de> ContextDeserialize<'de, ForkName> for SsePayloadAttributes { ForkName::Capella => { Self::V2(Deserialize::deserialize(deserializer).map_err(convert_err)?) } - ForkName::Deneb | ForkName::Electra | ForkName::Eip7805 | ForkName::Fulu => { + ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu + | ForkName::Eip7805 + | ForkName::Gloas => { Self::V3(Deserialize::deserialize(deserializer).map_err(convert_err)?) } }) @@ -1122,6 +1197,7 @@ pub enum EventKind { SingleAttestation(Box), Block(SseBlock), BlobSidecar(SseBlobSidecar), + DataColumnSidecar(SseDataColumnSidecar), FinalizedCheckpoint(SseFinalizedCheckpoint), Head(SseHead), VoluntaryExit(SignedVoluntaryExit), @@ -1146,6 +1222,7 @@ impl EventKind { EventKind::Head(_) => "head", EventKind::Block(_) => "block", EventKind::BlobSidecar(_) => "blob_sidecar", + EventKind::DataColumnSidecar(_) => "data_column_sidecar", EventKind::Attestation(_) => "attestation", EventKind::SingleAttestation(_) => "single_attestation", EventKind::VoluntaryExit(_) => "voluntary_exit", @@ -1182,6 +1259,11 @@ impl EventKind { "blob_sidecar" => Ok(EventKind::BlobSidecar(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Blob Sidecar: {:?}", e)), )?)), + "data_column_sidecar" => Ok(EventKind::DataColumnSidecar( + serde_json::from_str(data).map_err(|e| { + ServerError::InvalidServerSentEvent(format!("Data Column Sidecar: {:?}", e)) + })?, + )), "chain_reorg" => Ok(EventKind::ChainReorg(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Chain Reorg: {:?}", e)), )?)), @@ -1276,6 +1358,7 @@ pub enum EventTopic { Head, Block, BlobSidecar, + DataColumnSidecar, Attestation, SingleAttestation, VoluntaryExit, @@ -1303,6 +1386,7 @@ impl FromStr for EventTopic { "head" => Ok(EventTopic::Head), "block" => Ok(EventTopic::Block), "blob_sidecar" => Ok(EventTopic::BlobSidecar), + "data_column_sidecar" => Ok(EventTopic::DataColumnSidecar), "attestation" => Ok(EventTopic::Attestation), "single_attestation" => Ok(EventTopic::SingleAttestation), "voluntary_exit" => Ok(EventTopic::VoluntaryExit), @@ -1331,6 +1415,7 @@ impl fmt::Display for EventTopic { EventTopic::Head => write!(f, "head"), EventTopic::Block => write!(f, "block"), EventTopic::BlobSidecar => write!(f, "blob_sidecar"), + EventTopic::DataColumnSidecar => write!(f, "data_column_sidecar"), EventTopic::Attestation => write!(f, "attestation"), EventTopic::SingleAttestation => write!(f, "single_attestation"), EventTopic::VoluntaryExit => write!(f, "voluntary_exit"), @@ -1479,22 +1564,32 @@ pub struct ForkChoiceNode { pub weight: u64, pub validity: Option, pub execution_block_hash: Option, + pub extra_data: ForkChoiceExtraData, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ForkChoiceExtraData { + pub target_root: Hash256, + pub justified_root: Hash256, + pub finalized_root: Hash256, + pub unrealized_justified_root: Option, + pub unrealized_finalized_root: Option, + pub unrealized_justified_epoch: Option, + pub unrealized_finalized_epoch: Option, + pub execution_status: String, + pub best_child: Option, + pub best_descendant: Option, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BroadcastValidation { + #[default] Gossip, Consensus, ConsensusAndEquivocation, } -impl Default for BroadcastValidation { - fn default() -> Self { - Self::Gossip - } -} - impl Display for BroadcastValidation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -1527,7 +1622,7 @@ pub struct BroadcastValidationQuery { pub mod serde_status_code { use crate::StatusCode; - use serde::{de::Error, Deserialize, Serialize}; + use serde::{Deserialize, Serialize, de::Error}; pub fn serialize(status_code: &StatusCode, ser: S) -> Result where @@ -1611,8 +1706,8 @@ mod tests { BeaconBlock::::Deneb(BeaconBlockDeneb::empty(&spec)), Signature::empty(), ); - let blobs = BlobsList::::from(vec![Blob::::default()]); - let kzg_proofs = KzgProofs::::from(vec![KzgProof::empty()]); + let blobs = BlobsList::::try_from(vec![Blob::::default()]).unwrap(); + let kzg_proofs = KzgProofs::::try_from(vec![KzgProof::empty()]).unwrap(); let signed_block_contents = PublishBlockRequest::new(Arc::new(block), Some((kzg_proofs, blobs))); @@ -2152,7 +2247,8 @@ pub enum ContentType { Ssz, } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom)] +#[cfg_attr(test, derive(TestRandom))] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "E: EthSpec")] pub struct BlobsBundle { pub commitments: KzgCommitments, @@ -2245,6 +2341,14 @@ pub struct StandardAttestationRewards { pub total_rewards: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +#[serde(bound = "E: EthSpec")] +#[serde(transparent)] +pub struct BlobWrapper { + #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] + pub blob: Blob, +} + #[cfg(test)] mod test { use std::fmt::Debug; @@ -2353,6 +2457,9 @@ mod test { rng, )), ExecutionPayload::Fulu(ExecutionPayloadFulu::::random_for_test(rng)), + ExecutionPayload::Gloas(ExecutionPayloadGloas::::random_for_test( + rng, + )), ]; let merged_forks = &ForkName::list_all()[2..]; assert_eq!( @@ -2418,6 +2525,17 @@ mod test { blobs_bundle, } }, + { + let execution_payload = + ExecutionPayload::Gloas( + ExecutionPayloadGloas::::random_for_test(rng), + ); + let blobs_bundle = BlobsBundle::random_for_test(rng); + ExecutionPayloadAndBlobs { + execution_payload, + blobs_bundle, + } + }, ]; let blob_forks = &ForkName::list_all()[4..]; diff --git a/common/eth2_interop_keypairs/Cargo.toml b/common/eth2_interop_keypairs/Cargo.toml index c19b32014e..309ff233e6 100644 --- a/common/eth2_interop_keypairs/Cargo.toml +++ b/common/eth2_interop_keypairs/Cargo.toml @@ -3,6 +3,7 @@ name = "eth2_interop_keypairs" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } +autotests = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -15,3 +16,7 @@ serde_yaml = { workspace = true } [dev-dependencies] base64 = "0.13.0" + +[[test]] +name = "eth2_interop_keypairs_tests" +path = "tests/main.rs" diff --git a/common/eth2_interop_keypairs/tests/main.rs b/common/eth2_interop_keypairs/tests/main.rs new file mode 100644 index 0000000000..4ee50127f2 --- /dev/null +++ b/common/eth2_interop_keypairs/tests/main.rs @@ -0,0 +1,2 @@ +mod from_file; +mod generation; diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index da6c4dfd95..416ffb1975 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -6,19 +6,11 @@ edition = { workspace = true } build = "build.rs" -[build-dependencies] -eth2_config = { workspace = true } -zip = { workspace = true } - -[dev-dependencies] -ethereum_ssz = { workspace = true } -tempfile = { workspace = true } -tokio = { workspace = true } - [dependencies] bytes = { workspace = true } discv5 = { workspace = true } eth2_config = { workspace = true } +fixed_bytes = { workspace = true } kzg = { workspace = true } pretty_reqwest_error = { workspace = true } reqwest = { workspace = true } @@ -28,3 +20,12 @@ sha2 = { workspace = true } tracing = { workspace = true } types = { workspace = true } url = { workspace = true } + +[build-dependencies] +eth2_config = { workspace = true } +zip = { workspace = true } + +[dev-dependencies] +ethereum_ssz = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } diff --git a/common/eth2_network_config/build.rs b/common/eth2_network_config/build.rs index 3165930f4a..c1f5df4559 100644 --- a/common/eth2_network_config/build.rs +++ b/common/eth2_network_config/build.rs @@ -1,6 +1,6 @@ //! Extracts zipped genesis states on first run. use eth2_config::{ - Eth2NetArchiveAndDirectory, GenesisStateSource, ETH2_NET_DIRS, GENESIS_FILE_NAME, + ETH2_NET_DIRS, Eth2NetArchiveAndDirectory, GENESIS_FILE_NAME, GenesisStateSource, }; use std::fs::File; use std::io; diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index 4d4ccdf717..6bc41113d6 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -50,6 +50,9 @@ ELECTRA_FORK_EPOCH: 948224 # Thu Mar 6 2025 09:43:40 GMT+0000 # Fulu FULU_FORK_VERSION: 0x0600006f FULU_FORK_EPOCH: 18446744073709551615 +# Gloas +GLOAS_FORK_VERSION: 0x0700006f +GLOAS_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -149,9 +152,9 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 2 MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 # Fulu -NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 MAX_BLOBS_PER_BLOCK_FULU: 12 +# Gloas diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index 2158f97c09..d8286270e0 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -50,6 +50,9 @@ EIP7805_FORK_EPOCH: 18446744073709551615 # Fulu FULU_FORK_VERSION: 0x07000000 FULU_FORK_EPOCH: 18446744073709551615 +# Gloas +GLOAS_FORK_VERSION: 0x07000064 +GLOAS_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -136,9 +139,10 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 2 MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 256 # Fulu -NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 MAX_BLOBS_PER_BLOCK_FULU: 12 + +# Gloas \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index 19a3f79cc0..b1e9faea1d 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -38,12 +38,17 @@ ELECTRA_FORK_VERSION: 0x06017000 ELECTRA_FORK_EPOCH: 115968 # Fulu FULU_FORK_VERSION: 0x07017000 -FULU_FORK_EPOCH: 18446744073709551615 +FULU_FORK_EPOCH: 165120 +# Gloas +GLOAS_FORK_VERSION: 0x08017000 +GLOAS_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 1200 milliseconds +SLOT_DURATION_MS: 12000 # 14 (estimate from Eth1 mainnet) SECONDS_PER_ETH1_BLOCK: 14 # 2**8 (= 256) epochs ~27 hours @@ -52,6 +57,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- @@ -138,9 +155,30 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 9 MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 # Fulu -NUMBER_OF_COLUMNS: 128 +# 2**7 (= 128) groups NUMBER_OF_CUSTODY_GROUPS: 128 +# 2**7 (= 128) subnets DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +# MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS (= 128 * 128) sidecars +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +# 2**3 (= 8) samples SAMPLES_PER_SLOT: 8 +# 2**2 (= 4) sidecars CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +# 2**3 (= 8) sidecars +VALIDATOR_CUSTODY_REQUIREMENT: 8 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +# 2**12 (= 4,096) epochs +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + - EPOCH: 166400 + MAX_BLOBS_PER_BLOCK: 15 + - EPOCH: 167936 + MAX_BLOBS_PER_BLOCK: 21 + +# Gloas \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml index 5cca1cd037..256957e119 100644 --- a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml @@ -42,13 +42,19 @@ ELECTRA_FORK_EPOCH: 2048 # Fulu FULU_FORK_VERSION: 0x70000910 -FULU_FORK_EPOCH: 18446744073709551615 +FULU_FORK_EPOCH: 50688 + +# Gloas +GLOAS_FORK_VERSION: 0x80000910 +GLOAS_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 12000 milliseconds +SLOT_DURATION_MS: 12000 # 14 (estimate from Eth1 mainnet) SECONDS_PER_ETH1_BLOCK: 12 # 2**8 (= 256) epochs ~27 hours @@ -57,6 +63,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- @@ -150,14 +168,34 @@ WHISK_EPOCHS_PER_SHUFFLING_PHASE: 256 WHISK_PROPOSER_SELECTION_GAP: 2 # Fulu -NUMBER_OF_COLUMNS: 128 +# 2**7 (= 128) groups NUMBER_OF_CUSTODY_GROUPS: 128 +# 2**7 (= 128) subnets DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +# MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS (= 128 * 128) sidecars MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +# 2**3 (= 8) samples SAMPLES_PER_SLOT: 8 +# 2**2 (= 4) sidecars CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +# 2**3 (= 8) sidecars +VALIDATOR_CUSTODY_REQUIREMENT: 8 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +# 2**12 (= 4,096) epochs MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + - EPOCH: 52480 + MAX_BLOBS_PER_BLOCK: 15 + - EPOCH: 54016 + MAX_BLOBS_PER_BLOCK: 21 + +# Gloas + # EIP7732 MAX_REQUEST_PAYLOADS: 128 diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 88a8773b3a..1f66d34646 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -6,7 +6,9 @@ PRESET_BASE: 'mainnet' # Free-form short name of the network that this configuration applies to - known # canonical network names include: # * 'mainnet' - there can be only one +# * 'sepolia' - testnet # * 'holesky' - testnet +# * 'hoodi' - testnet # Must match the regex: [a-z0-9\-] CONFIG_NAME: 'mainnet' @@ -50,25 +52,42 @@ DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC # Electra ELECTRA_FORK_VERSION: 0x05000000 ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC -# eip7805 -EIP7805_FORK_VERSION: 0x06000000 -EIP7805_FORK_EPOCH: 18446744073709551615 # Fulu -FULU_FORK_VERSION: 0x07000000 -FULU_FORK_EPOCH: 18446744073709551615 +FULU_FORK_VERSION: 0x06000000 +FULU_FORK_EPOCH: 411392 # December 3, 2025, 09:49:11pm UTC +# Eip7805 +EIP7805_FORK_VERSION: 0x07000000 +EIP7805_FORK_EPOCH: 18446744073709551615 +# Gloas +GLOAS_FORK_VERSION: 0x08000000 +GLOAS_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- -# 12 seconds +# 12 seconds (*deprecated*) SECONDS_PER_SLOT: 12 +# 12000 milliseconds +SLOT_DURATION_MS: 12000 # 14 (estimate from Eth1 mainnet) SECONDS_PER_ETH1_BLOCK: 14 -# 2**8 (= 256) epochs ~27 hours +# 2**8 (= 256) epochs MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 -# 2**8 (= 256) epochs ~27 hours +# 2**8 (= 256) epochs SHARD_COMMITTEE_PERIOD: 256 -# 2**11 (= 2,048) Eth1 blocks ~8 hours +# 2**11 (= 2,048) Eth1 blocks ETH1_FOLLOW_DISTANCE: 2048 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle # --------------------------------------------------------------- @@ -78,13 +97,21 @@ INACTIVITY_SCORE_BIAS: 4 INACTIVITY_SCORE_RECOVERY_RATE: 16 # 2**4 * 10**9 (= 16,000,000,000) Gwei EJECTION_BALANCE: 16000000000 -# 2**2 (= 4) +# 2**2 (= 4) validators MIN_PER_EPOCH_CHURN_LIMIT: 4 # 2**16 (= 65,536) CHURN_LIMIT_QUOTIENT: 65536 -# [New in Deneb:EIP7514] 2**3 (= 8) + +# Deneb +# 2**3 (= 8) (*deprecated*) MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 +# Electra +# 2**7 * 10**9 (= 128,000,000,000) Gwei +MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 +# 2**8 * 10**9 (= 256,000,000,000) Gwei +MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 + # Fork choice # --------------------------------------------------------------- # 40% @@ -93,7 +120,7 @@ PROPOSER_SCORE_BOOST: 40 REORG_HEAD_WEIGHT_THRESHOLD: 20 # 160% REORG_PARENT_WEIGHT_THRESHOLD: 160 -# `2` epochs +# 2 epochs REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 # Deposit contract @@ -105,18 +132,19 @@ DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa # Networking # --------------------------------------------------------------- -# `10 * 2**20` (= 10485760, 10 MiB) +# 10 * 2**20 (= 10,485,760) bytes, 10 MiB MAX_PAYLOAD_SIZE: 10485760 -# `2**10` (= 1024) +# 2**10 (= 1,024) blocks MAX_REQUEST_BLOCKS: 1024 -# `2**8` (= 256) +# 2**8 (= 256) epochs EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 -# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +# MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2 (= 33,024) epochs MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 # 5s TTFB_TIMEOUT: 5 # 10s RESP_TIMEOUT: 10 +# 2**5 (= 32) slots ATTESTATION_PROPAGATION_SLOT_RANGE: 32 # 500ms MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 @@ -124,50 +152,64 @@ MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 # 2 subnets per node SUBNETS_PER_NODE: 2 -# 2**8 (= 64) +# 2**6 (= 64) subnets ATTESTATION_SUBNET_COUNT: 64 +# 0 bits ATTESTATION_SUBNET_EXTRA_BITS: 0 -# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS (= 6 + 0) bits ATTESTATION_SUBNET_PREFIX_BITS: 6 ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # Deneb -# `2**7` (=128) +# 2**7 (= 128) blocks MAX_REQUEST_BLOCKS_DENEB: 128 -# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK -MAX_REQUEST_BLOB_SIDECARS: 768 -# `2**12` (= 4096 epochs, ~18 days) +# 2**12 (= 4,096) epochs MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 -# `6` +# 6 subnets BLOB_SIDECAR_SUBNET_COUNT: 6 -# `uint64(6)` +# 6 blobs MAX_BLOBS_PER_BLOCK: 6 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK (= 128 * 6) sidecars +MAX_REQUEST_BLOB_SIDECARS: 768 # Electra -# 2**7 * 10**9 (= 128,000,000,000) -MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000 -# 2**8 * 10**9 (= 256,000,000,000) -MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000 -# `9` +# 9 subnets BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9 -# `uint64(9)` +# 9 blobs MAX_BLOBS_PER_BLOCK_ELECTRA: 9 -# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA (= 128 * 9) sidecars MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 # Fulu -NUMBER_OF_COLUMNS: 128 +# 2**7 (= 128) groups NUMBER_OF_CUSTODY_GROUPS: 128 +# 2**7 (= 128) subnets DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +# MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS (= 128 * 128) sidecars +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +# 2**3 (= 8) samples SAMPLES_PER_SLOT: 8 +# 2**2 (= 4) sidecars CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +# 2**3 (= 8) sidecars +VALIDATOR_CUSTODY_REQUIREMENT: 8 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +# 2**12 (= 4,096) epochs +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + - EPOCH: 412672 # December 9, 2025, 02:21:11pm UTC + MAX_BLOBS_PER_BLOCK: 15 + - EPOCH: 419072 # January 7, 2026, 01:01:11am UTC + MAX_BLOBS_PER_BLOCK: 21 # EIP 7805 ATTESTATION_DEADLINE: 4 PROPOSER_INCLUSION_LIST_CUT_OFF: 11 VIEW_FREEZE_DEADLINE: 9 -# 2**4 (= 16) -MAX_REQUEST_INCLUSION_LIST: 16 -# 2**13 (= 8192) -MAX_BYTES_PER_INCLUSION_LIST: 8192 \ No newline at end of file + +# Gloas diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml index 10be107263..b1a01933d7 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -40,10 +40,20 @@ DENEB_FORK_EPOCH: 132608 ELECTRA_FORK_VERSION: 0x90000074 ELECTRA_FORK_EPOCH: 222464 +# Fulu +FULU_FORK_VERSION: 0x90000075 +FULU_FORK_EPOCH: 272640 + +# Gloas +GLOAS_FORK_VERSION: 0x90000076 +GLOAS_FORK_EPOCH: 18446744073709551615 + # Time parameters # --------------------------------------------------------------- # 12 seconds SECONDS_PER_SLOT: 12 +# 12000 milliseconds +SLOT_DURATION_MS: 12000 # 14 (estimate from Eth1 mainnet) SECONDS_PER_ETH1_BLOCK: 14 # 2**8 (= 256) epochs ~27 hours @@ -52,6 +62,18 @@ MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 +# 1667 basis points, ~17% of SLOT_DURATION_MS +PROPOSER_REORG_CUTOFF_BPS: 1667 +# 3333 basis points, ~33% of SLOT_DURATION_MS +ATTESTATION_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +AGGREGATE_DUE_BPS: 6667 + +# Altair +# 3333 basis points, ~33% of SLOT_DURATION_MS +SYNC_MESSAGE_DUE_BPS: 3333 +# 6667 basis points, ~67% of SLOT_DURATION_MS +CONTRIBUTION_DUE_BPS: 6667 # Validator cycle @@ -139,9 +161,31 @@ MAX_BLOBS_PER_BLOCK_ELECTRA: 9 MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 # Fulu -NUMBER_OF_COLUMNS: 128 +# 2**7 (= 128) groups NUMBER_OF_CUSTODY_GROUPS: 128 +# 2**7 (= 128) subnets DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +# MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS (= 128 * 128) sidecars +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 +# 2**3 (= 8) samples SAMPLES_PER_SLOT: 8 +# 2**2 (= 4) sidecars CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +# 2**3 (= 8) sidecars +VALIDATOR_CUSTODY_REQUIREMENT: 8 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +# 2**12 (= 4,096) epochs +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 + + +# Blob Scheduling +# --------------------------------------------------------------- + +BLOB_SCHEDULE: + - EPOCH: 274176 + MAX_BLOBS_PER_BLOCK: 15 + - EPOCH: 275712 + MAX_BLOBS_PER_BLOCK: 21 + +# Gloas \ No newline at end of file diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index ac488ed2a3..16ee45e524 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -13,13 +13,13 @@ use bytes::Bytes; use discv5::enr::{CombinedKey, Enr}; -use eth2_config::{instantiate_hardcoded_nets, HardcodedNet}; +use eth2_config::{HardcodedNet, instantiate_hardcoded_nets}; use kzg::trusted_setup::get_trusted_setup; use pretty_reqwest_error::PrettyReqwestError; use reqwest::{Client, Error}; use sensitive_url::SensitiveUrl; use sha2::{Digest, Sha256}; -use std::fs::{create_dir_all, File}; +use std::fs::{File, create_dir_all}; use std::io::{Read, Write}; use std::path::PathBuf; use std::str::FromStr; @@ -464,9 +464,10 @@ fn parse_state_download_url(url: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use fixed_bytes::FixedBytesExtended; use ssz::Encode; use tempfile::Builder as TempBuilder; - use types::{Eth1Data, FixedBytesExtended, GnosisEthSpec, MainnetEthSpec}; + use types::{Eth1Data, GnosisEthSpec, MainnetEthSpec}; type E = MainnetEthSpec; diff --git a/common/eth2_wallet_manager/src/filesystem.rs b/common/eth2_wallet_manager/src/filesystem.rs index 131b218c7c..26e725a8ae 100644 --- a/common/eth2_wallet_manager/src/filesystem.rs +++ b/common/eth2_wallet_manager/src/filesystem.rs @@ -2,7 +2,7 @@ use eth2_wallet::Error as WalletError; use eth2_wallet::{Uuid, Wallet}; -use std::fs::{copy as copy_file, remove_file, File}; +use std::fs::{File, copy as copy_file, remove_file}; use std::io; use std::path::{Path, PathBuf}; diff --git a/common/eth2_wallet_manager/src/locked_wallet.rs b/common/eth2_wallet_manager/src/locked_wallet.rs index 2af863a4bf..308fe3de90 100644 --- a/common/eth2_wallet_manager/src/locked_wallet.rs +++ b/common/eth2_wallet_manager/src/locked_wallet.rs @@ -1,6 +1,6 @@ use crate::{ - filesystem::{read, update}, Error, + filesystem::{read, update}, }; use eth2_wallet::{Uuid, ValidatorKeystores, Wallet}; use lockfile::Lockfile; diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index c988ca4135..dea94435ce 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -1,12 +1,12 @@ use crate::{ - filesystem::{create, Error as FilesystemError}, LockedWallet, + filesystem::{Error as FilesystemError, create}, }; -use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBuilder}; +use eth2_wallet::{Error as WalletError, Uuid, Wallet, WalletBuilder, bip39::Mnemonic}; use lockfile::LockfileError; use std::collections::HashMap; use std::ffi::OsString; -use std::fs::{create_dir_all, read_dir, File}; +use std::fs::{File, create_dir_all, read_dir}; use std::io; use std::path::{Path, PathBuf}; diff --git a/common/filesystem/src/lib.rs b/common/filesystem/src/lib.rs index d73b7a355b..9b96e545bd 100644 --- a/common/filesystem/src/lib.rs +++ b/common/filesystem/src/lib.rs @@ -95,7 +95,7 @@ pub fn restrict_file_permissions>(path: P) -> Result<(), Error> { #[cfg(windows)] { use winapi::um::winnt::PSID; - use windows_acl::acl::{AceType, ACL}; + use windows_acl::acl::{ACL, AceType}; use windows_acl::helper::sid_to_string; let path_str = path diff --git a/common/health_metrics/Cargo.toml b/common/health_metrics/Cargo.toml index 08591471b2..66063dc0fe 100644 --- a/common/health_metrics/Cargo.toml +++ b/common/health_metrics/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = { workspace = true } [dependencies] -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["lighthouse"] } metrics = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] +procfs = { version = "0.18", default-features = false } psutil = "3.3.0" -procfs = "0.15.1" diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index cb4a43e407..ab9509cb1e 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -1,13 +1,8 @@ [package] name = "lighthouse_version" -version = "0.1.0" +version = { workspace = true } authors = ["Sigma Prime "] edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -git-version = "0.3.4" -target_info = "0.1.0" [dev-dependencies] regex = { workspace = true } diff --git a/common/lighthouse_version/build.rs b/common/lighthouse_version/build.rs new file mode 100644 index 0000000000..1af99996df --- /dev/null +++ b/common/lighthouse_version/build.rs @@ -0,0 +1,81 @@ +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; + +const CLIENT_NAME: &str = "Lighthouse"; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let manifest_path = Path::new(&manifest_dir); + + // The crate version is inherited from the workspace. + let semantic_version = env::var("CARGO_PKG_VERSION").unwrap(); + + // Hardcode the .git/ path. + // This assumes the `lighthouse_version` crate will never move. + let git_dir = manifest_path.join("../../.git"); + + if git_dir.exists() { + // HEAD either contains a commit hash directly (detached HEAD), or a reference to a branch. + let head_path = git_dir.join("HEAD"); + if head_path.exists() { + println!("cargo:rerun-if-changed={}", head_path.display()); + + if let Ok(head_content) = fs::read_to_string(&head_path) { + let head_content = head_content.trim(); + + // If HEAD is a reference, also check that file. + if let Some(ref_path) = head_content.strip_prefix("ref: ") { + let full_ref_path = git_dir.join(ref_path); + if full_ref_path.exists() { + println!("cargo:rerun-if-changed={}", full_ref_path.display()); + } + } + } + } + } + + // Construct Lighthouse version string without commit hash. + let base_version = format!("{}/v{}", CLIENT_NAME, semantic_version); + + let commit_hash = get_git_hash(7); + let commit_prefix = get_git_hash(8); + + // If commit hash is valid, construct the full version string. + let version = if !commit_hash.is_empty() && commit_hash.len() >= 7 { + format!("{}-{}", base_version, commit_hash) + } else { + base_version + }; + + println!("cargo:rustc-env=GIT_VERSION={}", version); + println!("cargo:rustc-env=GIT_COMMIT_PREFIX={}", commit_prefix); + println!("cargo:rustc-env=CLIENT_NAME={}", CLIENT_NAME); + println!("cargo:rustc-env=SEMANTIC_VERSION={}", semantic_version); +} + +fn get_git_hash(len: usize) -> String { + Command::new("git") + .args(["rev-parse", &format!("--short={}", len), "HEAD"]) + .output() + .ok() + .and_then(|output| { + if output.status.success() { + String::from_utf8(output.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| { + // Fallback commit prefix for execution engine reporting. + if len == 8 { + "00000000".to_string() + } else { + String::new() + } + }) +} diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index b20708e7b0..1466487520 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -1,67 +1,43 @@ -use git_version::git_version; -use target_info::Target; +use std::env::consts; /// Returns the current version of this build of Lighthouse. /// -/// A plus-sign (`+`) is appended to the git commit if the tree is dirty. /// Commit hash is omitted if the sources don't include git information. /// /// ## Example /// -/// `Lighthouse/v1.5.1-67da032+` -pub const VERSION: &str = git_version!( - args = [ - "--always", - "--dirty=+", - "--abbrev=7", - // NOTE: using --match instead of --exclude for compatibility with old Git - "--match=thiswillnevermatchlol" - ], - prefix = "Lighthouse/v7.1.0-beta.0-", - fallback = "Lighthouse/v7.1.0-beta.0" -); +/// `Lighthouse/v8.0.0-67da032` +pub const VERSION: &str = env!("GIT_VERSION"); /// Returns the first eight characters of the latest commit hash for this build. /// /// No indication is given if the tree is dirty. This is part of the standard /// for reporting the client version to the execution engine. -pub const COMMIT_PREFIX: &str = git_version!( - args = [ - "--always", - "--abbrev=8", - // NOTE: using --match instead of --exclude for compatibility with old Git - "--match=thiswillnevermatchlol" - ], - prefix = "", - suffix = "", - cargo_prefix = "", - cargo_suffix = "", - fallback = "00000000" -); +pub const COMMIT_PREFIX: &str = env!("GIT_COMMIT_PREFIX"); /// Returns `VERSION`, but with platform information appended to the end. /// /// ## Example /// -/// `Lighthouse/v1.5.1-67da032+/x86_64-linux` +/// `Lighthouse/v8.0.0-67da032/x86_64-linux` pub fn version_with_platform() -> String { - format!("{}/{}-{}", VERSION, Target::arch(), Target::os()) + format!("{}/{}-{}", VERSION, consts::ARCH, consts::OS) } /// Returns semantic versioning information only. /// /// ## Example /// -/// `1.5.1` +/// `8.0.0` pub fn version() -> &'static str { - "7.1.0-beta.0" + env!("SEMANTIC_VERSION") } /// Returns the name of the current client running. /// /// This will usually be "Lighthouse" pub fn client_name() -> &'static str { - "Lighthouse" + env!("CLIENT_NAME") } #[cfg(test)] @@ -72,7 +48,7 @@ mod test { #[test] fn version_formatting() { let re = Regex::new( - r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta).[0-9])?(-[[:xdigit:]]{7})?\+?$", + r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)\.[0-9])?(-[[:xdigit:]]{7})?$", ) .unwrap(); assert!( @@ -91,4 +67,14 @@ mod test { version() ); } + + #[test] + fn client_name_is_lighthouse() { + assert_eq!(client_name(), "Lighthouse"); + } + + #[test] + fn version_contains_semantic_version() { + assert!(VERSION.contains(version())); + } } diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index 5c4de1fd61..8ef3436b06 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -1,5 +1,3 @@ -use metrics::{try_create_int_counter, IntCounter, Result as MetricsResult}; -use std::sync::LazyLock; use std::time::{Duration, Instant}; use tracing_subscriber::EnvFilter; @@ -14,7 +12,7 @@ mod utils; pub use sse_logging_components::SSELoggingComponents; pub use tracing_libp2p_discv5_logging_layer::{ - create_libp2p_discv5_tracing_layer, Libp2pDiscv5TracingLayer, + Libp2pDiscv5TracingLayer, create_libp2p_discv5_tracing_layer, }; pub use tracing_logging_layer::LoggingLayer; pub use tracing_metrics_layer::MetricsLayer; @@ -23,15 +21,6 @@ pub use utils::build_workspace_filter; /// The minimum interval between log messages indicating that a queue is full. const LOG_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); -pub static INFOS_TOTAL: LazyLock> = - LazyLock::new(|| try_create_int_counter("info_total", "Count of infos logged")); -pub static WARNS_TOTAL: LazyLock> = - LazyLock::new(|| try_create_int_counter("warn_total", "Count of warns logged")); -pub static ERRORS_TOTAL: LazyLock> = - LazyLock::new(|| try_create_int_counter("error_total", "Count of errors logged")); -pub static CRITS_TOTAL: LazyLock> = - LazyLock::new(|| try_create_int_counter("crit_total", "Count of crits logged")); - /// Provides de-bounce functionality for logging. #[derive(Default)] pub struct TimeLatch(Option); diff --git a/common/logging/src/sse_logging_components.rs b/common/logging/src/sse_logging_components.rs index d526f2b040..66567704f8 100644 --- a/common/logging/src/sse_logging_components.rs +++ b/common/logging/src/sse_logging_components.rs @@ -1,8 +1,8 @@ //! This module provides an implementation of `tracing_subscriber::layer::Layer` that optionally writes to a channel if //! there are subscribers to a HTTP SSE stream. -use serde_json::json; use serde_json::Value; +use serde_json::json; use std::sync::Arc; use tokio::sync::broadcast::Sender; use tracing::field::{Field, Visit}; @@ -45,13 +45,12 @@ impl Layer for SSELoggingComponents { .get("fields") .and_then(|fields| fields.get("error_type")) .and_then(|val| val.as_str()) + && error_type.eq_ignore_ascii_case("crit") { - if error_type.eq_ignore_ascii_case("crit") { - log_entry["level"] = json!("CRIT"); + log_entry["level"] = json!("CRIT"); - if let Some(Value::Object(ref mut map)) = log_entry.get_mut("fields") { - map.remove("error_type"); - } + if let Some(Value::Object(map)) = log_entry.get_mut("fields") { + map.remove("error_type"); } } @@ -73,9 +72,11 @@ impl TracingEventVisitor { let mut log_entry = serde_json::Map::new(); log_entry.insert( "time".to_string(), - json!(chrono::Local::now() - .format("%b %d %H:%M:%S%.3f") - .to_string()), + json!( + chrono::Local::now() + .format("%b %d %H:%M:%S%.3f") + .to_string() + ), ); log_entry.insert("level".to_string(), json!(metadata.level().to_string())); log_entry.insert("target".to_string(), json!(metadata.target())); diff --git a/common/logging/src/tracing_libp2p_discv5_logging_layer.rs b/common/logging/src/tracing_libp2p_discv5_logging_layer.rs index 90033d11ad..1c34209e49 100644 --- a/common/logging/src/tracing_libp2p_discv5_logging_layer.rs +++ b/common/logging/src/tracing_libp2p_discv5_logging_layer.rs @@ -4,7 +4,7 @@ use std::io::Write; use std::path::PathBuf; use tracing::Subscriber; use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; -use tracing_subscriber::{layer::Context, Layer}; +use tracing_subscriber::{Layer, layer::Context}; pub struct Libp2pDiscv5TracingLayer { pub libp2p_non_blocking_writer: NonBlocking, @@ -59,28 +59,31 @@ impl tracing_core::field::Visit for LogMessageExtractor { pub fn create_libp2p_discv5_tracing_layer( base_tracing_log_path: Option, max_log_size: u64, + file_mode: u32, ) -> Option { if let Some(mut tracing_log_path) = base_tracing_log_path { // Ensure that `tracing_log_path` only contains directories. for p in tracing_log_path.clone().iter() { tracing_log_path = tracing_log_path.join(p); - if let Ok(metadata) = tracing_log_path.metadata() { - if !metadata.is_dir() { - tracing_log_path.pop(); - break; - } + if let Ok(metadata) = tracing_log_path.metadata() + && !metadata.is_dir() + { + tracing_log_path.pop(); + break; } } let libp2p_writer = LogRollerBuilder::new(tracing_log_path.clone(), PathBuf::from("libp2p.log")) .rotation(Rotation::SizeBased(RotationSize::MB(max_log_size))) - .max_keep_files(1); + .max_keep_files(1) + .file_mode(file_mode); let discv5_writer = LogRollerBuilder::new(tracing_log_path.clone(), PathBuf::from("discv5.log")) .rotation(Rotation::SizeBased(RotationSize::MB(max_log_size))) - .max_keep_files(1); + .max_keep_files(1) + .file_mode(file_mode); let libp2p_writer = match libp2p_writer.build() { Ok(writer) => writer, diff --git a/common/logging/src/tracing_logging_layer.rs b/common/logging/src/tracing_logging_layer.rs index c3784a8f62..e631d272b7 100644 --- a/common/logging/src/tracing_logging_layer.rs +++ b/common/logging/src/tracing_logging_layer.rs @@ -1,17 +1,16 @@ use crate::utils::is_ascii_control; +use std::collections::HashSet; use chrono::prelude::*; use serde_json::{Map, Value}; -use std::collections::HashMap; use std::io::Write; -use std::sync::{Arc, Mutex}; +use tracing::Subscriber; use tracing::field::Field; use tracing::span::Id; -use tracing::Subscriber; use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; +use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::Layer; const FIXED_MESSAGE_WIDTH: usize = 44; const ALIGNED_LEVEL_WIDTH: usize = 5; @@ -23,7 +22,6 @@ pub struct LoggingLayer { pub log_color: bool, pub log_format: Option, pub extra_info: bool, - span_fields: Arc>>, } impl LoggingLayer { @@ -43,7 +41,6 @@ impl LoggingLayer { log_color, log_format, extra_info, - span_fields: Arc::new(Mutex::new(HashMap::new())), } } } @@ -52,23 +49,20 @@ impl Layer for LoggingLayer where S: Subscriber + for<'a> LookupSpan<'a>, { - fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, _ctx: Context) { - let metadata = attrs.metadata(); - let span_name = metadata.name(); - - let mut visitor = SpanFieldsExtractor::default(); + fn on_new_span(&self, attrs: &tracing::span::Attributes<'_>, id: &Id, ctx: Context) { + let mut visitor = FieldVisitor::new(); attrs.record(&mut visitor); - let span_data = SpanData { - name: span_name.to_string(), - fields: visitor.fields, - }; + if let Some(span) = ctx.span(id) { + let mut extensions = span.extensions_mut(); - let mut span_fields = match self.span_fields.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - }; - span_fields.insert(id.clone(), span_data); + let span_data = SpanData { + name: attrs.metadata().name().to_string(), + fields: visitor.fields, + }; + + extensions.replace(span_data); + } } fn on_event(&self, event: &tracing::Event<'_>, ctx: Context) { @@ -82,13 +76,18 @@ where let mut writer = self.non_blocking_writer.clone(); - let mut visitor = LogMessageExtractor { - message: String::new(), - fields: Vec::new(), - is_crit: false, - }; + let mut visitor = FieldVisitor::new(); + event.record(&mut visitor); + let mut span_data = Vec::new(); + if let Some(mut scope) = ctx.event_scope(event) + && let Some(span) = scope.next() + && let Some(data) = span.extensions().get::() + { + span_data.extend(data.fields.clone()); + } + // Remove ascii control codes from message. // All following formatting and logs components are predetermined or known. if visitor.message.as_bytes().iter().any(u8::is_ascii_control) { @@ -145,23 +144,13 @@ where }; if self.log_format.as_deref() == Some("JSON") { - build_log_json( - &visitor, - plain_level_str, - meta, - &ctx, - &self.span_fields, - event, - &mut writer, - ); + build_log_json(&visitor, plain_level_str, meta, &span_data, &mut writer); } else { build_log_text( &visitor, plain_level_str, ×tamp, - &ctx, - &self.span_fields, - event, + &span_data, &location, color_level_str, self.log_color, @@ -171,79 +160,65 @@ where } } -struct SpanData { - name: String, - fields: Vec<(String, String)>, +#[derive(Clone, Debug)] +pub struct SpanData { + pub name: String, + pub fields: Vec<(String, String)>, } -#[derive(Default)] -struct SpanFieldsExtractor { - fields: Vec<(String, String)>, -} - -impl tracing_core::field::Visit for SpanFieldsExtractor { - fn record_str(&mut self, field: &Field, value: &str) { - self.fields - .push((field.name().to_string(), format!("\"{}\"", value))); - } - - fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { - self.fields - .push((field.name().to_string(), format!("{:?}", value))); - } - - fn record_i64(&mut self, field: &Field, value: i64) { - self.fields - .push((field.name().to_string(), value.to_string())); - } - - fn record_u64(&mut self, field: &Field, value: u64) { - self.fields - .push((field.name().to_string(), value.to_string())); - } - - fn record_bool(&mut self, field: &Field, value: bool) { - self.fields - .push((field.name().to_string(), value.to_string())); - } -} - -struct LogMessageExtractor { +struct FieldVisitor { message: String, fields: Vec<(String, String)>, is_crit: bool, } -impl tracing_core::field::Visit for LogMessageExtractor { +impl FieldVisitor { + fn new() -> Self { + FieldVisitor { + message: String::new(), + fields: Vec::new(), + is_crit: false, + } + } +} + +impl tracing_core::field::Visit for FieldVisitor { fn record_str(&mut self, field: &Field, value: &str) { - if field.name() == "message" { - if self.message.is_empty() { - self.message = value.to_string(); - } else { - self.fields - .push(("msg_id".to_string(), format!("\"{}\"", value))); + match field.name() { + "message" => { + if self.message.is_empty() { + self.message = value.to_string(); + } else { + self.fields + .push(("msg_id".to_string(), format!("\"{}\"", value))); + } + } + "error_type" if value == "crit" => { + self.is_crit = true; + } + _ => { + self.fields + .push((field.name().to_string(), format!("\"{}\"", value))); } - } else if field.name() == "error_type" && value == "crit" { - self.is_crit = true; - } else { - self.fields - .push((field.name().to_string(), format!("\"{}\"", value))); } } fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { - if field.name() == "message" { - if self.message.is_empty() { - self.message = format!("{:?}", value); - } else { - self.fields - .push(("msg_id".to_string(), format!("{:?}", value))); + let string_value = format!("{:?}", value); + match field.name() { + "message" => { + if self.message.is_empty() { + self.message = string_value; + } else { + self.fields.push(("msg_id".to_string(), string_value)); + } + } + "error_type" if string_value == "\"crit\"" => { + self.is_crit = true; + } + _ => { + self.fields.push((field.name().to_string(), string_value)); } - } else if field.name() == "error_type" && format!("{:?}", value) == "\"crit\"" { - self.is_crit = true; - } else { - self.fields - .push((field.name().to_string(), format!("{:?}", value))); } } @@ -263,17 +238,13 @@ impl tracing_core::field::Visit for LogMessageExtractor { } } -fn build_log_json<'a, S>( - visitor: &LogMessageExtractor, +fn build_log_json( + visitor: &FieldVisitor, plain_level_str: &str, meta: &tracing::Metadata<'_>, - ctx: &Context<'_, S>, - span_fields: &Arc>>, - event: &tracing::Event<'_>, + span_fields: &[(String, String)], writer: &mut impl Write, -) where - S: Subscriber + for<'lookup> LookupSpan<'lookup>, -{ +) { let utc_timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true); let mut log_map = Map::new(); @@ -291,6 +262,12 @@ fn build_log_json<'a, S>( let module_field = format!("{}:{}", module_path, line_number); log_map.insert("module".to_string(), Value::String(module_field)); + // Avoid adding duplicate fields; prefer event fields when duplicates exist. + for (key, val) in span_fields { + let parsed_span_val = parse_field(val); + log_map.insert(key.clone(), parsed_span_val); + } + for (key, val) in visitor.fields.clone().into_iter() { let cleaned_value = if val.starts_with('\"') && val.ends_with('\"') && val.len() >= 2 { &val[1..val.len() - 1] @@ -302,21 +279,6 @@ fn build_log_json<'a, S>( log_map.insert(key, parsed_val); } - if let Some(scope) = ctx.event_scope(event) { - let guard = span_fields.lock().ok(); - if let Some(span_map) = guard { - for span in scope { - let id = span.id(); - if let Some(span_data) = span_map.get(&id) { - for (key, val) in &span_data.fields { - let parsed_span_val = parse_field(val); - log_map.insert(key.clone(), parsed_span_val); - } - } - } - } - } - let json_obj = Value::Object(log_map); let output = format!("{}\n", json_obj); @@ -326,50 +288,18 @@ fn build_log_json<'a, S>( } #[allow(clippy::too_many_arguments)] -fn build_log_text<'a, S>( - visitor: &LogMessageExtractor, +fn build_log_text( + visitor: &FieldVisitor, plain_level_str: &str, timestamp: &str, - ctx: &Context<'_, S>, - span_fields: &Arc>>, - event: &tracing::Event<'_>, + span_fields: &[(String, String)], location: &str, color_level_str: &str, use_color: bool, writer: &mut impl Write, -) where - S: Subscriber + for<'lookup> LookupSpan<'lookup>, -{ +) { let bold_start = "\x1b[1m"; let bold_end = "\x1b[0m"; - let mut collected_span_fields = Vec::new(); - - if let Some(scope) = ctx.event_scope(event) { - for span in scope { - let id = span.id(); - let span_fields_map = span_fields.lock().unwrap(); - if let Some(span_data) = span_fields_map.get(&id) { - collected_span_fields.push((span_data.name.clone(), span_data.fields.clone())); - } - } - } - - let mut formatted_spans = String::new(); - for (_, fields) in collected_span_fields.iter().rev() { - for (i, (field_name, field_value)) in fields.iter().enumerate() { - if i > 0 && !visitor.fields.is_empty() { - formatted_spans.push_str(", "); - } - if use_color { - formatted_spans.push_str(&format!( - "{}{}{}: {}", - bold_start, field_name, bold_end, field_value - )); - } else { - formatted_spans.push_str(&format!("{}: {}", field_name, field_value)); - } - } - } let pad = if plain_level_str.len() < ALIGNED_LEVEL_WIDTH { " " @@ -406,23 +336,26 @@ fn build_log_text<'a, S>( message_content.clone() }; - let mut formatted_fields = String::new(); - for (i, (field_name, field_value)) in visitor.fields.iter().enumerate() { - if i > 0 { - formatted_fields.push_str(", "); - } - if use_color { - formatted_fields.push_str(&format!( - "{}{}{}: {}", - bold_start, field_name, bold_end, field_value - )); - } else { - formatted_fields.push_str(&format!("{}: {}", field_name, field_value)); - } - if i == visitor.fields.len() - 1 && !collected_span_fields.is_empty() { - formatted_fields.push(','); - } - } + // Avoid adding duplicate fields; prefer event fields when duplicates exist. + let mut added_field_names = HashSet::new(); + let formatted_fields = visitor + .fields + .iter() + .chain(span_fields.iter()) + .filter_map(|(field_name, field_value)| { + if added_field_names.insert(field_name) { + let formatted_field = if use_color { + format!("{}{}{}: {}", bold_start, field_name, bold_end, field_value) + } else { + format!("{}: {}", field_name, field_value) + }; + Some(formatted_field) + } else { + None + } + }) + .collect::>() + .join(", "); let full_message = if !formatted_fields.is_empty() { format!("{} {}", padded_message, formatted_fields) @@ -432,14 +365,11 @@ fn build_log_text<'a, S>( let message = if !location.is_empty() { format!( - "{} {} {} {} {}\n", - timestamp, level_str, location, full_message, formatted_spans + "{} {} {} {}\n", + timestamp, level_str, location, full_message ) } else { - format!( - "{} {} {} {}\n", - timestamp, level_str, full_message, formatted_spans - ) + format!("{} {} {}\n", timestamp, level_str, full_message) }; if let Err(e) = writer.write_all(message.as_bytes()) { @@ -455,3 +385,168 @@ fn parse_field(val: &str) -> Value { }; serde_json::from_str(cleaned).unwrap_or(Value::String(cleaned.to_string())) } + +#[cfg(test)] +mod tests { + use crate::tracing_logging_layer::{FieldVisitor, build_log_text}; + use std::io::Write; + + struct Buffer { + data: Vec, + } + + impl Buffer { + fn new() -> Self { + Buffer { data: Vec::new() } + } + + fn into_string(self) -> String { + String::from_utf8(self.data).unwrap() + } + } + + impl Write for Buffer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.data.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + #[test] + fn test_build_log_text_single_log_field() { + let log_fields = vec![("field_name".to_string(), "field_value".to_string())]; + let span_fields = vec![]; + let expected = "Jan 1 08:00:00.000 INFO test message field_name: field_value\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_multiple_log_fields() { + let log_fields = vec![ + ("field_name1".to_string(), "field_value1".to_string()), + ("field_name2".to_string(), "field_value2".to_string()), + ]; + let span_fields = vec![]; + let expected = "Jan 1 08:00:00.000 INFO test message field_name1: field_value1, field_name2: field_value2\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_log_field_and_span() { + let log_fields = vec![("field_name".to_string(), "field_value".to_string())]; + let span_fields = vec![( + "span_field_name".to_string(), + "span_field_value".to_string(), + )]; + let expected = "Jan 1 08:00:00.000 INFO test message field_name: field_value, span_field_name: span_field_value\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_single_span() { + let log_fields = vec![]; + let span_fields = vec![( + "span_field_name".to_string(), + "span_field_value".to_string(), + )]; + let expected = "Jan 1 08:00:00.000 INFO test message span_field_name: span_field_value\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_multiple_spans() { + let log_fields = vec![]; + let span_fields = vec![ + ( + "span_field_name1".to_string(), + "span_field_value1".to_string(), + ), + ( + "span_field_name2".to_string(), + "span_field_value2".to_string(), + ), + ]; + let expected = "Jan 1 08:00:00.000 INFO test message span_field_name1: span_field_value1, span_field_name2: span_field_value2\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_multiple_span_fields() { + let log_fields = vec![]; + let span_fields = vec![ + ( + "span_field_name1-1".to_string(), + "span_field_value1-1".to_string(), + ), + ( + "span_field_name1-2".to_string(), + "span_field_value1-2".to_string(), + ), + ]; + let expected = "Jan 1 08:00:00.000 INFO test message span_field_name1-1: span_field_value1-1, span_field_name1-2: span_field_value1-2\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_no_duplicate_log_span_fields() { + let log_fields = vec![ + ("field_name_1".to_string(), "field_value_1".to_string()), + ("field_name_2".to_string(), "field_value_2".to_string()), + ]; + let span_fields = vec![ + ("field_name_1".to_string(), "field_value_1".to_string()), + ("field_name_3".to_string(), "field_value_3".to_string()), + ]; + let expected = "Jan 1 08:00:00.000 INFO test message field_name_1: field_value_1, field_name_2: field_value_2, field_name_3: field_value_3\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + #[test] + fn test_build_log_text_duplicate_fields_prefer_log_fields() { + let log_fields = vec![ + ("field_name_1".to_string(), "field_value_1_log".to_string()), + ("field_name_2".to_string(), "field_value_2".to_string()), + ]; + let span_fields = vec![ + ("field_name_1".to_string(), "field_value_1_span".to_string()), + ("field_name_3".to_string(), "field_value_3".to_string()), + ]; + let expected = "Jan 1 08:00:00.000 INFO test message field_name_1: field_value_1_log, field_name_2: field_value_2, field_name_3: field_value_3\n"; + test_build_log_text(log_fields, span_fields, expected); + } + + fn test_build_log_text( + log_fields: Vec<(String, String)>, + span_fields: Vec<(String, String)>, + expected: &str, + ) { + let visitor = FieldVisitor { + message: "test message".to_string(), + fields: log_fields, + is_crit: false, + }; + let plain_level_str = "INFO"; + let timestamp = "Jan 1 08:00:00.000"; + let location = ""; + let color_level_str = "\x1b[32mINFO\x1b[0m"; + let use_color = false; + let mut writer = Buffer::new(); + + build_log_text( + &visitor, + plain_level_str, + timestamp, + &span_fields, + location, + color_level_str, + use_color, + &mut writer, + ); + + assert_eq!(expected, &writer.into_string()); + } +} diff --git a/common/logging/src/utils.rs b/common/logging/src/utils.rs index 784cd5ca70..64bacf9086 100644 --- a/common/logging/src/utils.rs +++ b/common/logging/src/utils.rs @@ -5,8 +5,8 @@ use workspace_members::workspace_crates; const WORKSPACE_CRATES: &[&str] = workspace_crates!(); /// Constructs a filter which only permits logging from crates which are members of the workspace. -pub fn build_workspace_filter( -) -> Result bool + Clone>, String> { +pub fn build_workspace_filter() +-> Result bool + Clone>, String> { let workspace_crates: HashSet<&str> = WORKSPACE_CRATES.iter().copied().collect(); Ok(tracing_subscriber::filter::FilterFn::new(move |metadata| { diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 64fb7b9aad..1052128852 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -4,23 +4,38 @@ version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } +# Features are not rich enough to express the complexity of our defaults, so we choose to just +# use the jemalloc feature to control whether the dependency is compiled, but avoid using it if +# the `sysmalloc` feature is set. +# +# On Windows, setting the jemalloc feature will result in a compile-time error. +[features] +default = [] +mallinfo2 = [] +# The jemalloc feature enables the compilation of jemalloc dependencies. Jemalloc is also the +# default allocator, unless `sysmalloc` is set. +# +# It should be turned off on Windows. +jemalloc = ["tikv-jemalloc-ctl", "tikv-jemallocator"] +jemalloc-profiling = ["tikv-jemallocator/profiling"] +# Force the use of system malloc (or glibc) rather than jemalloc. +# This is a no-op on Windows where jemalloc is always disabled. +sysmalloc = [] +# Enable jemalloc with unprefixed malloc (recommended for reproducible builds) +jemalloc-unprefixed = ["jemalloc", "tikv-jemallocator/unprefixed_malloc_on_supported_platforms"] + [dependencies] libc = "0.2.79" metrics = { workspace = true } parking_lot = { workspace = true } tikv-jemalloc-ctl = { version = "0.6.0", optional = true, features = ["stats"] } +[target.'cfg(not(target_os = "linux"))'.dependencies] +tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats"] } + # Jemalloc's background_threads feature requires Linux (pthreads). [target.'cfg(target_os = "linux")'.dependencies] tikv-jemallocator = { version = "0.6.0", optional = true, features = [ "stats", "background_threads", ] } - -[target.'cfg(not(target_os = "linux"))'.dependencies] -tikv-jemallocator = { version = "0.6.0", optional = true, features = ["stats"] } - -[features] -mallinfo2 = [] -jemalloc = ["tikv-jemallocator", "tikv-jemalloc-ctl"] -jemalloc-profiling = ["tikv-jemallocator/profiling"] diff --git a/common/malloc_utils/src/glibc.rs b/common/malloc_utils/src/glibc.rs index 30313d0672..87529580ef 100644 --- a/common/malloc_utils/src/glibc.rs +++ b/common/malloc_utils/src/glibc.rs @@ -33,7 +33,7 @@ const M_MMAP_THRESHOLD: c_int = -3; /// https://man7.org/linux/man-pages/man3/mallopt.3.html const ENV_VAR_MMAP_THRESHOLD: &str = "MALLOC_MMAP_THRESHOLD_"; -pub static GLOBAL_LOCK: LazyLock> = LazyLock::new(|| <_>::default()); +pub static GLOBAL_LOCK: LazyLock> = LazyLock::new(Default::default); // Metrics for the malloc. For more information, see: // @@ -173,11 +173,7 @@ fn mallinfo() -> libc::mallinfo2 { } fn into_result(result: c_int) -> Result<(), c_int> { - if result == 1 { - Ok(()) - } else { - Err(result) - } + if result == 1 { Ok(()) } else { Err(result) } } #[cfg(test)] diff --git a/common/malloc_utils/src/jemalloc.rs b/common/malloc_utils/src/jemalloc.rs index 2e90c0ddf3..6ee3e74da4 100644 --- a/common/malloc_utils/src/jemalloc.rs +++ b/common/malloc_utils/src/jemalloc.rs @@ -8,10 +8,10 @@ //! A) `JEMALLOC_SYS_WITH_MALLOC_CONF` at compile-time. //! B) `_RJEM_MALLOC_CONF` at runtime. use metrics::{ - set_gauge, set_gauge_vec, try_create_int_gauge, try_create_int_gauge_vec, IntGauge, IntGaugeVec, + IntGauge, IntGaugeVec, set_gauge, set_gauge_vec, try_create_int_gauge, try_create_int_gauge_vec, }; use std::sync::LazyLock; -use tikv_jemalloc_ctl::{arenas, epoch, raw, stats, Access, AsName, Error}; +use tikv_jemalloc_ctl::{Access, AsName, Error, arenas, epoch, raw, stats}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -114,8 +114,10 @@ pub fn scrape_jemalloc_metrics_fallible() -> Result<(), Error> { } unsafe fn set_stats_gauge(metric: &metrics::Result, arena: u32, stat: &str) { - if let Ok(val) = raw::read::(stat.as_bytes()) { - set_gauge_vec(metric, &[&format!("arena_{arena}")], val as i64); + unsafe { + if let Ok(val) = raw::read::(stat.as_bytes()) { + set_gauge_vec(metric, &[&format!("arena_{arena}")], val as i64); + } } } diff --git a/common/malloc_utils/src/lib.rs b/common/malloc_utils/src/lib.rs index 50d2785a74..9a5ea3c2ba 100644 --- a/common/malloc_utils/src/lib.rs +++ b/common/malloc_utils/src/lib.rs @@ -25,28 +25,35 @@ //! functions, then try to compile with the `not_glibc_interface` module. #[cfg(all( + any(feature = "sysmalloc", not(feature = "jemalloc")), target_os = "linux", - not(target_env = "musl"), - not(feature = "jemalloc") + not(target_env = "musl") ))] pub mod glibc; -#[cfg(feature = "jemalloc")] +#[cfg(all(unix, not(feature = "sysmalloc"), feature = "jemalloc"))] pub mod jemalloc; pub use interface::*; +// Glibc malloc is the default on non-musl Linux if the sysmalloc feature is enabled, or jemalloc +// is disabled. #[cfg(all( + any(feature = "sysmalloc", not(feature = "jemalloc")), target_os = "linux", - not(target_env = "musl"), - not(feature = "jemalloc") + not(target_env = "musl") ))] mod interface { pub use crate::glibc::configure_glibc_malloc as configure_memory_allocator; pub use crate::glibc::scrape_mallinfo_metrics as scrape_allocator_metrics; + + pub fn allocator_name() -> String { + "glibc".to_string() + } } -#[cfg(feature = "jemalloc")] +// Jemalloc is the default on UNIX (including musl) unless the sysmalloc feature is enabled. +#[cfg(all(unix, not(feature = "sysmalloc"), feature = "jemalloc"))] mod interface { #[allow(dead_code)] pub fn configure_memory_allocator() -> Result<(), String> { @@ -54,11 +61,21 @@ mod interface { } pub use crate::jemalloc::scrape_jemalloc_metrics as scrape_allocator_metrics; + + pub fn allocator_name() -> String { + match crate::jemalloc::page_size() { + Ok(page_size) => format!("jemalloc ({}K)", page_size / 1024), + Err(e) => format!("jemalloc (error: {e:?})"), + } + } } -#[cfg(all( - any(not(target_os = "linux"), target_env = "musl"), - not(feature = "jemalloc") +#[cfg(any( + not(unix), + all( + any(feature = "sysmalloc", not(feature = "jemalloc")), + any(not(target_os = "linux"), target_env = "musl") + ) ))] mod interface { #[allow(dead_code, clippy::unnecessary_wraps)] @@ -68,4 +85,8 @@ mod interface { #[allow(dead_code)] pub fn scrape_allocator_metrics() {} + + pub fn allocator_name() -> String { + "system".to_string() + } } diff --git a/common/metrics/src/lib.rs b/common/metrics/src/lib.rs index 22513af8bc..de64fbb2c9 100644 --- a/common/metrics/src/lib.rs +++ b/common/metrics/src/lib.rs @@ -55,10 +55,9 @@ use std::time::Duration; use prometheus::core::{Atomic, GenericGauge, GenericGaugeVec}; pub use prometheus::{ - exponential_buckets, linear_buckets, + DEFAULT_BUCKETS, Encoder, Gauge, GaugeVec, Histogram, HistogramTimer, HistogramVec, IntCounter, + IntCounterVec, IntGauge, IntGaugeVec, Result, TextEncoder, exponential_buckets, linear_buckets, proto::{Metric, MetricFamily, MetricType}, - Encoder, Gauge, GaugeVec, Histogram, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, - IntGauge, IntGaugeVec, Result, TextEncoder, DEFAULT_BUCKETS, }; /// Collect all the metrics for reporting. @@ -423,10 +422,10 @@ pub trait TryExt { impl TryExt for std::result::Result { fn discard_timer_on_break(self, timer_opt: &mut Option) -> Self { - if self.is_err() { - if let Some(timer) = timer_opt.take() { - timer.stop_and_discard(); - } + if self.is_err() + && let Some(timer) = timer_opt.take() + { + timer.stop_and_discard(); } self } @@ -434,10 +433,10 @@ impl TryExt for std::result::Result { impl TryExt for Option { fn discard_timer_on_break(self, timer_opt: &mut Option) -> Self { - if self.is_none() { - if let Some(timer) = timer_opt.take() { - timer.stop_and_discard(); - } + if self.is_none() + && let Some(timer) = timer_opt.take() + { + timer.stop_and_discard(); } self } diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 9e2c36e2c7..e00b1f027b 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["lighthouse"] } health_metrics = { workspace = true } lighthouse_version = { workspace = true } metrics = { workspace = true } diff --git a/common/monitoring_api/src/lib.rs b/common/monitoring_api/src/lib.rs index 966a1a3054..03b93f2faa 100644 --- a/common/monitoring_api/src/lib.rs +++ b/common/monitoring_api/src/lib.rs @@ -10,7 +10,7 @@ pub use reqwest::{StatusCode, Url}; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use task_executor::TaskExecutor; -use tokio::time::{interval_at, Instant}; +use tokio::time::{Instant, interval_at}; use tracing::{debug, error, info}; use types::*; @@ -195,7 +195,7 @@ impl MonitoringHttpClient { endpoint = %self.monitoring_endpoint, "Sending metrics to remote endpoint" ); - self.post(self.monitoring_endpoint.full.clone(), &metrics) + self.post(self.monitoring_endpoint.expose_full().clone(), &metrics) .await } } diff --git a/common/network_utils/Cargo.toml b/common/network_utils/Cargo.toml new file mode 100644 index 0000000000..5206249e6f --- /dev/null +++ b/common/network_utils/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "network_utils" +version = "0.1.0" +edition = { workspace = true } + +[dependencies] +discv5 = { workspace = true } +libp2p-identity = "0.2" +lru_cache = { workspace = true } +metrics = { workspace = true } +multiaddr = "0.18.2" +parking_lot = { workspace = true } +serde = { workspace = true } +tiny-keccak = { version = "2", features = ["keccak"] } + +[dev-dependencies] +hex = { workspace = true } diff --git a/common/network_utils/src/discovery_metrics.rs b/common/network_utils/src/discovery_metrics.rs new file mode 100644 index 0000000000..26a9e8a45f --- /dev/null +++ b/common/network_utils/src/discovery_metrics.rs @@ -0,0 +1,45 @@ +use metrics::*; +use std::sync::LazyLock; + +pub static NAT_OPEN: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "nat_open", + "An estimate indicating if the local node is reachable from external nodes", + &["protocol"], + ) +}); +pub static DISCOVERY_BYTES: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( + "discovery_bytes", + "The number of bytes sent and received in discovery", + &["direction"], + ) +}); +pub static DISCOVERY_QUEUE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "discovery_queue_size", + "The number of discovery queries awaiting execution", + ) +}); +pub static DISCOVERY_REQS: LazyLock> = LazyLock::new(|| { + try_create_float_gauge( + "discovery_requests", + "The number of unsolicited discovery requests per second", + ) +}); +pub static DISCOVERY_SESSIONS: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "discovery_sessions", + "The number of active discovery sessions with peers", + ) +}); + +pub fn scrape_discovery_metrics() { + let metrics = discv5::metrics::Metrics::from(discv5::Discv5::raw_metrics()); + set_float_gauge(&DISCOVERY_REQS, metrics.unsolicited_requests_per_second); + set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); + set_gauge_vec(&DISCOVERY_BYTES, &["inbound"], metrics.bytes_recv as i64); + set_gauge_vec(&DISCOVERY_BYTES, &["outbound"], metrics.bytes_sent as i64); + set_gauge_vec(&NAT_OPEN, &["discv5_ipv4"], metrics.ipv4_contactable as i64); + set_gauge_vec(&NAT_OPEN, &["discv5_ipv6"], metrics.ipv6_contactable as i64); +} diff --git a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs b/common/network_utils/src/enr_ext.rs similarity index 83% rename from beacon_node/lighthouse_network/src/discovery/enr_ext.rs rename to common/network_utils/src/enr_ext.rs index bae7235604..627dd15559 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs +++ b/common/network_utils/src/enr_ext.rs @@ -1,11 +1,12 @@ //! ENR extension trait to support libp2p integration. -use crate::{Enr, Multiaddr, PeerId}; use discv5::enr::{CombinedKey, CombinedPublicKey}; -use libp2p::core::multiaddr::Protocol; -use libp2p::identity::{ed25519, secp256k1, KeyType, Keypair, PublicKey}; +use libp2p_identity::{KeyType, Keypair, PublicKey, ed25519, secp256k1}; +use multiaddr::{Multiaddr, PeerId, Protocol}; use tiny_keccak::{Hasher, Keccak}; +type Enr = discv5::enr::Enr; + pub const QUIC_ENR_KEY: &str = "quic"; pub const QUIC6_ENR_KEY: &str = "quic6"; @@ -164,21 +165,21 @@ impl EnrExt for Enr { fn multiaddr_p2p_tcp(&self) -> Vec { let peer_id = self.peer_id(); let mut multiaddrs: Vec = Vec::new(); - if let Some(ip) = self.ip4() { - if let Some(tcp) = self.tcp4() { - let mut multiaddr: Multiaddr = ip.into(); - multiaddr.push(Protocol::Tcp(tcp)); - multiaddr.push(Protocol::P2p(peer_id)); - multiaddrs.push(multiaddr); - } + if let Some(ip) = self.ip4() + && let Some(tcp) = self.tcp4() + { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddr.push(Protocol::P2p(peer_id)); + multiaddrs.push(multiaddr); } - if let Some(ip6) = self.ip6() { - if let Some(tcp6) = self.tcp6() { - let mut multiaddr: Multiaddr = ip6.into(); - multiaddr.push(Protocol::Tcp(tcp6)); - multiaddr.push(Protocol::P2p(peer_id)); - multiaddrs.push(multiaddr); - } + if let Some(ip6) = self.ip6() + && let Some(tcp6) = self.tcp6() + { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddr.push(Protocol::P2p(peer_id)); + multiaddrs.push(multiaddr); } multiaddrs } @@ -190,21 +191,21 @@ impl EnrExt for Enr { fn multiaddr_p2p_udp(&self) -> Vec { let peer_id = self.peer_id(); let mut multiaddrs: Vec = Vec::new(); - if let Some(ip) = self.ip4() { - if let Some(udp) = self.udp4() { - let mut multiaddr: Multiaddr = ip.into(); - multiaddr.push(Protocol::Udp(udp)); - multiaddr.push(Protocol::P2p(peer_id)); - multiaddrs.push(multiaddr); - } + if let Some(ip) = self.ip4() + && let Some(udp) = self.udp4() + { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(udp)); + multiaddr.push(Protocol::P2p(peer_id)); + multiaddrs.push(multiaddr); } - if let Some(ip6) = self.ip6() { - if let Some(udp6) = self.udp6() { - let mut multiaddr: Multiaddr = ip6.into(); - multiaddr.push(Protocol::Udp(udp6)); - multiaddr.push(Protocol::P2p(peer_id)); - multiaddrs.push(multiaddr); - } + if let Some(ip6) = self.ip6() + && let Some(udp6) = self.udp6() + { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(udp6)); + multiaddr.push(Protocol::P2p(peer_id)); + multiaddrs.push(multiaddr); } multiaddrs } @@ -212,22 +213,22 @@ impl EnrExt for Enr { /// Returns a list of multiaddrs if the ENR has an `ip` and a `quic` key **or** an `ip6` and a `quic6`. fn multiaddr_quic(&self) -> Vec { let mut multiaddrs: Vec = Vec::new(); - if let Some(quic_port) = self.quic4() { - if let Some(ip) = self.ip4() { - let mut multiaddr: Multiaddr = ip.into(); - multiaddr.push(Protocol::Udp(quic_port)); - multiaddr.push(Protocol::QuicV1); - multiaddrs.push(multiaddr); - } + if let Some(quic_port) = self.quic4() + && let Some(ip) = self.ip4() + { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(quic_port)); + multiaddr.push(Protocol::QuicV1); + multiaddrs.push(multiaddr); } - if let Some(quic6_port) = self.quic6() { - if let Some(ip6) = self.ip6() { - let mut multiaddr: Multiaddr = ip6.into(); - multiaddr.push(Protocol::Udp(quic6_port)); - multiaddr.push(Protocol::QuicV1); - multiaddrs.push(multiaddr); - } + if let Some(quic6_port) = self.quic6() + && let Some(ip6) = self.ip6() + { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(quic6_port)); + multiaddr.push(Protocol::QuicV1); + multiaddrs.push(multiaddr); } multiaddrs } @@ -235,19 +236,19 @@ impl EnrExt for Enr { /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp` or `udp` key **or** an `ip6` and either a `tcp6` or `udp6`. fn multiaddr_tcp(&self) -> Vec { let mut multiaddrs: Vec = Vec::new(); - if let Some(ip) = self.ip4() { - if let Some(tcp) = self.tcp4() { - let mut multiaddr: Multiaddr = ip.into(); - multiaddr.push(Protocol::Tcp(tcp)); - multiaddrs.push(multiaddr); - } + if let Some(ip) = self.ip4() + && let Some(tcp) = self.tcp4() + { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Tcp(tcp)); + multiaddrs.push(multiaddr); } - if let Some(ip6) = self.ip6() { - if let Some(tcp6) = self.tcp6() { - let mut multiaddr: Multiaddr = ip6.into(); - multiaddr.push(Protocol::Tcp(tcp6)); - multiaddrs.push(multiaddr); - } + if let Some(ip6) = self.ip6() + && let Some(tcp6) = self.tcp6() + { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Tcp(tcp6)); + multiaddrs.push(multiaddr); } multiaddrs } diff --git a/common/network_utils/src/lib.rs b/common/network_utils/src/lib.rs new file mode 100644 index 0000000000..c3d6ee1e0c --- /dev/null +++ b/common/network_utils/src/lib.rs @@ -0,0 +1,4 @@ +pub mod discovery_metrics; +pub mod enr_ext; +pub mod listen_addr; +pub mod unused_port; diff --git a/beacon_node/lighthouse_network/src/listen_addr.rs b/common/network_utils/src/listen_addr.rs similarity index 86% rename from beacon_node/lighthouse_network/src/listen_addr.rs rename to common/network_utils/src/listen_addr.rs index 3b0ff98b34..bdd94b3414 100644 --- a/beacon_node/lighthouse_network/src/listen_addr.rs +++ b/common/network_utils/src/listen_addr.rs @@ -1,6 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use libp2p::{multiaddr::Protocol, Multiaddr}; +use multiaddr::{Multiaddr, Protocol}; use serde::{Deserialize, Serialize}; /// A listening address composed by an Ip, an UDP port and a TCP port. @@ -84,23 +84,21 @@ impl ListenAddress { .chain(v6_tcp_multiaddr) } - #[cfg(test)] pub fn unused_v4_ports() -> Self { ListenAddress::V4(ListenAddr { addr: Ipv4Addr::UNSPECIFIED, - disc_port: unused_port::unused_udp4_port().unwrap(), - quic_port: unused_port::unused_udp4_port().unwrap(), - tcp_port: unused_port::unused_tcp4_port().unwrap(), + disc_port: crate::unused_port::unused_udp4_port().unwrap(), + quic_port: crate::unused_port::unused_udp4_port().unwrap(), + tcp_port: crate::unused_port::unused_tcp4_port().unwrap(), }) } - #[cfg(test)] pub fn unused_v6_ports() -> Self { ListenAddress::V6(ListenAddr { addr: Ipv6Addr::UNSPECIFIED, - disc_port: unused_port::unused_udp6_port().unwrap(), - quic_port: unused_port::unused_udp6_port().unwrap(), - tcp_port: unused_port::unused_tcp6_port().unwrap(), + disc_port: crate::unused_port::unused_udp6_port().unwrap(), + quic_port: crate::unused_port::unused_udp6_port().unwrap(), + tcp_port: crate::unused_port::unused_tcp6_port().unwrap(), }) } } diff --git a/common/unused_port/src/lib.rs b/common/network_utils/src/unused_port.rs similarity index 100% rename from common/unused_port/src/lib.rs rename to common/network_utils/src/unused_port.rs diff --git a/common/sensitive_url/Cargo.toml b/common/sensitive_url/Cargo.toml deleted file mode 100644 index ff56209722..0000000000 --- a/common/sensitive_url/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "sensitive_url" -version = "0.1.0" -authors = ["Mac L "] -edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -serde = { workspace = true } -url = { workspace = true } diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs deleted file mode 100644 index b6068a2dca..0000000000 --- a/common/sensitive_url/src/lib.rs +++ /dev/null @@ -1,120 +0,0 @@ -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt; -use std::str::FromStr; -use url::Url; - -#[derive(Debug)] -pub enum SensitiveError { - InvalidUrl(String), - ParseError(url::ParseError), - RedactError(String), -} - -impl fmt::Display for SensitiveError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -// Wrapper around Url which provides a custom `Display` implementation to protect user secrets. -#[derive(Clone, PartialEq)] -pub struct SensitiveUrl { - pub full: Url, - pub redacted: String, -} - -impl fmt::Display for SensitiveUrl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.redacted.fmt(f) - } -} - -impl fmt::Debug for SensitiveUrl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.redacted.fmt(f) - } -} - -impl AsRef for SensitiveUrl { - fn as_ref(&self) -> &str { - self.redacted.as_str() - } -} - -impl Serialize for SensitiveUrl { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(self.full.as_ref()) - } -} - -impl<'de> Deserialize<'de> for SensitiveUrl { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s: String = Deserialize::deserialize(deserializer)?; - SensitiveUrl::parse(&s) - .map_err(|e| de::Error::custom(format!("Failed to deserialize sensitive URL {:?}", e))) - } -} - -impl FromStr for SensitiveUrl { - type Err = SensitiveError; - - fn from_str(s: &str) -> Result { - Self::parse(s) - } -} - -impl SensitiveUrl { - pub fn parse(url: &str) -> Result { - let surl = Url::parse(url).map_err(SensitiveError::ParseError)?; - SensitiveUrl::new(surl) - } - - pub fn new(full: Url) -> Result { - let mut redacted = full.clone(); - redacted - .path_segments_mut() - .map_err(|_| SensitiveError::InvalidUrl("URL cannot be a base.".to_string()))? - .clear(); - redacted.set_query(None); - - if redacted.has_authority() { - redacted.set_username("").map_err(|_| { - SensitiveError::RedactError("Unable to redact username.".to_string()) - })?; - redacted.set_password(None).map_err(|_| { - SensitiveError::RedactError("Unable to redact password.".to_string()) - })?; - } - - Ok(Self { - full, - redacted: redacted.to_string(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn redact_remote_url() { - let full = "https://project:secret@example.com/example?somequery"; - let surl = SensitiveUrl::parse(full).unwrap(); - assert_eq!(surl.to_string(), "https://example.com/"); - assert_eq!(surl.full.to_string(), full); - } - #[test] - fn redact_localhost_url() { - let full = "http://localhost:5052/"; - let surl = SensitiveUrl::parse(full).unwrap(); - assert_eq!(surl.to_string(), "http://localhost:5052/"); - assert_eq!(surl.full.to_string(), full); - } -} diff --git a/common/slot_clock/src/lib.rs b/common/slot_clock/src/lib.rs index a742e29457..e51bc3f647 100644 --- a/common/slot_clock/src/lib.rs +++ b/common/slot_clock/src/lib.rs @@ -8,8 +8,8 @@ pub use crate::manual_slot_clock::ManualSlotClock as TestingSlotClock; pub use crate::manual_slot_clock::ManualSlotClock; pub use crate::system_time_slot_clock::SystemTimeSlotClock; pub use metrics::scrape_for_metrics; -use types::consts::bellatrix::INTERVALS_PER_SLOT; pub use types::Slot; +use types::consts::bellatrix::INTERVALS_PER_SLOT; /// A clock that reports the current slot. /// diff --git a/common/system_health/Cargo.toml b/common/system_health/Cargo.toml index 034683f72e..2cafc42d6e 100644 --- a/common/system_health/Cargo.toml +++ b/common/system_health/Cargo.toml @@ -5,6 +5,8 @@ edition = { workspace = true } [dependencies] lighthouse_network = { workspace = true } +metrics = { workspace = true } +network_utils = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } sysinfo = { workspace = true } diff --git a/common/system_health/src/lib.rs b/common/system_health/src/lib.rs index 9f351e943b..b61bdec486 100644 --- a/common/system_health/src/lib.rs +++ b/common/system_health/src/lib.rs @@ -1,4 +1,5 @@ -use lighthouse_network::{types::SyncState, NetworkGlobals}; +use lighthouse_network::{NetworkGlobals, types::SyncState}; +use network_utils::discovery_metrics; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -219,33 +220,21 @@ impl NatState { /// Observes if NAT traversal is possible. pub fn observe_nat() -> NatState { - let discv5_ipv4 = lighthouse_network::metrics::get_int_gauge( - &lighthouse_network::metrics::NAT_OPEN, - &["discv5_ipv4"], - ) - .map(|g| g.get() == 1) - .unwrap_or_default(); + let discv5_ipv4 = metrics::get_int_gauge(&discovery_metrics::NAT_OPEN, &["discv5_ipv4"]) + .map(|g| g.get() == 1) + .unwrap_or_default(); - let discv5_ipv6 = lighthouse_network::metrics::get_int_gauge( - &lighthouse_network::metrics::NAT_OPEN, - &["discv5_ipv6"], - ) - .map(|g| g.get() == 1) - .unwrap_or_default(); + let discv5_ipv6 = metrics::get_int_gauge(&discovery_metrics::NAT_OPEN, &["discv5_ipv6"]) + .map(|g| g.get() == 1) + .unwrap_or_default(); - let libp2p_ipv4 = lighthouse_network::metrics::get_int_gauge( - &lighthouse_network::metrics::NAT_OPEN, - &["libp2p_ipv4"], - ) - .map(|g| g.get() == 1) - .unwrap_or_default(); + let libp2p_ipv4 = metrics::get_int_gauge(&discovery_metrics::NAT_OPEN, &["libp2p_ipv4"]) + .map(|g| g.get() == 1) + .unwrap_or_default(); - let libp2p_ipv6 = lighthouse_network::metrics::get_int_gauge( - &lighthouse_network::metrics::NAT_OPEN, - &["libp2p_ipv6"], - ) - .map(|g| g.get() == 1) - .unwrap_or_default(); + let libp2p_ipv6 = metrics::get_int_gauge(&discovery_metrics::NAT_OPEN, &["libp2p_ipv6"]) + .map(|g| g.get() == 1) + .unwrap_or_default(); NatState { discv5_ipv4, diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index 4224f00acc..92a4fc4b59 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -8,5 +8,10 @@ edition = { workspace = true } async-channel = { workspace = true } futures = { workspace = true } metrics = { workspace = true } +num_cpus = { workspace = true } +rayon = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tokio_unstable)"] } diff --git a/common/task_executor/src/lib.rs b/common/task_executor/src/lib.rs index dbdac600f3..0b8e9f8eba 100644 --- a/common/task_executor/src/lib.rs +++ b/common/task_executor/src/lib.rs @@ -1,12 +1,15 @@ mod metrics; +mod rayon_pool_provider; pub mod test_utils; use futures::channel::mpsc::Sender; use futures::prelude::*; -use std::sync::Weak; +use std::sync::{Arc, Weak}; use tokio::runtime::{Handle, Runtime}; -use tracing::{debug, instrument}; +use tracing::debug; +use crate::rayon_pool_provider::RayonPoolProvider; +pub use crate::rayon_pool_provider::RayonPoolType; pub use tokio::task::JoinHandle; /// Provides a reason when Lighthouse is shut down. @@ -81,7 +84,11 @@ pub struct TaskExecutor { signal_tx: Sender, /// The name of the service for inclusion in the logger output. + // FIXME(sproul): delete? + #[allow(dead_code)] service_name: String, + + rayon_pool_provider: Arc, } impl TaskExecutor { @@ -92,7 +99,6 @@ impl TaskExecutor { /// This function should only be used during testing. In production, prefer to obtain an /// instance of `Self` via a `environment::RuntimeContext` (see the `lighthouse/environment` /// crate). - #[instrument(parent = None,level = "info", fields(service = service_name), name = "task_executor", skip_all)] pub fn new>( handle: T, exit: async_channel::Receiver<()>, @@ -104,17 +110,18 @@ impl TaskExecutor { exit, signal_tx, service_name, + rayon_pool_provider: Arc::new(RayonPoolProvider::default()), } } /// Clones the task executor adding a service name. - #[instrument(parent = None,level = "info", fields(service = service_name), name = "task_executor", skip_all)] pub fn clone_with_name(&self, service_name: String) -> Self { TaskExecutor { handle_provider: self.handle_provider.clone(), exit: self.exit.clone(), signal_tx: self.signal_tx.clone(), service_name, + rayon_pool_provider: self.rayon_pool_provider.clone(), } } @@ -124,7 +131,6 @@ impl TaskExecutor { /// The purpose of this function is to create a compile error if some function which previously /// returned `()` starts returning something else. Such a case may otherwise result in /// accidental error suppression. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_ignoring_error( &self, task: impl Future> + Send + 'static, @@ -136,7 +142,6 @@ impl TaskExecutor { /// Spawn a task to monitor the completion of another task. /// /// If the other task exits by panicking, then the monitor task will shut down the executor. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] fn spawn_monitor( &self, task_handle: impl Future> + Send + 'static, @@ -144,16 +149,23 @@ impl TaskExecutor { ) { let mut shutdown_sender = self.shutdown_sender(); if let Some(handle) = self.handle() { - handle.spawn(async move { + let fut = async move { let timer = metrics::start_timer_vec(&metrics::TASKS_HISTOGRAM, &[name]); - if let Err(join_error) = task_handle.await { - if let Ok(_panic) = join_error.try_into_panic() { - let _ = shutdown_sender - .try_send(ShutdownReason::Failure("Panic (fatal error)")); - } + if let Err(join_error) = task_handle.await + && let Ok(_panic) = join_error.try_into_panic() + { + let _ = + shutdown_sender.try_send(ShutdownReason::Failure("Panic (fatal error)")); } drop(timer); - }); + }; + #[cfg(tokio_unstable)] + tokio::task::Builder::new() + .name(&format!("{name}-monitor")) + .spawn_on(fut, &handle) + .expect("Failed to spawn monitor task"); + #[cfg(not(tokio_unstable))] + handle.spawn(fut); } else { debug!("Couldn't spawn monitor task. Runtime shutting down") } @@ -168,7 +180,6 @@ impl TaskExecutor { /// of a panic, the executor will be shut down via `self.signal_tx`. /// /// This function generates prometheus metrics on number of tasks and task duration. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn(&self, task: impl Future + Send + 'static, name: &'static str) { if let Some(task_handle) = self.spawn_handle(task, name) { self.spawn_monitor(task_handle, name) @@ -184,7 +195,6 @@ impl TaskExecutor { /// This is useful in cases where the future to be spawned needs to do additional cleanup work when /// the task is completed/canceled (e.g. writing local variables to disk) or the task is created from /// some framework which does its own cleanup (e.g. a hyper server). - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_without_exit( &self, task: impl Future + Send + 'static, @@ -199,6 +209,12 @@ impl TaskExecutor { int_gauge.inc(); if let Some(handle) = self.handle() { + #[cfg(tokio_unstable)] + tokio::task::Builder::new() + .name(name) + .spawn_on(future, &handle) + .expect("Failed to spawn task"); + #[cfg(not(tokio_unstable))] handle.spawn(future); } else { debug!("Couldn't spawn task. Runtime shutting down"); @@ -217,12 +233,52 @@ impl TaskExecutor { } } + /// Spawns a blocking task on a dedicated tokio thread pool and installs a rayon context within it. + pub fn spawn_blocking_with_rayon( + self, + task: F, + rayon_pool_type: RayonPoolType, + name: &'static str, + ) where + F: FnOnce() + Send + 'static, + { + let thread_pool = self.rayon_pool_provider.get_thread_pool(rayon_pool_type); + self.spawn_blocking( + move || { + thread_pool.install(|| { + task(); + }); + }, + name, + ) + } + + /// Spawns a blocking computation on a rayon thread pool and awaits the result. + pub async fn spawn_blocking_with_rayon_async( + &self, + rayon_pool_type: RayonPoolType, + task: F, + ) -> Result + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let thread_pool = self.rayon_pool_provider.get_thread_pool(rayon_pool_type); + let (tx, rx) = tokio::sync::oneshot::channel(); + + thread_pool.spawn(move || { + let result = task(); + let _ = tx.send(result); + }); + + rx.await + } + /// Spawn a future on the tokio runtime wrapped in an `async-channel::Receiver` returning an optional /// join handle to the future. /// The task is cancelled when the corresponding async-channel is dropped. /// /// This function generates prometheus metrics on number of tasks and task duration. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_handle( &self, task: impl Future + Send + 'static, @@ -234,7 +290,7 @@ impl TaskExecutor { let int_gauge_1 = int_gauge.clone(); int_gauge.inc(); if let Some(handle) = self.handle() { - Some(handle.spawn(async move { + let fut = async move { futures::pin_mut!(exit); let result = match future::select(Box::pin(task), exit).await { future::Either::Left((value, _)) => Some(value), @@ -245,7 +301,16 @@ impl TaskExecutor { }; int_gauge_1.dec(); result - })) + }; + #[cfg(tokio_unstable)] + return Some( + tokio::task::Builder::new() + .name(name) + .spawn_on(fut, &handle) + .expect("Failed to spawn task"), + ); + #[cfg(not(tokio_unstable))] + Some(handle.spawn(fut)) } else { debug!("Couldn't spawn task. Runtime shutting down"); None @@ -261,12 +326,11 @@ impl TaskExecutor { /// The Future returned behaves like the standard JoinHandle which can return an error if the /// task failed. /// This function generates prometheus metrics on number of tasks and task duration. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn spawn_blocking_handle( &self, task: F, name: &'static str, - ) -> Option>> + ) -> Option> + Send + 'static + use> where F: FnOnce() -> R + Send + 'static, R: Send + 'static, @@ -310,7 +374,6 @@ impl TaskExecutor { /// a `tokio` context present in the thread-local storage due to some `rayon` funkiness. Talk to /// @paulhauner if you plan to use this function in production. He has put metrics in here to /// track any use of it, so don't think you can pull a sneaky one on him. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn block_on_dangerous( &self, future: F, @@ -346,14 +409,13 @@ impl TaskExecutor { } /// Returns a `Handle` to the current runtime. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn handle(&self) -> Option { self.handle_provider.handle() } /// Returns a future that completes when `async-channel::Sender` is dropped or () is sent, /// which translates to the exit signal being triggered. - pub fn exit(&self) -> impl Future { + pub fn exit(&self) -> impl Future + 'static { let exit = self.exit.clone(); async move { let _ = exit.recv().await; @@ -361,7 +423,6 @@ impl TaskExecutor { } /// Get a channel to request shutting down. - #[instrument(parent = None,level = "info", fields(service = self.service_name), name = "task_executor", skip_all)] pub fn shutdown_sender(&self) -> Sender { self.signal_tx.clone() } diff --git a/common/task_executor/src/rayon_pool_provider.rs b/common/task_executor/src/rayon_pool_provider.rs new file mode 100644 index 0000000000..8e12f7eaa4 --- /dev/null +++ b/common/task_executor/src/rayon_pool_provider.rs @@ -0,0 +1,58 @@ +use rayon::{ThreadPool, ThreadPoolBuilder}; +use std::sync::Arc; + +const DEFAULT_LOW_PRIORITY_CPU_PERCENTAGE: usize = 25; +const DEFAULT_HIGH_PRIORITY_CPU_PERCENTAGE: usize = 80; +const MINIMUM_THREAD_COUNT: usize = 1; + +pub enum RayonPoolType { + HighPriority, + LowPriority, +} + +pub struct RayonPoolProvider { + /// Smaller rayon thread pool for lower-priority, compute-intensive tasks. + /// By default ~25% of CPUs or a minimum of 1 thread. + low_priority_thread_pool: Arc, + /// Larger rayon thread pool for high-priority, compute-intensive tasks. + /// By default ~80% of CPUs or a minimum of 1 thread. Citical/highest + /// priority tasks should use the global pool instead. + high_priority_thread_pool: Arc, +} + +impl Default for RayonPoolProvider { + fn default() -> Self { + let low_prio_threads = + (num_cpus::get() * DEFAULT_LOW_PRIORITY_CPU_PERCENTAGE / 100).max(MINIMUM_THREAD_COUNT); + let low_priority_thread_pool = Arc::new( + ThreadPoolBuilder::new() + .num_threads(low_prio_threads) + .build() + .expect("failed to build low-priority rayon pool"), + ); + + let high_prio_threads = (num_cpus::get() * DEFAULT_HIGH_PRIORITY_CPU_PERCENTAGE / 100) + .max(MINIMUM_THREAD_COUNT); + let high_priority_thread_pool = Arc::new( + ThreadPoolBuilder::new() + .num_threads(high_prio_threads) + .build() + .expect("failed to build high-priority rayon pool"), + ); + Self { + low_priority_thread_pool, + high_priority_thread_pool, + } + } +} + +impl RayonPoolProvider { + /// Get a scoped thread pool by priority level. + /// For critical/highest priority tasks, use the global pool instead. + pub fn get_thread_pool(&self, rayon_pool_type: RayonPoolType) -> Arc { + match rayon_pool_type { + RayonPoolType::HighPriority => self.high_priority_thread_pool.clone(), + RayonPoolType::LowPriority => self.low_priority_thread_pool.clone(), + } + } +} diff --git a/common/test_random_derive/src/lib.rs b/common/test_random_derive/src/lib.rs index 8c4b1ef7c3..bf57d79aaa 100644 --- a/common/test_random_derive/src/lib.rs +++ b/common/test_random_derive/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::{DeriveInput, parse_macro_input}; /// Returns true if some field has an attribute declaring it should be generated from default (not /// randomized). @@ -8,7 +8,8 @@ use syn::{parse_macro_input, DeriveInput}; /// The field attribute is: `#[test_random(default)]` fn should_use_default(field: &syn::Field) -> bool { field.attrs.iter().any(|attr| { - attr.path.is_ident("test_random") && attr.tokens.to_string().replace(' ', "") == "(default)" + attr.path().is_ident("test_random") + && matches!(&attr.meta, syn::Meta::List(list) if list.tokens.to_string().replace(' ', "") == "default") }) } @@ -27,7 +28,7 @@ pub fn test_random_derive(input: TokenStream) -> TokenStream { let mut quotes = vec![]; for field in &struct_data.fields { match &field.ident { - Some(ref ident) => { + Some(ident) => { if should_use_default(field) { quotes.push(quote! { #ident: <_>::default(), diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml deleted file mode 100644 index 2d771cd600..0000000000 --- a/common/unused_port/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "unused_port" -version = "0.1.0" -edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lru_cache = { workspace = true } -parking_lot = { workspace = true } diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 4c03b7662e..a5b373fcae 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -11,7 +11,7 @@ insecure_keys = [] [dependencies] bls = { workspace = true } deposit_contract = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } eth2_keystore = { workspace = true } filesystem = { workspace = true } hex = { workspace = true } diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 2e971a8b1a..ab495242e4 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -1,13 +1,13 @@ use crate::{Error as DirError, ValidatorDir}; -use bls::get_withdrawal_credentials; -use deposit_contract::{encode_eth1_tx_data, Error as DepositError}; +use bls::{Keypair, Signature, get_withdrawal_credentials}; +use deposit_contract::{Error as DepositError, encode_eth1_tx_data}; use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText}; use filesystem::create_with_600_perms; -use rand::{distributions::Alphanumeric, Rng}; -use std::fs::{create_dir_all, File}; +use rand::{Rng, distr::Alphanumeric}; +use std::fs::{File, create_dir_all}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use types::{ChainSpec, DepositData, Hash256, Keypair, Signature}; +use types::{ChainSpec, DepositData, Hash256}; /// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62 /// characters. @@ -314,7 +314,7 @@ pub fn write_password_to_file>(path: P, bytes: &[u8]) -> Result<( /// Generates a random keystore with a random password. fn random_keystore() -> Result<(Keystore, PlainText), Error> { let keypair = Keypair::random(); - let password: PlainText = rand::thread_rng() + let password: PlainText = rand::rng() .sample_iter(&Alphanumeric) .take(DEFAULT_PASSWORD_LEN) .map(char::from) diff --git a/common/validator_dir/src/insecure_keys.rs b/common/validator_dir/src/insecure_keys.rs index 83720bb58c..7e48a13454 100644 --- a/common/validator_dir/src/insecure_keys.rs +++ b/common/validator_dir/src/insecure_keys.rs @@ -6,8 +6,8 @@ use crate::{Builder, BuilderError}; use eth2_keystore::{ + DKLEN, Keystore, KeystoreBuilder, PlainText, json_keystore::{Kdf, Scrypt}, - Keystore, KeystoreBuilder, PlainText, DKLEN, }; use std::path::PathBuf; use types::test_utils::generate_deterministic_keypair; diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index c21b0b44cf..7c1f0721e2 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -11,10 +11,10 @@ pub mod insecure_keys; mod validator_dir; pub use crate::validator_dir::{ - unlock_keypair_from_password_path, Error, Eth1DepositData, ValidatorDir, - ETH1_DEPOSIT_TX_HASH_FILE, + ETH1_DEPOSIT_TX_HASH_FILE, Error, Eth1DepositData, ValidatorDir, + unlock_keypair_from_password_path, }; pub use builder::{ - keystore_password_path, Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, - VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, + Builder, ETH1_DEPOSIT_DATA_FILE, Error as BuilderError, VOTING_KEYSTORE_FILE, + WITHDRAWAL_KEYSTORE_FILE, keystore_password_path, }; diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index 4f9b786844..0799897a70 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -1,16 +1,17 @@ use crate::builder::{ - keystore_password_path, ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, - WITHDRAWAL_KEYSTORE_FILE, + ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, + WITHDRAWAL_KEYSTORE_FILE, keystore_password_path, }; +use bls::Keypair; use deposit_contract::decode_eth1_tx_data; -use derivative::Derivative; +use educe::Educe; use eth2_keystore::{Error as KeystoreError, Keystore, PlainText}; use lockfile::{Lockfile, LockfileError}; -use std::fs::{read, write, File}; +use std::fs::{File, read, write}; use std::io; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; -use types::{DepositData, Hash256, Keypair}; +use types::{DepositData, Hash256}; /// The file used to save the Eth1 transaction hash from a deposit. pub const ETH1_DEPOSIT_TX_HASH_FILE: &str = "eth1-deposit-tx-hash.txt"; @@ -32,7 +33,7 @@ pub enum Error { UnableToReadDepositAmount(io::Error), UnableToParseDepositAmount(std::num::ParseIntError), DepositAmountIsNotUtf8(std::string::FromUtf8Error), - UnableToParseDepositData(deposit_contract::DecodeError), + UnableToParseDepositData(deposit_contract::Error), Eth1TxHashExists(PathBuf), UnableToWriteEth1TxHash(io::Error), /// The deposit root in the deposit data file does not match the one generated locally. This is @@ -56,11 +57,11 @@ pub struct Eth1DepositData { /// /// Holds a lockfile in `self.dir` to attempt to prevent concurrent access from multiple /// processes. -#[derive(Debug, Derivative)] -#[derivative(PartialEq)] +#[derive(Debug, Educe)] +#[educe(PartialEq)] pub struct ValidatorDir { dir: PathBuf, - #[derivative(PartialEq = "ignore")] + #[educe(PartialEq(ignore))] _lockfile: Lockfile, } diff --git a/common/validator_dir/tests/tests.rs b/common/validator_dir/tests/tests.rs index a782d81bbe..ede80c244e 100644 --- a/common/validator_dir/tests/tests.rs +++ b/common/validator_dir/tests/tests.rs @@ -1,13 +1,14 @@ #![cfg(not(debug_assertions))] +use bls::Keypair; use eth2_keystore::{Keystore, KeystoreBuilder, PlainText}; use std::fs::{self, File}; use std::path::Path; -use tempfile::{tempdir, TempDir}; -use types::{test_utils::generate_deterministic_keypair, EthSpec, Keypair, MainnetEthSpec}; +use tempfile::{TempDir, tempdir}; +use types::{EthSpec, MainnetEthSpec, test_utils::generate_deterministic_keypair}; use validator_dir::{ - Builder, BuilderError, ValidatorDir, ETH1_DEPOSIT_DATA_FILE, ETH1_DEPOSIT_TX_HASH_FILE, - VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, + Builder, BuilderError, ETH1_DEPOSIT_DATA_FILE, ETH1_DEPOSIT_TX_HASH_FILE, VOTING_KEYSTORE_FILE, + ValidatorDir, WITHDRAWAL_KEYSTORE_FILE, }; /// A very weak password with which to encrypt the keystores. diff --git a/common/warp_utils/src/json.rs b/common/warp_utils/src/json.rs index bc7d61557b..b3f066194d 100644 --- a/common/warp_utils/src/json.rs +++ b/common/warp_utils/src/json.rs @@ -20,12 +20,12 @@ pub fn json() -> impl Filter(CONTENT_TYPE_HEADER) .and(warp::body::bytes()) .and_then(|header: Option, bytes: Bytes| async move { - if let Some(header) = header { - if header == SSZ_CONTENT_TYPE_HEADER { - return Err(reject::unsupported_media_type( - "The request's content-type is not supported".to_string(), - )); - } + if let Some(header) = header + && header == SSZ_CONTENT_TYPE_HEADER + { + return Err(reject::unsupported_media_type( + "The request's content-type is not supported".to_string(), + )); } Json::decode(bytes) .map_err(|err| reject::custom_deserialize_error(format!("{:?}", err))) @@ -33,17 +33,17 @@ pub fn json() -> impl Filter( -) -> impl Filter + Copy { +pub fn json_no_body() +-> impl Filter + Copy { warp::header::optional::(CONTENT_TYPE_HEADER) .and(warp::body::bytes()) .and_then(|header: Option, bytes: Bytes| async move { - if let Some(header) = header { - if header == SSZ_CONTENT_TYPE_HEADER { - return Err(reject::unsupported_media_type( - "The request's content-type is not supported".to_string(), - )); - } + if let Some(header) = header + && header == SSZ_CONTENT_TYPE_HEADER + { + return Err(reject::unsupported_media_type( + "The request's content-type is not supported".to_string(), + )); } // Handle the case when the HTTP request has no body, i.e., without the -d header diff --git a/common/warp_utils/src/lib.rs b/common/warp_utils/src/lib.rs index c10adbac0d..1c77d4d84b 100644 --- a/common/warp_utils/src/lib.rs +++ b/common/warp_utils/src/lib.rs @@ -5,5 +5,6 @@ pub mod cors; pub mod json; pub mod query; pub mod reject; +pub mod status_code; pub mod task; pub mod uor; diff --git a/common/warp_utils/src/query.rs b/common/warp_utils/src/query.rs index c5ed5c5f12..8121a90139 100644 --- a/common/warp_utils/src/query.rs +++ b/common/warp_utils/src/query.rs @@ -4,8 +4,8 @@ use warp::Filter; // Custom query filter using `serde_array_query`. // This allows duplicate keys inside query strings. -pub fn multi_key_query<'de, T: Deserialize<'de>>( -) -> impl warp::Filter,), Error = std::convert::Infallible> + Copy +pub fn multi_key_query<'de, T: Deserialize<'de>>() +-> impl warp::Filter,), Error = std::convert::Infallible> + Copy { raw_query().then(|query_str: String| async move { serde_array_query::from_str(&query_str).map_err(|e| custom_bad_request(e.to_string())) diff --git a/common/warp_utils/src/reject.rs b/common/warp_utils/src/reject.rs index 3c7ef5e4fa..c478870950 100644 --- a/common/warp_utils/src/reject.rs +++ b/common/warp_utils/src/reject.rs @@ -3,7 +3,7 @@ use std::convert::Infallible; use std::error::Error; use std::fmt; use std::fmt::Debug; -use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; +use warp::{Reply, http::StatusCode, reject::Reject, reply::Response}; #[derive(Debug)] pub struct ServerSentEventError(pub String); @@ -237,15 +237,9 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result(res: Result) -> Response { match res { Ok(response) => response.into_response(), - Err(e) => match handle_rejection(e).await { - Ok(reply) => reply.into_response(), - // We can simplify this once Rust 1.82 is MSRV - #[allow(unreachable_patterns)] - Err(_) => warp::reply::with_status( - warp::reply::json(&"unhandled error"), - eth2::StatusCode::INTERNAL_SERVER_ERROR, - ) - .into_response(), - }, + Err(e) => { + let Ok(reply) = handle_rejection(e).await; + reply.into_response() + } } } diff --git a/common/warp_utils/src/status_code.rs b/common/warp_utils/src/status_code.rs new file mode 100644 index 0000000000..1b05297359 --- /dev/null +++ b/common/warp_utils/src/status_code.rs @@ -0,0 +1,9 @@ +use eth2::StatusCode; +use warp::Rejection; + +/// Convert from a "new" `http::StatusCode` to a `warp` compatible one. +pub fn convert(code: StatusCode) -> Result { + code.as_u16().try_into().map_err(|e| { + crate::reject::custom_server_error(format!("bad status code {code:?} - {e:?}")) + }) +} diff --git a/common/warp_utils/src/uor.rs b/common/warp_utils/src/uor.rs index 363f1df7d4..c5f421693b 100644 --- a/common/warp_utils/src/uor.rs +++ b/common/warp_utils/src/uor.rs @@ -1,4 +1,4 @@ -use warp::{filters::BoxedFilter, Filter, Rejection}; +use warp::{Filter, Rejection, filters::BoxedFilter}; /// Mixin trait for `Filter` providing the unifying-or method. pub trait UnifyingOrFilter: Filter + Sized + Send + Sync + 'static diff --git a/consensus/context_deserialize/Cargo.toml b/consensus/context_deserialize/Cargo.toml deleted file mode 100644 index 30dae76136..0000000000 --- a/consensus/context_deserialize/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "context_deserialize" -version = "0.1.0" -edition = "2021" - -[dependencies] -milhouse = { workspace = true } -serde = { workspace = true } -ssz_types = { workspace = true } diff --git a/consensus/context_deserialize/src/impls.rs b/consensus/context_deserialize/src/impls.rs deleted file mode 100644 index 803619365f..0000000000 --- a/consensus/context_deserialize/src/impls.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::ContextDeserialize; -use serde::de::{Deserialize, DeserializeSeed, Deserializer, SeqAccess, Visitor}; -use std::marker::PhantomData; -use std::sync::Arc; - -impl<'de, C, T> ContextDeserialize<'de, T> for Arc -where - C: ContextDeserialize<'de, T>, -{ - fn context_deserialize(deserializer: D, context: T) -> Result - where - D: Deserializer<'de>, - { - Ok(Arc::new(C::context_deserialize(deserializer, context)?)) - } -} - -impl<'de, T, C> ContextDeserialize<'de, C> for Vec -where - T: ContextDeserialize<'de, C>, - C: Clone, -{ - fn context_deserialize(deserializer: D, context: C) -> Result - where - D: Deserializer<'de>, - { - // Our Visitor, which owns one copy of the context T - struct ContextVisitor { - context: T, - _marker: PhantomData, - } - - impl<'de, C, T> Visitor<'de> for ContextVisitor - where - C: ContextDeserialize<'de, T>, - T: Clone, - { - type Value = Vec; - - fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - fmt.write_str("a sequence of context‐deserialized elements") - } - - fn visit_seq(self, mut seq: A) -> Result, A::Error> - where - A: SeqAccess<'de>, - { - let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0)); - // for each element, we clone the context and hand it to the seed - while let Some(elem) = seq.next_element_seed(ContextSeed { - context: self.context.clone(), - _marker: PhantomData, - })? { - out.push(elem); - } - Ok(out) - } - } - - // A little seed that hands the deserializer + context into C::context_deserialize - struct ContextSeed { - context: C, - _marker: PhantomData, - } - - impl<'de, T, C> DeserializeSeed<'de> for ContextSeed - where - T: ContextDeserialize<'de, C>, - C: Clone, - { - type Value = T; - - fn deserialize(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - T::context_deserialize(deserializer, self.context) - } - } - - deserializer.deserialize_seq(ContextVisitor { - context, - _marker: PhantomData, - }) - } -} - -macro_rules! trivial_deserialize { - ($($t:ty),* $(,)?) => { - $( - impl<'de, T> ContextDeserialize<'de, T> for $t { - fn context_deserialize(deserializer: D, _context: T) -> Result - where - D: Deserializer<'de>, - { - <$t>::deserialize(deserializer) - } - } - )* - }; -} - -trivial_deserialize!(bool, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64); diff --git a/consensus/context_deserialize/src/lib.rs b/consensus/context_deserialize/src/lib.rs deleted file mode 100644 index 9de819247b..0000000000 --- a/consensus/context_deserialize/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod impls; -pub mod milhouse; -pub mod ssz_impls; - -extern crate serde; -use serde::de::Deserializer; - -/// General-purpose deserialization trait that accepts extra context `C`. -pub trait ContextDeserialize<'de, C>: Sized { - fn context_deserialize(deserializer: D, context: C) -> Result - where - D: Deserializer<'de>; -} diff --git a/consensus/context_deserialize/src/milhouse.rs b/consensus/context_deserialize/src/milhouse.rs deleted file mode 100644 index 3b86f067a3..0000000000 --- a/consensus/context_deserialize/src/milhouse.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::ContextDeserialize; -use milhouse::{List, Value, Vector}; -use serde::de::Deserializer; -use ssz_types::typenum::Unsigned; - -impl<'de, C, T, N> ContextDeserialize<'de, C> for List -where - T: ContextDeserialize<'de, C> + Value, - N: Unsigned, - C: Clone, -{ - fn context_deserialize(deserializer: D, context: C) -> Result - where - D: Deserializer<'de>, - { - // First deserialize as a Vec. - // This is not the most efficient implementation as it allocates a temporary Vec. In future - // we could write a more performant implementation using `List::builder()`. - let vec = Vec::::context_deserialize(deserializer, context)?; - - // Then convert to List, which will check the length. - List::new(vec) - .map_err(|e| serde::de::Error::custom(format!("Failed to create List: {:?}", e))) - } -} - -impl<'de, C, T, N> ContextDeserialize<'de, C> for Vector -where - T: ContextDeserialize<'de, C> + Value, - N: Unsigned, - C: Clone, -{ - fn context_deserialize(deserializer: D, context: C) -> Result - where - D: Deserializer<'de>, - { - // First deserialize as a List - let list = List::::context_deserialize(deserializer, context)?; - - // Then convert to Vector, which will check the length - Vector::try_from(list).map_err(|e| { - serde::de::Error::custom(format!("Failed to convert List to Vector: {:?}", e)) - }) - } -} diff --git a/consensus/context_deserialize/src/ssz_impls.rs b/consensus/context_deserialize/src/ssz_impls.rs deleted file mode 100644 index e989d67b29..0000000000 --- a/consensus/context_deserialize/src/ssz_impls.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::serde::de::Error; -use crate::ContextDeserialize; -use serde::de::Deserializer; -use serde::Deserialize; -use ssz_types::length::{Fixed, Variable}; -use ssz_types::typenum::Unsigned; -use ssz_types::{Bitfield, FixedVector}; - -impl<'de, C, T, N> ContextDeserialize<'de, C> for FixedVector -where - T: ContextDeserialize<'de, C>, - N: Unsigned, - C: Clone, -{ - fn context_deserialize(deserializer: D, context: C) -> Result - where - D: Deserializer<'de>, - { - let vec = Vec::::context_deserialize(deserializer, context)?; - FixedVector::new(vec).map_err(|e| D::Error::custom(format!("{:?}", e))) - } -} - -impl<'de, C, N> ContextDeserialize<'de, C> for Bitfield> -where - N: Unsigned + Clone, -{ - fn context_deserialize(deserializer: D, _context: C) -> Result - where - D: Deserializer<'de>, - { - Bitfield::>::deserialize(deserializer) - .map_err(|e| D::Error::custom(format!("{:?}", e))) - } -} - -impl<'de, C, N> ContextDeserialize<'de, C> for Bitfield> -where - N: Unsigned + Clone, -{ - fn context_deserialize(deserializer: D, _context: C) -> Result - where - D: Deserializer<'de>, - { - Bitfield::>::deserialize(deserializer) - .map_err(|e| D::Error::custom(format!("{:?}", e))) - } -} diff --git a/consensus/context_deserialize_derive/Cargo.toml b/consensus/context_deserialize_derive/Cargo.toml deleted file mode 100644 index eedae30cdf..0000000000 --- a/consensus/context_deserialize_derive/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "context_deserialize_derive" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -quote = { workspace = true } -syn = { workspace = true } - -[dev-dependencies] -context_deserialize = { path = "../context_deserialize" } -serde = { workspace = true } -serde_json = "1.0" diff --git a/consensus/context_deserialize_derive/src/lib.rs b/consensus/context_deserialize_derive/src/lib.rs deleted file mode 100644 index 0b73a43b0a..0000000000 --- a/consensus/context_deserialize_derive/src/lib.rs +++ /dev/null @@ -1,118 +0,0 @@ -extern crate proc_macro; -extern crate quote; -extern crate syn; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse_macro_input, AttributeArgs, DeriveInput, GenericParam, LifetimeDef, Meta, NestedMeta, - WhereClause, -}; - -#[proc_macro_attribute] -pub fn context_deserialize(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as AttributeArgs); - let input = parse_macro_input!(item as DeriveInput); - let ident = &input.ident; - - let mut ctx_types = Vec::new(); - let mut explicit_where: Option = None; - - for meta in args { - match meta { - NestedMeta::Meta(Meta::Path(p)) => { - ctx_types.push(p); - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("bound") => { - if let syn::Lit::Str(lit_str) = &nv.lit { - let where_string = format!("where {}", lit_str.value()); - match syn::parse_str::(&where_string) { - Ok(where_clause) => { - explicit_where = Some(where_clause); - } - Err(err) => { - return syn::Error::new_spanned( - lit_str, - format!("Invalid where clause '{}': {}", lit_str.value(), err), - ) - .to_compile_error() - .into(); - } - } - } else { - return syn::Error::new_spanned( - &nv, - "Expected a string literal for `bound` value", - ) - .to_compile_error() - .into(); - } - } - _ => { - return syn::Error::new_spanned( - &meta, - "Expected paths or `bound = \"...\"` in #[context_deserialize(...)]", - ) - .to_compile_error() - .into(); - } - } - } - - if ctx_types.is_empty() { - return quote! { - compile_error!("Usage: #[context_deserialize(Type1, Type2, ..., bound = \"...\")]"); - } - .into(); - } - - let original_generics = input.generics.clone(); - - // Clone and clean generics for impl use (remove default params) - let mut impl_generics = input.generics.clone(); - for param in impl_generics.params.iter_mut() { - if let GenericParam::Type(ty) = param { - ty.eq_token = None; - ty.default = None; - } - } - - // Ensure 'de lifetime exists in impl generics - let has_de = impl_generics - .lifetimes() - .any(|LifetimeDef { lifetime, .. }| lifetime.ident == "de"); - - if !has_de { - impl_generics.params.insert(0, syn::parse_quote! { 'de }); - } - - let (_, ty_generics, _) = original_generics.split_for_impl(); - let (impl_gens, _, _) = impl_generics.split_for_impl(); - - // Generate: no `'de` applied to the type name - let mut impls = quote! {}; - for ctx in ctx_types { - impls.extend(quote! { - impl #impl_gens context_deserialize::ContextDeserialize<'de, #ctx> - for #ident #ty_generics - #explicit_where - { - fn context_deserialize( - deserializer: D, - _context: #ctx, - ) -> Result - where - D: serde::de::Deserializer<'de>, - { - ::deserialize(deserializer) - } - } - }); - } - - quote! { - #input - #impls - } - .into() -} diff --git a/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs b/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs deleted file mode 100644 index d6883400e0..0000000000 --- a/consensus/context_deserialize_derive/tests/context_deserialize_derive.rs +++ /dev/null @@ -1,94 +0,0 @@ -use context_deserialize::ContextDeserialize; -use context_deserialize_derive::context_deserialize; -use serde::{Deserialize, Serialize}; - -#[test] -fn test_context_deserialize_derive() { - type TestContext = (); - - #[context_deserialize(TestContext)] - #[derive(Debug, PartialEq, Serialize, Deserialize)] - struct Test { - field: String, - } - - let test = Test { - field: "test".to_string(), - }; - let serialized = serde_json::to_string(&test).unwrap(); - let deserialized = - Test::context_deserialize(&mut serde_json::Deserializer::from_str(&serialized), ()) - .unwrap(); - assert_eq!(test, deserialized); -} - -#[test] -fn test_context_deserialize_derive_multiple_types() { - #[allow(dead_code)] - struct TestContext1(u64); - #[allow(dead_code)] - struct TestContext2(String); - - // This will derive: - // - ContextDeserialize for Test - // - ContextDeserialize for Test - // by just leveraging the Deserialize impl - #[context_deserialize(TestContext1, TestContext2)] - #[derive(Debug, PartialEq, Serialize, Deserialize)] - struct Test { - field: String, - } - - let test = Test { - field: "test".to_string(), - }; - let serialized = serde_json::to_string(&test).unwrap(); - let deserialized = Test::context_deserialize( - &mut serde_json::Deserializer::from_str(&serialized), - TestContext1(1), - ) - .unwrap(); - assert_eq!(test, deserialized); - - let deserialized = Test::context_deserialize( - &mut serde_json::Deserializer::from_str(&serialized), - TestContext2("2".to_string()), - ) - .unwrap(); - - assert_eq!(test, deserialized); -} - -#[test] -fn test_context_deserialize_derive_bound() { - use std::fmt::Debug; - - struct TestContext; - - #[derive(Debug, PartialEq, Serialize, Deserialize)] - struct Inner { - value: u64, - } - - #[context_deserialize( - TestContext, - bound = "T: Serialize + for<'a> Deserialize<'a> + Debug + PartialEq" - )] - #[derive(Debug, PartialEq, Serialize, Deserialize)] - struct Wrapper { - inner: T, - } - - let val = Wrapper { - inner: Inner { value: 42 }, - }; - - let serialized = serde_json::to_string(&val).unwrap(); - let deserialized = Wrapper::::context_deserialize( - &mut serde_json::Deserializer::from_str(&serialized), - TestContext, - ) - .unwrap(); - - assert_eq!(val, deserialized); -} diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index 5c009a5e78..a07aa38aa5 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -8,10 +8,12 @@ edition = { workspace = true } [dependencies] ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } logging = { workspace = true } metrics = { workspace = true } proto_array = { workspace = true } state_processing = { workspace = true } +superstruct = { workspace = true } tracing = { workspace = true } types = { workspace = true } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 1840d37476..3fb049633f 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,10 +1,12 @@ use crate::metrics::{self, scrape_for_metrics}; use crate::{ForkChoiceStore, InvalidationOperation}; +use fixed_bytes::FixedBytesExtended; use logging::crit; use proto_array::{ - Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, ProposerHeadError, - ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, + Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, JustifiedBalances, + ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, }; +use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use state_processing::{ per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing, @@ -13,12 +15,13 @@ use std::cmp::Ordering; use std::collections::BTreeSet; use std::marker::PhantomData; use std::time::Duration; -use tracing::{debug, warn}; +use superstruct::superstruct; +use tracing::{debug, instrument, warn}; use types::{ - consts::bellatrix::INTERVALS_PER_SLOT, AbstractExecPayload, AttestationShufflingId, - AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, - Epoch, EthSpec, ExecPayload, ExecutionBlockHash, FixedBytesExtended, Hash256, - IndexedAttestationRef, RelativeEpoch, SignedBeaconBlock, Slot, + AbstractExecPayload, AttestationShufflingId, AttesterSlashingRef, BeaconBlockRef, BeaconState, + BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, + Hash256, IndexedAttestationRef, RelativeEpoch, SignedBeaconBlock, Slot, + consts::bellatrix::INTERVALS_PER_SLOT, }; #[derive(Debug)] @@ -473,6 +476,7 @@ where /// Is equivalent to: /// /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_head + #[instrument(skip_all, level = "debug")] pub fn get_head( &mut self, system_time_current_slot: Slot, @@ -521,6 +525,7 @@ where /// /// You *must* call `get_head` for the proposal slot prior to calling this function and pass /// in the result of `get_head` as `canonical_head`. + #[instrument(level = "debug", skip_all)] pub fn get_proposer_head( &self, current_slot: Slot, @@ -624,7 +629,7 @@ where op: &InvalidationOperation, ) -> Result<(), Error> { self.proto_array - .process_execution_payload_invalidation::(op) + .process_execution_payload_invalidation::(op, self.finalized_checkpoint()) .map_err(Error::FailedToProcessInvalidExecutionPayload) } @@ -653,6 +658,12 @@ where /// The supplied block **must** pass the `state_transition` function as it will not be run /// here. #[allow(clippy::too_many_arguments)] + #[instrument( + name = "fork_choice_on_block", + skip_all, + fields( + fork_choice_block_delay = ?block_delay + ))] pub fn on_block>( &mut self, system_time_current_slot: Slot, @@ -736,6 +747,11 @@ where self.update_checkpoints( state.current_justified_checkpoint(), state.finalized_checkpoint(), + || { + state + .get_state_root_at_epoch_start(state.current_justified_checkpoint().epoch) + .map_err(Into::into) + }, )?; // Update unrealized justified/finalized checkpoints. @@ -795,8 +811,15 @@ where if unrealized_justified_checkpoint.epoch > self.fc_store.unrealized_justified_checkpoint().epoch { - self.fc_store - .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); + // Justification has recently updated therefore the justified state root should be in + // range of the head state's `state_roots` vector. + let unrealized_justified_state_root = + state.get_state_root_at_epoch_start(unrealized_justified_checkpoint.epoch)?; + + self.fc_store.set_unrealized_justified_checkpoint( + unrealized_justified_checkpoint, + unrealized_justified_state_root, + ); } if unrealized_finalized_checkpoint.epoch > self.fc_store.unrealized_finalized_checkpoint().epoch @@ -810,6 +833,13 @@ where self.pull_up_store_checkpoints( unrealized_justified_checkpoint, unrealized_finalized_checkpoint, + || { + // In the case where we actually update justification, it must be that the + // unrealized justification is recent and in range of the `state_roots` vector. + state + .get_state_root_at_epoch_start(unrealized_justified_checkpoint.epoch) + .map_err(Into::into) + }, )?; } @@ -849,7 +879,7 @@ where block_slot: block.slot(), block_root, payload_verification_status, - }) + }); } } } @@ -886,6 +916,8 @@ where unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint), }, current_slot, + self.justified_checkpoint(), + self.finalized_checkpoint(), )?; Ok(()) @@ -896,11 +928,13 @@ where &mut self, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, + justified_state_root_producer: impl FnOnce() -> Result>, ) -> Result<(), Error> { // Update justified checkpoint. if justified_checkpoint.epoch > self.fc_store.justified_checkpoint().epoch { + let justified_state_root = justified_state_root_producer()?; self.fc_store - .set_justified_checkpoint(justified_checkpoint) + .set_justified_checkpoint(justified_checkpoint, justified_state_root) .map_err(Error::UnableToSetJustifiedCheckpoint)?; } @@ -1166,10 +1200,12 @@ where // Update the justified/finalized checkpoints based upon the // best-observed unrealized justification/finality. let unrealized_justified_checkpoint = *self.fc_store.unrealized_justified_checkpoint(); + let unrealized_justified_state_root = self.fc_store.unrealized_justified_state_root(); let unrealized_finalized_checkpoint = *self.fc_store.unrealized_finalized_checkpoint(); self.pull_up_store_checkpoints( unrealized_justified_checkpoint, unrealized_finalized_checkpoint, + || Ok(unrealized_justified_state_root), )?; Ok(()) @@ -1179,10 +1215,12 @@ where &mut self, unrealized_justified_checkpoint: Checkpoint, unrealized_finalized_checkpoint: Checkpoint, + unrealized_justified_state_root_producer: impl FnOnce() -> Result>, ) -> Result<(), Error> { self.update_checkpoints( unrealized_justified_checkpoint, unrealized_finalized_checkpoint, + unrealized_justified_state_root_producer, ) } @@ -1260,7 +1298,7 @@ where /// Return `true` if `block_root` is equal to the finalized checkpoint, or a known descendant of it. pub fn is_finalized_checkpoint_or_descendant(&self, block_root: Hash256) -> bool { self.proto_array - .is_finalized_checkpoint_or_descendant::(block_root) + .is_finalized_checkpoint_or_descendant::(block_root, self.finalized_checkpoint()) } pub fn is_descendant(&self, ancestor_root: Hash256, descendant_root: Hash256) -> bool { @@ -1375,12 +1413,16 @@ where /// Instantiate `Self` from some `PersistedForkChoice` generated by a earlier call to /// `Self::to_persisted`. pub fn proto_array_from_persisted( - persisted: &PersistedForkChoice, + persisted_proto_array: proto_array::core::SszContainer, + justified_balances: JustifiedBalances, reset_payload_statuses: ResetPayloadStatuses, spec: &ChainSpec, ) -> Result> { - let mut proto_array = ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes) - .map_err(Error::InvalidProtoArrayBytes)?; + let mut proto_array = ProtoArrayForkChoice::from_container( + persisted_proto_array.clone(), + justified_balances.clone(), + ) + .map_err(Error::InvalidProtoArrayBytes)?; let contains_invalid_payloads = proto_array.contains_invalid_payloads(); debug!( @@ -1408,7 +1450,7 @@ where info = "please report this error", "Failed to reset payload statuses" ); - ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes) + ProtoArrayForkChoice::from_container(persisted_proto_array, justified_balances) .map_err(Error::InvalidProtoArrayBytes) } else { debug!("Successfully reset all payload statuses"); @@ -1424,8 +1466,13 @@ where fc_store: T, spec: &ChainSpec, ) -> Result> { - let proto_array = - Self::proto_array_from_persisted(&persisted, reset_payload_statuses, spec)?; + let justified_balances = fc_store.justified_balances().clone(); + let proto_array = Self::proto_array_from_persisted( + persisted.proto_array, + justified_balances, + reset_payload_statuses, + spec, + )?; let current_slot = fc_store.get_current_slot(); @@ -1471,7 +1518,9 @@ where /// be instantiated again later. pub fn to_persisted(&self) -> PersistedForkChoice { PersistedForkChoice { - proto_array_bytes: self.proto_array().as_bytes(), + proto_array: self + .proto_array() + .as_ssz_container(self.justified_checkpoint(), self.finalized_checkpoint()), queued_attestations: self.queued_attestations().to_vec(), } } @@ -1485,10 +1534,46 @@ where /// Helper struct that is used to encode/decode the state of the `ForkChoice` as SSZ bytes. /// /// This is used when persisting the state of the fork choice to disk. -#[derive(Encode, Decode, Clone)] +#[superstruct( + variants(V17, V28), + variant_attributes(derive(Encode, Decode, Clone)), + no_enum +)] pub struct PersistedForkChoice { + #[superstruct(only(V17))] pub proto_array_bytes: Vec, - queued_attestations: Vec, + #[superstruct(only(V28))] + pub proto_array: proto_array::core::SszContainerV28, + pub queued_attestations: Vec, +} + +pub type PersistedForkChoice = PersistedForkChoiceV28; + +impl TryFrom for PersistedForkChoiceV28 { + type Error = ssz::DecodeError; + + fn try_from(v17: PersistedForkChoiceV17) -> Result { + let container_v17 = + proto_array::core::SszContainerV17::from_ssz_bytes(&v17.proto_array_bytes)?; + let container_v28 = container_v17.into(); + + Ok(Self { + proto_array: container_v28, + queued_attestations: v17.queued_attestations, + }) + } +} + +impl From<(PersistedForkChoiceV28, JustifiedBalances)> for PersistedForkChoiceV17 { + fn from((v28, balances): (PersistedForkChoiceV28, JustifiedBalances)) -> Self { + let container_v17 = proto_array::core::SszContainerV17::from((v28.proto_array, balances)); + let proto_array_bytes = container_v17.as_ssz_bytes(); + + Self { + proto_array_bytes, + queued_attestations: v28.queued_attestations, + } + } } #[cfg(test)] diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index 3953469cd8..7bbfb2e4de 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -44,6 +44,9 @@ pub trait ForkChoiceStore: Sized { /// Returns the `justified_checkpoint`. fn justified_checkpoint(&self) -> &Checkpoint; + /// Returns the state root of the justified checkpoint. + fn justified_state_root(&self) -> Hash256; + /// Returns balances from the `state` identified by `justified_checkpoint.root`. fn justified_balances(&self) -> &JustifiedBalances; @@ -53,6 +56,9 @@ pub trait ForkChoiceStore: Sized { /// Returns the `unrealized_justified_checkpoint`. fn unrealized_justified_checkpoint(&self) -> &Checkpoint; + /// Returns the state root of the unrealized justified checkpoint. + fn unrealized_justified_state_root(&self) -> Hash256; + /// Returns the `unrealized_finalized_checkpoint`. fn unrealized_finalized_checkpoint(&self) -> &Checkpoint; @@ -63,10 +69,14 @@ pub trait ForkChoiceStore: Sized { fn set_finalized_checkpoint(&mut self, checkpoint: Checkpoint); /// Sets the `justified_checkpoint`. - fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Self::Error>; + fn set_justified_checkpoint( + &mut self, + checkpoint: Checkpoint, + state_root: Hash256, + ) -> Result<(), Self::Error>; /// Sets the `unrealized_justified_checkpoint`. - fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint); + fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256); /// Sets the `unrealized_finalized_checkpoint`. fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint); diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 17f1dc38a6..afe06dee1b 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -5,7 +5,7 @@ mod metrics; pub use crate::fork_choice::{ AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice, - QueuedAttestation, ResetPayloadStatuses, + PersistedForkChoiceV17, PersistedForkChoiceV28, QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{ diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 95bdee574d..d3a84ee85b 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -7,6 +7,7 @@ use beacon_chain::{ BeaconChain, BeaconChainError, BeaconForkChoiceStore, ChainConfig, ForkChoiceError, StateSkipConfig, WhenSlotSkipped, }; +use fixed_bytes::FixedBytesExtended; use fork_choice::{ ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation, }; @@ -15,10 +16,11 @@ use std::fmt; use std::sync::Mutex; use std::time::Duration; use store::MemoryStore; +use types::SingleAttestation; use types::{ - test_utils::generate_deterministic_keypair, BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, - Epoch, EthSpec, FixedBytesExtended, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, - RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, + BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, ForkName, Hash256, + IndexedAttestation, MainnetEthSpec, RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, + test_utils::generate_deterministic_keypair, }; pub type E = MainnetEthSpec; @@ -463,10 +465,17 @@ impl ForkChoiceTest { ) .expect("should sign attestation"); + let single_attestation = SingleAttestation { + attester_index: validator_index as u64, + committee_index: validator_committee_index as u64, + data: attestation.data().clone(), + signature: attestation.signature().clone(), + }; + let mut verified_attestation = self .harness .chain - .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)) + .verify_unaggregated_attestation_for_gossip(&single_attestation, Some(subnet_id)) .expect("precondition: should gossip verify attestation"); if let MutationDelay::Blocks(slots) = delay { @@ -744,11 +753,11 @@ async fn invalid_attestation_empty_bitfield() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| match attestation { - IndexedAttestation::Base(ref mut att) => { - att.attesting_indices = vec![].into(); + IndexedAttestation::Base(att) => { + att.attesting_indices = vec![].try_into().unwrap(); } - IndexedAttestation::Electra(ref mut att) => { - att.attesting_indices = vec![].into(); + IndexedAttestation::Electra(att) => { + att.attesting_indices = vec![].try_into().unwrap(); } }, |result| { diff --git a/consensus/merkle_proof/Cargo.toml b/consensus/merkle_proof/Cargo.toml index 2f721d917b..5ba8a1b949 100644 --- a/consensus/merkle_proof/Cargo.toml +++ b/consensus/merkle_proof/Cargo.toml @@ -4,6 +4,9 @@ version = "0.2.0" authors = ["Michael Sproul "] edition = { workspace = true } +[features] +arbitrary = ["alloy-primitives/arbitrary"] + [dependencies] alloy-primitives = { workspace = true } ethereum_hashing = { workspace = true } @@ -11,8 +14,4 @@ fixed_bytes = { workspace = true } safe_arith = { workspace = true } [dev-dependencies] -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } - -[features] -arbitrary = ["alloy-primitives/arbitrary"] +proptest = { workspace = true } diff --git a/consensus/merkle_proof/src/lib.rs b/consensus/merkle_proof/src/lib.rs index 271e676df1..494c73d05c 100644 --- a/consensus/merkle_proof/src/lib.rs +++ b/consensus/merkle_proof/src/lib.rs @@ -1,4 +1,4 @@ -use ethereum_hashing::{hash, hash32_concat, ZERO_HASHES}; +use ethereum_hashing::{ZERO_HASHES, hash, hash32_concat}; use safe_arith::ArithError; use std::sync::LazyLock; @@ -113,13 +113,13 @@ impl MerkleTree { Zero(_) => { *self = MerkleTree::create(&[elem], depth); } - Node(ref mut hash, ref mut left, ref mut right) => { + Node(hash, left, right) => { let left: &mut MerkleTree = &mut *left; let right: &mut MerkleTree = &mut *right; match (&*left, &*right) { // Tree is full (Leaf(_), Leaf(_)) | (Finalized(_), Leaf(_)) => { - return Err(MerkleTreeError::MerkleTreeFull) + return Err(MerkleTreeError::MerkleTreeFull); } // There is a right node so insert in right node (Node(_, _, _), Node(_, _, _)) | (Finalized(_), Node(_, _, _)) => { @@ -413,50 +413,70 @@ impl From for MerkleTreeError { #[cfg(test)] mod tests { use super::*; - use quickcheck::TestResult; - use quickcheck_macros::quickcheck; - /// Check that we can: - /// 1. Build a MerkleTree from arbitrary leaves and an arbitrary depth. - /// 2. Generate valid proofs for all of the leaves of this MerkleTree. - #[quickcheck] - fn quickcheck_create_and_verify(int_leaves: Vec, depth: usize) -> TestResult { - if depth > MAX_TREE_DEPTH || int_leaves.len() > 2usize.pow(depth as u32) { - return TestResult::discard(); - } + use proptest::prelude::*; - let leaves: Vec<_> = int_leaves.into_iter().map(H256::from_low_u64_be).collect(); - let merkle_tree = MerkleTree::create(&leaves, depth); - let merkle_root = merkle_tree.hash(); + // Limit test depth to avoid generating huge trees. Depth 10 = 1024 max leaves. + const TEST_MAX_DEPTH: usize = 10; - let proofs_ok = (0..leaves.len()).all(|i| { - let (leaf, branch) = merkle_tree - .generate_proof(i, depth) - .expect("should generate proof"); - leaf == leaves[i] && verify_merkle_proof(leaf, &branch, depth, i, merkle_root) - }); - - TestResult::from_bool(proofs_ok) + fn merkle_leaves_strategy(max_depth: usize) -> impl Strategy, usize)> { + (0..=max_depth).prop_flat_map(|depth| { + let max_leaves = 2usize.pow(depth as u32); + ( + proptest::collection::vec(any::(), 0..=max_leaves), + Just(depth), + ) + }) } - #[quickcheck] - fn quickcheck_push_leaf_and_verify(int_leaves: Vec, depth: usize) -> TestResult { - if depth == 0 || depth > MAX_TREE_DEPTH || int_leaves.len() > 2usize.pow(depth as u32) { - return TestResult::discard(); + fn merkle_leaves_strategy_min_depth( + max_depth: usize, + min_depth: usize, + ) -> impl Strategy, usize)> { + (min_depth..=max_depth).prop_flat_map(|depth| { + let max_leaves = 2usize.pow(depth as u32); + ( + proptest::collection::vec(any::(), 0..=max_leaves), + Just(depth), + ) + }) + } + + proptest::proptest! { + /// Check that we can: + /// 1. Build a MerkleTree from arbitrary leaves and an arbitrary depth. + /// 2. Generate valid proofs for all of the leaves of this MerkleTree. + #[test] + fn proptest_create_and_verify((int_leaves, depth) in merkle_leaves_strategy(TEST_MAX_DEPTH)) { + let leaves: Vec<_> = int_leaves.into_iter().map(H256::from_low_u64_be).collect(); + let merkle_tree = MerkleTree::create(&leaves, depth); + let merkle_root = merkle_tree.hash(); + + let proofs_ok = (0..leaves.len()).all(|i| { + let (leaf, branch) = merkle_tree + .generate_proof(i, depth) + .expect("should generate proof"); + leaf == leaves[i] && verify_merkle_proof(leaf, &branch, depth, i, merkle_root) + }); + + proptest::prop_assert!(proofs_ok); } - let leaves_iter = int_leaves.into_iter().map(H256::from_low_u64_be); - let mut merkle_tree = MerkleTree::create(&[], depth); + #[test] + fn proptest_push_leaf_and_verify((int_leaves, depth) in merkle_leaves_strategy_min_depth(TEST_MAX_DEPTH, 1)) { + let leaves_iter = int_leaves.into_iter().map(H256::from_low_u64_be); + let mut merkle_tree = MerkleTree::create(&[], depth); - let proofs_ok = leaves_iter.enumerate().all(|(i, leaf)| { - assert_eq!(merkle_tree.push_leaf(leaf, depth), Ok(())); - let (stored_leaf, branch) = merkle_tree - .generate_proof(i, depth) - .expect("should generate proof"); - stored_leaf == leaf && verify_merkle_proof(leaf, &branch, depth, i, merkle_tree.hash()) - }); + let proofs_ok = leaves_iter.enumerate().all(|(i, leaf)| { + assert_eq!(merkle_tree.push_leaf(leaf, depth), Ok(())); + let (stored_leaf, branch) = merkle_tree + .generate_proof(i, depth) + .expect("should generate proof"); + stored_leaf == leaf && verify_merkle_proof(leaf, &branch, depth, i, merkle_tree.hash()) + }); - TestResult::from_bool(proofs_ok) + proptest::prop_assert!(proofs_ok); + } } #[test] diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index 293d6ab8f0..3f5bb3f4f5 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -11,6 +11,7 @@ path = "src/bin.rs" [dependencies] ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } safe_arith = { workspace = true } serde = { workspace = true } serde_yaml = { workspace = true } diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 42e7e0ac8b..6087bdd7ad 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -5,11 +5,12 @@ mod votes; use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice}; use crate::{InvalidationOperation, JustifiedBalances}; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, HashMap}; use types::{ - AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, FixedBytesExtended, - Hash256, MainnetEthSpec, Slot, + AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + MainnetEthSpec, Slot, }; pub use execution_status::*; @@ -213,7 +214,12 @@ impl ForkChoiceTestDefinition { unrealized_finalized_checkpoint: None, }; fork_choice - .process_block::(block, slot) + .process_block::( + block, + slot, + self.justified_checkpoint, + self.finalized_checkpoint, + ) .unwrap_or_else(|e| { panic!( "process_block op at index {} returned error: {:?}", @@ -273,7 +279,10 @@ impl ForkChoiceTestDefinition { } }; fork_choice - .process_execution_payload_invalidation::(&op) + .process_execution_payload_invalidation::( + &op, + self.finalized_checkpoint, + ) .unwrap() } Operation::AssertWeight { block_root, weight } => assert_eq!( @@ -306,9 +315,10 @@ fn get_checkpoint(i: u64) -> Checkpoint { } fn check_bytes_round_trip(original: &ProtoArrayForkChoice) { - let bytes = original.as_bytes(); - let decoded = - ProtoArrayForkChoice::from_bytes(&bytes).expect("fork choice should decode from bytes"); + // The checkpoint are ignored `ProtoArrayForkChoice::from_bytes` so any value is ok + let bytes = original.as_bytes(Checkpoint::default(), Checkpoint::default()); + let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone()) + .expect("fork choice should decode from bytes"); assert!( *original == decoded, "fork choice should encode and decode without change" diff --git a/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs b/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs index de84fbdd12..d20eaacb99 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/no_votes.rs @@ -1,4 +1,4 @@ -use types::FixedBytesExtended; +use fixed_bytes::FixedBytesExtended; use super::*; diff --git a/consensus/proto_array/src/lib.rs b/consensus/proto_array/src/lib.rs index b05a55e686..964e836d91 100644 --- a/consensus/proto_array/src/lib.rs +++ b/consensus/proto_array/src/lib.rs @@ -6,7 +6,7 @@ mod proto_array_fork_choice; mod ssz_container; pub use crate::justified_balances::JustifiedBalances; -pub use crate::proto_array::{calculate_committee_fraction, InvalidationOperation}; +pub use crate::proto_array::{InvalidationOperation, calculate_committee_fraction}; pub use crate::proto_array_fork_choice::{ Block, DisallowedReOrgOffsets, DoNotReOrg, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, @@ -16,5 +16,5 @@ pub use error::Error; pub mod core { pub use super::proto_array::{ProposerBoost, ProtoArray, ProtoNode}; pub use super::proto_array_fork_choice::VoteTracker; - pub use super::ssz_container::{SszContainer, SszContainerV17}; + pub use super::ssz_container::{SszContainer, SszContainerV17, SszContainerV28}; } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 0536e20f7a..135801ce33 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1,15 +1,16 @@ use crate::error::InvalidBestNodeInfo; -use crate::{error::Error, Block, ExecutionStatus, JustifiedBalances}; +use crate::{Block, ExecutionStatus, JustifiedBalances, error::Error}; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; -use ssz::four_byte_option_impl; use ssz::Encode; +use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use std::collections::{HashMap, HashSet}; use superstruct::superstruct; use tracing::info; use types::{ - AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, - FixedBytesExtended, Hash256, Slot, + AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + Slot, }; // Define a "legacy" implementation of `Option` which uses four bytes for encoding the union @@ -131,8 +132,6 @@ pub struct ProtoArray { /// Do not attempt to prune the tree unless it has at least this many nodes. Small prunes /// simply waste time. pub prune_threshold: usize, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, pub nodes: Vec, pub indices: HashMap, pub unsatisfied_inclusion_list_blocks: HashMap, @@ -157,8 +156,8 @@ impl ProtoArray { pub fn apply_score_changes( &mut self, mut deltas: Vec, - justified_checkpoint: Checkpoint, - finalized_checkpoint: Checkpoint, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, new_justified_balances: &JustifiedBalances, proposer_boost_root: Hash256, current_slot: Slot, @@ -171,13 +170,6 @@ impl ProtoArray { }); } - if justified_checkpoint != self.justified_checkpoint - || finalized_checkpoint != self.finalized_checkpoint - { - self.justified_checkpoint = justified_checkpoint; - self.finalized_checkpoint = finalized_checkpoint; - } - // Default the proposer boost score to zero. let mut proposer_score = 0; @@ -242,21 +234,18 @@ impl ProtoArray { // the delta by the new score amount (unless the block has an invalid execution status). // // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_latest_attesting_balance - if let Some(proposer_score_boost) = spec.proposer_score_boost { - if proposer_boost_root != Hash256::zero() + if let Some(proposer_score_boost) = spec.proposer_score_boost + && proposer_boost_root != Hash256::zero() && proposer_boost_root == node.root // Invalid nodes (or their ancestors) should not receive a proposer boost. && !execution_status_is_invalid - { - proposer_score = calculate_committee_fraction::( - new_justified_balances, - proposer_score_boost, - ) - .ok_or(Error::ProposerBoostOverflow(node_index))?; - node_delta = node_delta - .checked_add(proposer_score as i64) - .ok_or(Error::DeltaOverflow(node_index))?; - } + { + proposer_score = + calculate_committee_fraction::(new_justified_balances, proposer_score_boost) + .ok_or(Error::ProposerBoostOverflow(node_index))?; + node_delta = node_delta + .checked_add(proposer_score as i64) + .ok_or(Error::DeltaOverflow(node_index))?; } // Apply the delta to the node. @@ -318,6 +307,8 @@ impl ProtoArray { parent_index, node_index, current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, )?; } } @@ -328,7 +319,13 @@ impl ProtoArray { /// Register a block with the fork choice. /// /// It is only sane to supply a `None` parent for the genesis block. - pub fn on_block(&mut self, block: Block, current_slot: Slot) -> Result<(), Error> { + pub fn on_block( + &mut self, + block: Block, + current_slot: Slot, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, + ) -> Result<(), Error> { // If the block is already known, simply ignore it. if self.indices.contains_key(&block.root) { return Ok(()); @@ -379,6 +376,8 @@ impl ProtoArray { parent_index, node_index, current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, )?; if matches!(block.execution_status, ExecutionStatus::Valid(_)) { @@ -447,7 +446,7 @@ impl ProtoArray { return Err(Error::InvalidAncestorOfValidPayload { ancestor_block_root: node.root, ancestor_payload_block_hash, - }) + }); } }; @@ -461,6 +460,7 @@ impl ProtoArray { pub fn propagate_execution_payload_invalidation( &mut self, op: &InvalidationOperation, + best_finalized_checkpoint: Checkpoint, ) -> Result<(), Error> { let mut invalidated_indices: HashSet = <_>::default(); let head_block_root = op.block_root(); @@ -489,7 +489,10 @@ impl ProtoArray { let latest_valid_ancestor_is_descendant = latest_valid_ancestor_root.is_some_and(|ancestor_root| { self.is_descendant(ancestor_root, head_block_root) - && self.is_finalized_checkpoint_or_descendant::(ancestor_root) + && self.is_finalized_checkpoint_or_descendant::( + ancestor_root, + best_finalized_checkpoint, + ) }); // Collect all *ancestors* which were declared invalid since they reside between the @@ -556,7 +559,7 @@ impl ProtoArray { return Err(Error::ValidExecutionStatusBecameInvalid { block_root: node.root, payload_block_hash: *hash, - }) + }); } ExecutionStatus::Optimistic(hash) => { invalidated_indices.insert(index); @@ -613,27 +616,27 @@ impl ProtoArray { .get_mut(index) .ok_or(Error::InvalidNodeIndex(index))?; - if let Some(parent_index) = node.parent { - if invalidated_indices.contains(&parent_index) { - match &node.execution_status { - ExecutionStatus::Valid(hash) => { - return Err(Error::ValidExecutionStatusBecameInvalid { - block_root: node.root, - payload_block_hash: *hash, - }) - } - ExecutionStatus::Optimistic(hash) | ExecutionStatus::Invalid(hash) => { - node.execution_status = ExecutionStatus::Invalid(*hash) - } - ExecutionStatus::Irrelevant(_) => { - return Err(Error::IrrelevantDescendant { - block_root: node.root, - }) - } + if let Some(parent_index) = node.parent + && invalidated_indices.contains(&parent_index) + { + match &node.execution_status { + ExecutionStatus::Valid(hash) => { + return Err(Error::ValidExecutionStatusBecameInvalid { + block_root: node.root, + payload_block_hash: *hash, + }); + } + ExecutionStatus::Optimistic(hash) | ExecutionStatus::Invalid(hash) => { + node.execution_status = ExecutionStatus::Invalid(*hash) + } + ExecutionStatus::Irrelevant(_) => { + return Err(Error::IrrelevantDescendant { + block_root: node.root, + }); } - - invalidated_indices.insert(index); } + + invalidated_indices.insert(index); } } @@ -652,6 +655,8 @@ impl ProtoArray { &self, justified_root: &Hash256, current_slot: Slot, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, ) -> Result { let justified_index = self .indices @@ -685,12 +690,17 @@ impl ProtoArray { .ok_or(Error::InvalidBestDescendant(best_descendant_index))?; // Perform a sanity check that the node is indeed valid to be the head. - if !self.node_is_viable_for_head::(best_node, current_slot) { + if !self.node_is_viable_for_head::( + best_node, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + ) { return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo { current_slot, start_root: *justified_root, - justified_checkpoint: self.justified_checkpoint, - finalized_checkpoint: self.finalized_checkpoint, + justified_checkpoint: best_justified_checkpoint, + finalized_checkpoint: best_finalized_checkpoint, head_root: best_node.root, head_justified_checkpoint: best_node.justified_checkpoint, head_finalized_checkpoint: best_node.finalized_checkpoint, @@ -787,6 +797,8 @@ impl ProtoArray { parent_index: usize, child_index: usize, current_slot: Slot, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, ) -> Result<(), Error> { let child = self .nodes @@ -798,8 +810,12 @@ impl ProtoArray { .get(parent_index) .ok_or(Error::InvalidNodeIndex(parent_index))?; - let child_leads_to_viable_head = - self.node_leads_to_viable_head::(child, current_slot)?; + let child_leads_to_viable_head = self.node_leads_to_viable_head::( + child, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + )?; // These three variables are aliases to the three options that we may set the // `parent.best_child` and `parent.best_descendant` to. @@ -828,8 +844,12 @@ impl ProtoArray { .get(best_child_index) .ok_or(Error::InvalidBestDescendant(best_child_index))?; - let best_child_leads_to_viable_head = - self.node_leads_to_viable_head::(best_child, current_slot)?; + let best_child_leads_to_viable_head = self.node_leads_to_viable_head::( + best_child, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + )?; if child_leads_to_viable_head && !best_child_leads_to_viable_head { // The child leads to a viable head, but the current best-child doesn't. @@ -878,6 +898,8 @@ impl ProtoArray { &self, node: &ProtoNode, current_slot: Slot, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, ) -> Result { let best_descendant_is_viable_for_head = if let Some(best_descendant_index) = node.best_descendant { @@ -886,13 +908,23 @@ impl ProtoArray { .get(best_descendant_index) .ok_or(Error::InvalidBestDescendant(best_descendant_index))?; - self.node_is_viable_for_head::(best_descendant, current_slot) + self.node_is_viable_for_head::( + best_descendant, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + ) } else { false }; Ok(best_descendant_is_viable_for_head - || self.node_is_viable_for_head::(node, current_slot)) + || self.node_is_viable_for_head::( + node, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + )) } /// This is the equivalent to the `filter_block_tree` function in the eth2 spec: @@ -901,7 +933,13 @@ impl ProtoArray { /// /// Any node that has a different finalized or justified epoch should not be viable for the /// head. - fn node_is_viable_for_head(&self, node: &ProtoNode, current_slot: Slot) -> bool { + fn node_is_viable_for_head( + &self, + node: &ProtoNode, + current_slot: Slot, + best_justified_checkpoint: Checkpoint, + best_finalized_checkpoint: Checkpoint, + ) -> bool { if node.execution_status.is_invalid() { return false; } @@ -938,12 +976,13 @@ impl ProtoArray { node_justified_checkpoint }; - let correct_justified = self.justified_checkpoint.epoch == genesis_epoch - || voting_source.epoch == self.justified_checkpoint.epoch + let correct_justified = best_justified_checkpoint.epoch == genesis_epoch + || voting_source.epoch == best_justified_checkpoint.epoch || voting_source.epoch + 2 >= current_epoch; - let correct_finalized = self.finalized_checkpoint.epoch == genesis_epoch - || self.is_finalized_checkpoint_or_descendant::(node.root); + let correct_finalized = best_finalized_checkpoint.epoch == genesis_epoch + || self + .is_finalized_checkpoint_or_descendant::(node.root, best_finalized_checkpoint); correct_justified && correct_finalized } @@ -998,10 +1037,13 @@ impl ProtoArray { /// /// Notably, this function is checking ancestory of the finalized /// *checkpoint* not the finalized *block*. - pub fn is_finalized_checkpoint_or_descendant(&self, root: Hash256) -> bool { - let finalized_root = self.finalized_checkpoint.root; - let finalized_slot = self - .finalized_checkpoint + pub fn is_finalized_checkpoint_or_descendant( + &self, + root: Hash256, + best_finalized_checkpoint: Checkpoint, + ) -> bool { + let finalized_root = best_finalized_checkpoint.root; + let finalized_slot = best_finalized_checkpoint .epoch .start_slot(E::slots_per_epoch()); @@ -1024,7 +1066,7 @@ impl ProtoArray { // If the conditions don't match for this node then they're unlikely to // start matching for its ancestors. for checkpoint in &[node.finalized_checkpoint, node.justified_checkpoint] { - if checkpoint == &self.finalized_checkpoint { + if checkpoint == &best_finalized_checkpoint { return true; } } @@ -1033,7 +1075,7 @@ impl ProtoArray { node.unrealized_finalized_checkpoint, node.unrealized_justified_checkpoint, ] { - if checkpoint.is_some_and(|cp| cp == self.finalized_checkpoint) { + if checkpoint.is_some_and(|cp| cp == best_finalized_checkpoint) { return true; } } @@ -1081,12 +1123,18 @@ impl ProtoArray { /// For informational purposes like the beacon HTTP API, we use this as the list of known heads, /// even though some of them might not be viable. We do this to maintain consistency between the /// definition of "head" used by pruning (which does not consider viability) and fork choice. - pub fn heads_descended_from_finalization(&self) -> Vec<&ProtoNode> { + pub fn heads_descended_from_finalization( + &self, + best_finalized_checkpoint: Checkpoint, + ) -> Vec<&ProtoNode> { self.nodes .iter() .filter(|node| { node.best_child.is_none() - && self.is_finalized_checkpoint_or_descendant::(node.root) + && self.is_finalized_checkpoint_or_descendant::( + node.root, + best_finalized_checkpoint, + ) }) .collect() } diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index d3be3f967e..fad7d34615 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -1,12 +1,13 @@ use crate::{ + JustifiedBalances, error::Error, proto_array::{ - calculate_committee_fraction, InvalidationOperation, Iter, ProposerBoost, ProtoArray, - ProtoNode, + InvalidationOperation, Iter, ProposerBoost, ProtoArray, ProtoNode, + calculate_committee_fraction, }, ssz_container::SszContainer, - JustifiedBalances, }; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -15,8 +16,8 @@ use std::{ fmt, }; use types::{ - AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, - FixedBytesExtended, Hash256, Slot, + AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + Slot, }; pub const DEFAULT_PRUNE_THRESHOLD: usize = 256; @@ -160,6 +161,56 @@ pub struct Block { pub unrealized_finalized_checkpoint: Option, } +impl Block { + /// Compute the proposer shuffling decision root of a child block in `child_block_epoch`. + /// + /// This function assumes that `child_block_epoch >= self.epoch`. It is the responsibility of + /// the caller to check this condition, or else incorrect results will be produced. + pub fn proposer_shuffling_root_for_child_block( + &self, + child_block_epoch: Epoch, + spec: &ChainSpec, + ) -> Hash256 { + let block_epoch = self.current_epoch_shuffling_id.shuffling_epoch; + + // For child blocks in the Fulu fork epoch itself, we want to use the old logic. There is no + // lookahead in the first Fulu epoch. So we check whether Fulu is enabled at + // `child_block_epoch - 1`, i.e. whether `child_block_epoch > fulu_fork_epoch`. + if !spec + .fork_name_at_epoch(child_block_epoch.saturating_sub(1_u64)) + .fulu_enabled() + { + // Prior to Fulu the proposer shuffling decision root for the current epoch is the same + // as the attestation shuffling for the *next* epoch, i.e. it is determined at the start + // of the current epoch. + if block_epoch == child_block_epoch { + self.next_epoch_shuffling_id.shuffling_decision_block + } else { + // Otherwise, the child block epoch is greater, so its decision root is its parent + // root itself (this block's root). + self.root + } + } else { + // After Fulu the proposer shuffling is determined with lookahead, so if the block + // lies in the same epoch as its parent, its decision root is the same as the + // parent's current epoch attester shuffling + // + // i.e. the block from the end of epoch N - 2. + if child_block_epoch == block_epoch { + self.current_epoch_shuffling_id.shuffling_decision_block + } else if child_block_epoch == block_epoch + 1 { + // If the block is the next epoch, then it instead shares its decision root with + // the parent's *next epoch* attester shuffling. + self.next_epoch_shuffling_id.shuffling_decision_block + } else { + // The child block lies in the future beyond the lookahead, at the point where this + // block (its parent) will be the decision block. + self.root + } + } + } +} + /// A Vec-wrapper which will grow to match any request. /// /// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using @@ -375,8 +426,6 @@ impl ProtoArrayForkChoice { ) -> Result { let mut proto_array = ProtoArray { prune_threshold: DEFAULT_PRUNE_THRESHOLD, - justified_checkpoint, - finalized_checkpoint, nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), unsatisfied_inclusion_list_blocks, @@ -401,7 +450,12 @@ impl ProtoArrayForkChoice { }; proto_array - .on_block::(block, current_slot) + .on_block::( + block, + current_slot, + justified_checkpoint, + finalized_checkpoint, + ) .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; Ok(Self { @@ -425,9 +479,10 @@ impl ProtoArrayForkChoice { pub fn process_execution_payload_invalidation( &mut self, op: &InvalidationOperation, + finalized_checkpoint: Checkpoint, ) -> Result<(), String> { self.proto_array - .propagate_execution_payload_invalidation::(op) + .propagate_execution_payload_invalidation::(op, finalized_checkpoint) .map_err(|e| format!("Failed to process invalid payload: {:?}", e)) } @@ -451,13 +506,20 @@ impl ProtoArrayForkChoice { &mut self, block: Block, current_slot: Slot, + justified_checkpoint: Checkpoint, + finalized_checkpoint: Checkpoint, ) -> Result<(), String> { if block.parent_root.is_none() { return Err("Missing parent root".to_string()); } self.proto_array - .on_block::(block, current_slot) + .on_block::( + block, + current_slot, + justified_checkpoint, + finalized_checkpoint, + ) .map_err(|e| format!("process_block_error: {:?}", e)) } @@ -499,7 +561,12 @@ impl ProtoArrayForkChoice { *old_balances = new_balances.clone(); self.proto_array - .find_head::(&justified_checkpoint.root, current_slot) + .find_head::( + &justified_checkpoint.root, + current_slot, + justified_checkpoint, + finalized_checkpoint, + ) .map_err(|e| format!("find_head failed: {:?}", e)) } @@ -707,24 +774,22 @@ impl ProtoArrayForkChoice { // If the invalid root was boosted, apply the weight to it and // ancestors. - if let Some(proposer_score_boost) = spec.proposer_score_boost { - if self.proto_array.previous_proposer_boost.root == node.root { - // Compute the score based upon the current balances. We can't rely on - // the `previous_proposr_boost.score` since it is set to zero with an - // invalid node. - let proposer_score = calculate_committee_fraction::( - &self.balances, - proposer_score_boost, - ) - .ok_or("Failed to compute proposer boost")?; - // Store the score we've applied here so it can be removed in - // a later call to `apply_score_changes`. - self.proto_array.previous_proposer_boost.score = proposer_score; - // Apply this boost to this node. - restored_weight = restored_weight - .checked_add(proposer_score) - .ok_or("Overflow when adding boost to weight")?; - } + if let Some(proposer_score_boost) = spec.proposer_score_boost + && self.proto_array.previous_proposer_boost.root == node.root + { + // Compute the score based upon the current balances. We can't rely on + // the `previous_proposr_boost.score` since it is set to zero with an + // invalid node. + let proposer_score = + calculate_committee_fraction::(&self.balances, proposer_score_boost) + .ok_or("Failed to compute proposer boost")?; + // Store the score we've applied here so it can be removed in + // a later call to `apply_score_changes`. + self.proto_array.previous_proposer_boost.score = proposer_score; + // Apply this boost to this node. + restored_weight = restored_weight + .checked_add(proposer_score) + .ok_or("Overflow when adding boost to weight")?; } // Add the restored weight to the node and all ancestors. @@ -838,9 +903,10 @@ impl ProtoArrayForkChoice { pub fn is_finalized_checkpoint_or_descendant( &self, descendant_root: Hash256, + best_finalized_checkpoint: Checkpoint, ) -> bool { self.proto_array - .is_finalized_checkpoint_or_descendant::(descendant_root) + .is_finalized_checkpoint_or_descendant::(descendant_root, best_finalized_checkpoint) } pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> { @@ -858,7 +924,7 @@ impl ProtoArrayForkChoice { } /// See `ProtoArray::iter_nodes` - pub fn iter_nodes(&self, block_root: &Hash256) -> Iter { + pub fn iter_nodes(&self, block_root: &Hash256) -> Iter<'_> { self.proto_array.iter_nodes(block_root) } @@ -866,18 +932,38 @@ impl ProtoArrayForkChoice { pub fn iter_block_roots( &self, block_root: &Hash256, - ) -> impl Iterator + use<'_> { + ) -> impl Iterator + '_ { self.proto_array.iter_block_roots(block_root) } - pub fn as_bytes(&self) -> Vec { - SszContainer::from(self).as_ssz_bytes() + pub fn as_ssz_container( + &self, + justified_checkpoint: Checkpoint, + finalized_checkpoint: Checkpoint, + ) -> SszContainer { + SszContainer::from_proto_array(self, justified_checkpoint, finalized_checkpoint) } - pub fn from_bytes(bytes: &[u8]) -> Result { + pub fn as_bytes( + &self, + justified_checkpoint: Checkpoint, + finalized_checkpoint: Checkpoint, + ) -> Vec { + self.as_ssz_container(justified_checkpoint, finalized_checkpoint) + .as_ssz_bytes() + } + + pub fn from_bytes(bytes: &[u8], balances: JustifiedBalances) -> Result { let container = SszContainer::from_ssz_bytes(bytes) .map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))?; - container + Self::from_container(container, balances) + } + + pub fn from_container( + container: SszContainer, + balances: JustifiedBalances, + ) -> Result { + (container, balances) .try_into() .map_err(|e| format!("Failed to initialize ProtoArrayForkChoice: {e:?}")) } @@ -897,8 +983,12 @@ impl ProtoArrayForkChoice { } /// Returns all nodes that have zero children and are descended from the finalized checkpoint. - pub fn heads_descended_from_finalization(&self) -> Vec<&ProtoNode> { - self.proto_array.heads_descended_from_finalization::() + pub fn heads_descended_from_finalization( + &self, + best_finalized_checkpoint: Checkpoint, + ) -> Vec<&ProtoNode> { + self.proto_array + .heads_descended_from_finalization::(best_finalized_checkpoint) } } @@ -1008,7 +1098,8 @@ fn compute_deltas( #[cfg(test)] mod test_compute_deltas { use super::*; - use types::{FixedBytesExtended, MainnetEthSpec}; + use fixed_bytes::FixedBytesExtended; + use types::MainnetEthSpec; /// Gives a hash that is not the zero hash (unless i is `usize::MAX)`. fn hash_from_index(i: usize) -> Hash256 { @@ -1069,6 +1160,8 @@ mod test_compute_deltas { unrealized_finalized_checkpoint: Some(genesis_checkpoint), }, genesis_slot + 1, + genesis_checkpoint, + genesis_checkpoint, ) .unwrap(); @@ -1092,6 +1185,8 @@ mod test_compute_deltas { unrealized_finalized_checkpoint: None, }, genesis_slot + 1, + genesis_checkpoint, + genesis_checkpoint, ) .unwrap(); @@ -1105,10 +1200,24 @@ mod test_compute_deltas { assert!(!fc.is_descendant(finalized_root, not_finalized_desc)); assert!(!fc.is_descendant(finalized_root, unknown)); - assert!(fc.is_finalized_checkpoint_or_descendant::(finalized_root)); - assert!(fc.is_finalized_checkpoint_or_descendant::(finalized_desc)); - assert!(!fc.is_finalized_checkpoint_or_descendant::(not_finalized_desc)); - assert!(!fc.is_finalized_checkpoint_or_descendant::(unknown)); + assert!(fc.is_finalized_checkpoint_or_descendant::( + finalized_root, + genesis_checkpoint + )); + assert!(fc.is_finalized_checkpoint_or_descendant::( + finalized_desc, + genesis_checkpoint + )); + assert!(!fc.is_finalized_checkpoint_or_descendant::( + not_finalized_desc, + genesis_checkpoint + )); + assert!( + !fc.is_finalized_checkpoint_or_descendant::( + unknown, + genesis_checkpoint + ) + ); assert!(!fc.is_descendant(finalized_desc, not_finalized_desc)); assert!(fc.is_descendant(finalized_desc, finalized_desc)); @@ -1205,6 +1314,8 @@ mod test_compute_deltas { unrealized_finalized_checkpoint: Some(genesis_checkpoint), }, Slot::from(block.slot), + genesis_checkpoint, + genesis_checkpoint, ) .unwrap(); }; @@ -1259,29 +1370,34 @@ mod test_compute_deltas { // Set the finalized checkpoint to finalize the first slot of epoch 1 on // the canonical chain. - fc.proto_array.finalized_checkpoint = Checkpoint { + let finalized_checkpoint = Checkpoint { root: finalized_root, epoch: Epoch::new(1), }; assert!( fc.proto_array - .is_finalized_checkpoint_or_descendant::(finalized_root), + .is_finalized_checkpoint_or_descendant::( + finalized_root, + finalized_checkpoint + ), "the finalized checkpoint is the finalized checkpoint" ); assert!( fc.proto_array - .is_finalized_checkpoint_or_descendant::(get_block_root( - canonical_slot - )), + .is_finalized_checkpoint_or_descendant::( + get_block_root(canonical_slot), + finalized_checkpoint + ), "the canonical block is a descendant of the finalized checkpoint" ); assert!( !fc.proto_array - .is_finalized_checkpoint_or_descendant::(get_block_root( - non_canonical_slot - )), + .is_finalized_checkpoint_or_descendant::( + get_block_root(non_canonical_slot), + finalized_checkpoint + ), "although the non-canonical block is a descendant of the finalized block, \ it's not a descendant of the finalized checkpoint" ); diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index e6ffc2ac04..63badf2112 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -1,10 +1,10 @@ use crate::proto_array::ProposerBoost; use crate::{ + Error, JustifiedBalances, proto_array::{ProtoArray, ProtoNodeV17}, proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker}, - Error, JustifiedBalances, }; -use ssz::{four_byte_option_impl, Encode}; +use ssz::{Encode, four_byte_option_impl}; use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use superstruct::superstruct; @@ -14,32 +14,41 @@ use types::{Checkpoint, Hash256, Slot}; // selector. four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint); -pub type SszContainer = SszContainerV17; +pub type SszContainer = SszContainerV28; -#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)] +#[superstruct( + variants(V17, V28), + variant_attributes(derive(Encode, Decode, Clone)), + no_enum +)] pub struct SszContainer { pub votes: Vec, + #[superstruct(only(V17))] pub balances: Vec, pub prune_threshold: usize, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - #[superstruct(only(V17))] + // Deprecated, remove in a future schema migration + justified_checkpoint: Checkpoint, + // Deprecated, remove in a future schema migration + finalized_checkpoint: Checkpoint, pub nodes: Vec, pub indices: Vec<(Hash256, usize)>, pub previous_proposer_boost: ProposerBoost, pub unsatisfied_inclusion_list_blocks: Vec<(Slot, Hash256)>, } -impl From<&ProtoArrayForkChoice> for SszContainer { - fn from(from: &ProtoArrayForkChoice) -> Self { +impl SszContainer { + pub fn from_proto_array( + from: &ProtoArrayForkChoice, + justified_checkpoint: Checkpoint, + finalized_checkpoint: Checkpoint, + ) -> Self { let proto_array = &from.proto_array; Self { votes: from.votes.0.clone(), - balances: from.balances.effective_balances.clone(), prune_threshold: proto_array.prune_threshold, - justified_checkpoint: proto_array.justified_checkpoint, - finalized_checkpoint: proto_array.finalized_checkpoint, + justified_checkpoint, + finalized_checkpoint, nodes: proto_array.nodes.clone(), indices: proto_array.indices.iter().map(|(k, v)| (*k, *v)).collect(), previous_proposer_boost: proto_array.previous_proposer_boost, @@ -52,14 +61,12 @@ impl From<&ProtoArrayForkChoice> for SszContainer { } } -impl TryFrom for ProtoArrayForkChoice { +impl TryFrom<(SszContainer, JustifiedBalances)> for ProtoArrayForkChoice { type Error = Error; - fn try_from(from: SszContainer) -> Result { + fn try_from((from, balances): (SszContainer, JustifiedBalances)) -> Result { let proto_array = ProtoArray { prune_threshold: from.prune_threshold, - justified_checkpoint: from.justified_checkpoint, - finalized_checkpoint: from.finalized_checkpoint, nodes: from.nodes, indices: from.indices.into_iter().collect::>(), previous_proposer_boost: from.previous_proposer_boost, @@ -72,7 +79,40 @@ impl TryFrom for ProtoArrayForkChoice { Ok(Self { proto_array, votes: ElasticList(from.votes), - balances: JustifiedBalances::from_effective_balances(from.balances)?, + balances, }) } } + +// Convert V17 to V28 by dropping balances. +impl From for SszContainerV28 { + fn from(v17: SszContainerV17) -> Self { + Self { + votes: v17.votes, + prune_threshold: v17.prune_threshold, + justified_checkpoint: v17.justified_checkpoint, + finalized_checkpoint: v17.finalized_checkpoint, + nodes: v17.nodes, + indices: v17.indices, + previous_proposer_boost: v17.previous_proposer_boost, + unsatisfied_inclusion_list_blocks: v17.unsatisfied_inclusion_list_blocks, + } + } +} + +// Convert V28 to V17 by re-adding balances. +impl From<(SszContainerV28, JustifiedBalances)> for SszContainerV17 { + fn from((v28, balances): (SszContainerV28, JustifiedBalances)) -> Self { + Self { + votes: v28.votes, + balances: balances.effective_balances.clone(), + prune_threshold: v28.prune_threshold, + justified_checkpoint: v28.justified_checkpoint, + finalized_checkpoint: v28.finalized_checkpoint, + nodes: v28.nodes, + indices: v28.indices, + previous_proposer_boost: v28.previous_proposer_boost, + unsatisfied_inclusion_list_blocks: v28.unsatisfied_inclusion_list_blocks, + } + } +} diff --git a/consensus/safe_arith/Cargo.toml b/consensus/safe_arith/Cargo.toml deleted file mode 100644 index 9ac9fe28d3..0000000000 --- a/consensus/safe_arith/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "safe_arith" -version = "0.1.0" -authors = ["Michael Sproul "] -edition = { workspace = true } -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/consensus/safe_arith/src/iter.rs b/consensus/safe_arith/src/iter.rs deleted file mode 100644 index d5ee51b588..0000000000 --- a/consensus/safe_arith/src/iter.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{Result, SafeArith}; - -/// Extension trait for iterators, providing a safe replacement for `sum`. -pub trait SafeArithIter { - fn safe_sum(self) -> Result; -} - -impl SafeArithIter for I -where - I: Iterator + Sized, - T: SafeArith, -{ - fn safe_sum(mut self) -> Result { - self.try_fold(T::ZERO, |acc, x| acc.safe_add(x)) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::ArithError; - - #[test] - fn empty_sum() { - let v: Vec = vec![]; - assert_eq!(v.into_iter().safe_sum(), Ok(0)); - } - - #[test] - fn unsigned_sum_small() { - let arr = [400u64, 401, 402, 403, 404, 405, 406]; - assert_eq!( - arr.iter().copied().safe_sum().unwrap(), - arr.iter().copied().sum() - ); - } - - #[test] - fn unsigned_sum_overflow() { - let v = vec![u64::MAX, 1]; - assert_eq!(v.into_iter().safe_sum(), Err(ArithError::Overflow)); - } - - #[test] - fn signed_sum_small() { - let v = vec![-1i64, -2i64, -3i64, 3, 2, 1]; - assert_eq!(v.into_iter().safe_sum(), Ok(0)); - } - - #[test] - fn signed_sum_overflow_above() { - let v = vec![1, 2, 3, 4, i16::MAX, 0, 1, 2, 3]; - assert_eq!(v.into_iter().safe_sum(), Err(ArithError::Overflow)); - } - - #[test] - fn signed_sum_overflow_below() { - let v = vec![i16::MIN, -1]; - assert_eq!(v.into_iter().safe_sum(), Err(ArithError::Overflow)); - } - - #[test] - fn signed_sum_almost_overflow() { - let arr = [i64::MIN, 1, -1i64, i64::MAX, i64::MAX, 1]; - assert_eq!( - arr.iter().copied().safe_sum().unwrap(), - arr.iter().copied().sum() - ); - } -} diff --git a/consensus/safe_arith/src/lib.rs b/consensus/safe_arith/src/lib.rs deleted file mode 100644 index aa397c0603..0000000000 --- a/consensus/safe_arith/src/lib.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Library for safe arithmetic on integers, avoiding overflow and division by zero. -mod iter; - -pub use iter::SafeArithIter; - -/// Error representing the failure of an arithmetic operation. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum ArithError { - Overflow, - DivisionByZero, -} - -pub type Result = std::result::Result; - -macro_rules! assign_method { - ($name:ident, $op:ident, $doc_op:expr) => { - assign_method!($name, $op, Self, $doc_op); - }; - ($name:ident, $op:ident, $rhs_ty:ty, $doc_op:expr) => { - #[doc = "Safe variant of `"] - #[doc = $doc_op] - #[doc = "`."] - #[inline] - fn $name(&mut self, other: $rhs_ty) -> Result<()> { - *self = self.$op(other)?; - Ok(()) - } - }; -} - -/// Trait providing safe arithmetic operations for built-in types. -pub trait SafeArith: Sized + Copy { - const ZERO: Self; - const ONE: Self; - - /// Safe variant of `+` that guards against overflow. - fn safe_add(&self, other: Rhs) -> Result; - - /// Safe variant of `-` that guards against overflow. - fn safe_sub(&self, other: Rhs) -> Result; - - /// Safe variant of `*` that guards against overflow. - fn safe_mul(&self, other: Rhs) -> Result; - - /// Safe variant of `/` that guards against division by 0. - fn safe_div(&self, other: Rhs) -> Result; - - /// Safe variant of `%` that guards against division by 0. - fn safe_rem(&self, other: Rhs) -> Result; - - /// Safe variant of `<<` that guards against overflow. - fn safe_shl(&self, other: u32) -> Result; - - /// Safe variant of `>>` that guards against overflow. - fn safe_shr(&self, other: u32) -> Result; - - assign_method!(safe_add_assign, safe_add, Rhs, "+="); - assign_method!(safe_sub_assign, safe_sub, Rhs, "-="); - assign_method!(safe_mul_assign, safe_mul, Rhs, "*="); - assign_method!(safe_div_assign, safe_div, Rhs, "/="); - assign_method!(safe_rem_assign, safe_rem, Rhs, "%="); - assign_method!(safe_shl_assign, safe_shl, u32, "<<="); - assign_method!(safe_shr_assign, safe_shr, u32, ">>="); -} - -macro_rules! impl_safe_arith { - ($typ:ty) => { - impl SafeArith for $typ { - const ZERO: Self = 0; - const ONE: Self = 1; - - #[inline] - fn safe_add(&self, other: Self) -> Result { - self.checked_add(other).ok_or(ArithError::Overflow) - } - - #[inline] - fn safe_sub(&self, other: Self) -> Result { - self.checked_sub(other).ok_or(ArithError::Overflow) - } - - #[inline] - fn safe_mul(&self, other: Self) -> Result { - self.checked_mul(other).ok_or(ArithError::Overflow) - } - - #[inline] - fn safe_div(&self, other: Self) -> Result { - self.checked_div(other).ok_or(ArithError::DivisionByZero) - } - - #[inline] - fn safe_rem(&self, other: Self) -> Result { - self.checked_rem(other).ok_or(ArithError::DivisionByZero) - } - - #[inline] - fn safe_shl(&self, other: u32) -> Result { - self.checked_shl(other).ok_or(ArithError::Overflow) - } - - #[inline] - fn safe_shr(&self, other: u32) -> Result { - self.checked_shr(other).ok_or(ArithError::Overflow) - } - } - }; -} - -impl_safe_arith!(u8); -impl_safe_arith!(u16); -impl_safe_arith!(u32); -impl_safe_arith!(u64); -impl_safe_arith!(usize); -impl_safe_arith!(i8); -impl_safe_arith!(i16); -impl_safe_arith!(i32); -impl_safe_arith!(i64); -impl_safe_arith!(isize); - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn basic() { - let x = 10u32; - let y = 11; - assert_eq!(x.safe_add(y), Ok(x + y)); - assert_eq!(y.safe_sub(x), Ok(y - x)); - assert_eq!(x.safe_mul(y), Ok(x * y)); - assert_eq!(x.safe_div(y), Ok(x / y)); - assert_eq!(x.safe_rem(y), Ok(x % y)); - - assert_eq!(x.safe_shl(1), Ok(x << 1)); - assert_eq!(x.safe_shr(1), Ok(x >> 1)); - } - - #[test] - fn mutate() { - let mut x = 0u8; - x.safe_add_assign(2).unwrap(); - assert_eq!(x, 2); - x.safe_sub_assign(1).unwrap(); - assert_eq!(x, 1); - x.safe_shl_assign(1).unwrap(); - assert_eq!(x, 2); - x.safe_mul_assign(3).unwrap(); - assert_eq!(x, 6); - x.safe_div_assign(4).unwrap(); - assert_eq!(x, 1); - x.safe_shr_assign(1).unwrap(); - assert_eq!(x, 0); - } - - #[test] - fn errors() { - assert!(u32::MAX.safe_add(1).is_err()); - assert!(u32::MIN.safe_sub(1).is_err()); - assert!(u32::MAX.safe_mul(2).is_err()); - assert!(u32::MAX.safe_div(0).is_err()); - assert!(u32::MAX.safe_rem(0).is_err()); - assert!(u32::MAX.safe_shl(32).is_err()); - assert!(u32::MAX.safe_shr(32).is_err()); - } -} diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 502ffe3cf6..a08035d583 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -4,41 +4,44 @@ version = "0.2.0" authors = ["Paul Hauner ", "Michael Sproul "] edition = { workspace = true } -[dev-dependencies] -beacon_chain = { workspace = true } -env_logger = { workspace = true } -tokio = { workspace = true } +[features] +default = ["legacy-arith"] +fake_crypto = ["bls/fake_crypto"] +legacy-arith = ["types/legacy-arith"] +arbitrary-fuzz = [ + "types/arbitrary-fuzz", + "merkle_proof/arbitrary", + "ethereum_ssz/arbitrary", + "ssz_types/arbitrary", + "tree_hash/arbitrary", +] +portable = ["bls/supranational-portable"] [dependencies] arbitrary = { workspace = true } bls = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } ethereum_hashing = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +fixed_bytes = { workspace = true } int_to_bytes = { workspace = true } integer-sqrt = "0.1.5" itertools = { workspace = true } merkle_proof = { workspace = true } metrics = { workspace = true } +milhouse = { workspace = true } rand = { workspace = true } rayon = { workspace = true } safe_arith = { workspace = true } smallvec = { workspace = true } ssz_types = { workspace = true } test_random_derive = { path = "../../common/test_random_derive" } +tracing = { workspace = true } tree_hash = { workspace = true } +typenum = { workspace = true } types = { workspace = true } -[features] -default = ["legacy-arith"] -fake_crypto = ["bls/fake_crypto"] -legacy-arith = ["types/legacy-arith"] -arbitrary-fuzz = [ - "types/arbitrary-fuzz", - "merkle_proof/arbitrary", - "ethereum_ssz/arbitrary", - "ssz_types/arbitrary", - "tree_hash/arbitrary", -] -portable = ["bls/supranational-portable"] +[dev-dependencies] +beacon_chain = { workspace = true } +tokio = { workspace = true } diff --git a/consensus/state_processing/src/all_caches.rs b/consensus/state_processing/src/all_caches.rs index e49eb395c4..0381bb820f 100644 --- a/consensus/state_processing/src/all_caches.rs +++ b/consensus/state_processing/src/all_caches.rs @@ -1,8 +1,7 @@ use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use crate::epoch_cache::initialize_epoch_cache; -use types::{ - BeaconState, ChainSpec, EpochCacheError, EthSpec, FixedBytesExtended, Hash256, RelativeEpoch, -}; +use tracing::instrument; +use types::{BeaconState, ChainSpec, EpochCacheError, EthSpec, Hash256, RelativeEpoch}; /// Mixin trait for the beacon state that provides operations on *all* caches. /// @@ -23,6 +22,7 @@ pub trait AllCaches { } impl AllCaches for BeaconState { + #[instrument(skip_all)] fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), EpochCacheError> { self.build_caches(spec)?; initialize_epoch_cache(self, spec)?; @@ -32,8 +32,7 @@ impl AllCaches for BeaconState { fn all_caches_built(&self) -> bool { let current_epoch = self.current_epoch(); - let Ok(epoch_cache_decision_block_root) = - self.proposer_shuffling_decision_root(Hash256::zero()) + let Ok(epoch_cache_decision_block_root) = self.epoch_cache_decision_root(Hash256::ZERO) else { return false; }; diff --git a/consensus/state_processing/src/block_replayer.rs b/consensus/state_processing/src/block_replayer.rs index 0cdb2a2bed..56e667cdd3 100644 --- a/consensus/state_processing/src/block_replayer.rs +++ b/consensus/state_processing/src/block_replayer.rs @@ -1,7 +1,7 @@ use crate::{ - per_block_processing, per_epoch_processing::EpochProcessingSummary, per_slot_processing, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, - VerifyBlockRoot, + VerifyBlockRoot, per_block_processing, per_epoch_processing::EpochProcessingSummary, + per_slot_processing, }; use itertools::Itertools; use std::iter::Peekable; @@ -193,12 +193,11 @@ where } // Otherwise try to source a root from the previous block. - if let Some(prev_i) = i.checked_sub(1) { - if let Some(prev_block) = blocks.get(prev_i) { - if prev_block.slot() == slot { - return Ok(prev_block.state_root()); - } - } + if let Some(prev_i) = i.checked_sub(1) + && let Some(prev_block) = blocks.get(prev_i) + && prev_block.slot() == slot + { + return Ok(prev_block.state_root()); } self.state_root_miss = true; diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 2c6fd3b215..71bf6329f1 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -1,13 +1,13 @@ use integer_sqrt::IntegerSquareRoot; use smallvec::SmallVec; +use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; use types::{ + BeaconStateError as Error, consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }, - BeaconStateError as Error, }; -use types::{AttestationData, BeaconState, ChainSpec, EthSpec}; /// Get the participation flags for a valid attestation. /// diff --git a/consensus/state_processing/src/common/get_attesting_indices.rs b/consensus/state_processing/src/common/get_attesting_indices.rs index 842adce431..dc7be7c251 100644 --- a/consensus/state_processing/src/common/get_attesting_indices.rs +++ b/consensus/state_processing/src/common/get_attesting_indices.rs @@ -2,6 +2,7 @@ use types::*; pub mod attesting_indices_base { use crate::per_block_processing::errors::{AttestationInvalid as Invalid, BlockOperationError}; + use ssz_types::{BitList, VariableList}; use types::*; /// Convert `attestation` to (almost) indexed-verifiable form. @@ -44,10 +45,10 @@ pub mod attesting_indices_base { } pub mod attesting_indices_electra { - use std::collections::HashSet; - use crate::per_block_processing::errors::{AttestationInvalid as Invalid, BlockOperationError}; use safe_arith::SafeArith; + use ssz_types::{BitList, BitVector, VariableList}; + use std::collections::HashSet; use types::*; /// Compute an Electra IndexedAttestation given a list of committees. @@ -118,10 +119,10 @@ pub mod attesting_indices_electra { .iter() .enumerate() .filter_map(|(i, &index)| { - if let Ok(aggregation_bit_index) = committee_offset.safe_add(i) { - if aggregation_bits.get(aggregation_bit_index).unwrap_or(false) { - return Some(index as u64); - } + if let Ok(aggregation_bit_index) = committee_offset.safe_add(i) + && aggregation_bits.get(aggregation_bit_index).unwrap_or(false) + { + return Some(index as u64); } None }) diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index bd60f16014..01c1855fb1 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -1,11 +1,12 @@ use crate::common::update_progressive_balances_cache::update_progressive_balances_on_slashing; use crate::{ + ConsensusContext, common::{decrease_balance, increase_balance, initiate_validator_exit}, per_block_processing::errors::BlockProcessingError, - ConsensusContext, }; use safe_arith::SafeArith; use std::cmp; +use typenum::Unsigned; use types::{ consts::altair::{PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}, *, diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs index 1fdfe802c4..24a5db2025 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -5,12 +5,14 @@ use crate::metrics::{ }; use crate::{BlockProcessingError, EpochProcessingError}; use metrics::set_gauge; +use tracing::instrument; use types::{ - is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, - EpochTotalBalances, EthSpec, ParticipationFlags, ProgressiveBalancesCache, Validator, + BeaconState, BeaconStateError, ChainSpec, Epoch, EpochTotalBalances, EthSpec, + ParticipationFlags, ProgressiveBalancesCache, Validator, is_progressive_balances_enabled, }; /// Initializes the `ProgressiveBalancesCache` if it is unbuilt. +#[instrument(skip_all, level = "debug")] pub fn initialize_progressive_balances_cache( state: &mut BeaconState, spec: &ChainSpec, diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 0c176d4ab1..07d554e303 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,7 +1,7 @@ +use crate::EpochCacheError; use crate::common::{attesting_indices_base, attesting_indices_electra}; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; -use crate::EpochCacheError; -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, @@ -148,12 +148,12 @@ impl ConsensusContext { } #[allow(unknown_lints)] - #[allow(elided_named_lifetimes)] + #[allow(mismatched_lifetime_syntaxes)] pub fn get_indexed_attestation<'a>( &'a mut self, state: &BeaconState, attestation: AttestationRef<'a, E>, - ) -> Result, BlockOperationError> { + ) -> Result, BlockOperationError> { let key = attestation.tree_hash_root(); match attestation { AttestationRef::Base(attn) => match self.indexed_attestations.entry(key) { diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index dc1d79709e..ee03596d09 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -2,11 +2,11 @@ use crate::common::altair::BaseRewardPerIncrement; use crate::common::base::SqrtTotalActiveBalance; use crate::common::{altair, base}; use crate::metrics; +use fixed_bytes::FixedBytesExtended; use safe_arith::SafeArith; +use tracing::instrument; use types::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; -use types::{ - ActivationQueue, BeaconState, ChainSpec, EthSpec, FixedBytesExtended, ForkName, Hash256, -}; +use types::{ActivationQueue, BeaconState, ChainSpec, EthSpec, ForkName, Hash256}; /// Precursor to an `EpochCache`. pub struct PreEpochCache { @@ -122,7 +122,7 @@ pub fn is_epoch_cache_initialized( let current_epoch = state.current_epoch(); let epoch_cache: &EpochCache = state.epoch_cache(); let decision_block_root = state - .proposer_shuffling_decision_root(Hash256::zero()) + .epoch_cache_decision_root(Hash256::zero()) .map_err(EpochCacheError::BeaconState)?; Ok(epoch_cache @@ -130,6 +130,7 @@ pub fn is_epoch_cache_initialized( .is_ok()) } +#[instrument(skip_all, level = "debug")] pub fn initialize_epoch_cache( state: &mut BeaconState, spec: &ChainSpec, @@ -144,7 +145,7 @@ pub fn initialize_epoch_cache( let current_epoch = state.current_epoch(); let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; let decision_block_root = state - .proposer_shuffling_decision_root(Hash256::zero()) + .epoch_cache_decision_root(Hash256::zero()) .map_err(EpochCacheError::BeaconState)?; state.build_total_active_balance_cache(spec)?; diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 9c9e183be8..4eee60549c 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -5,8 +5,9 @@ use crate::common::DepositDataTree; use crate::upgrade::electra::upgrade_state_to_electra; use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_eip7805, upgrade_to_fulu, + upgrade_to_eip7805, upgrade_to_fulu, upgrade_to_gloas, }; +use fixed_bytes::FixedBytesExtended; use safe_arith::{ArithError, SafeArith}; use std::sync::Arc; use tree_hash::TreeHash; @@ -167,11 +168,26 @@ pub fn initialize_beacon_state_from_eth1( state.fork_mut().previous_version = spec.fulu_fork_version; // Override latest execution payload header. - if let Some(ExecutionPayloadHeader::Fulu(header)) = execution_payload_header { + if let Some(ExecutionPayloadHeader::Fulu(ref header)) = execution_payload_header { *state.latest_execution_payload_header_fulu_mut()? = header.clone(); } } + // Upgrade to gloas if configured from genesis. + if spec + .gloas_fork_epoch + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_gloas(&mut state, spec)?; + + // Remove intermediate Fulu fork from `state.fork`. + state.fork_mut().previous_version = spec.gloas_fork_version; + + // Override latest execution payload header. + // Here's where we *would* clone the header but there is no header here so.. + // TODO(EIP7732): check this + } + // Now that we have our validators, initialize the caches (including the committees) state.build_caches(spec)?; diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index adabf6862d..9b2696c6d5 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -37,12 +37,12 @@ pub use genesis::{ process_activations, }; pub use per_block_processing::{ - block_signature_verifier, errors::BlockProcessingError, per_block_processing, signature_sets, BlockSignatureStrategy, BlockSignatureVerifier, VerifyBlockRoot, VerifySignatures, + block_signature_verifier, errors::BlockProcessingError, per_block_processing, signature_sets, }; pub use per_epoch_processing::{ errors::EpochProcessingError, process_epoch as per_epoch_processing, }; -pub use per_slot_processing::{per_slot_processing, Error as SlotProcessingError}; +pub use per_slot_processing::{Error as SlotProcessingError, per_slot_processing}; pub use types::{EpochCache, EpochCacheError, EpochCacheKey}; pub use verify_operation::{SigVerifiedOp, TransformPersist, VerifyOperation, VerifyOperationAt}; diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index 8772dbd4f8..65690ae30a 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -7,23 +7,23 @@ use std::sync::LazyLock; pub static PARTICIPATION_PREV_EPOCH_HEAD_ATTESTING_GWEI_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_gauge( - "beacon_participation_prev_epoch_head_attesting_gwei_total", - "Total effective balance (gwei) of validators who attested to the head in the previous epoch" - ) + "beacon_participation_prev_epoch_head_attesting_gwei_total", + "Total effective balance (gwei) of validators who attested to the head in the previous epoch", + ) }); pub static PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_gauge( - "beacon_participation_prev_epoch_target_attesting_gwei_total", - "Total effective balance (gwei) of validators who attested to the target in the previous epoch" - ) + "beacon_participation_prev_epoch_target_attesting_gwei_total", + "Total effective balance (gwei) of validators who attested to the target in the previous epoch", + ) }); pub static PARTICIPATION_PREV_EPOCH_SOURCE_ATTESTING_GWEI_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_gauge( - "beacon_participation_prev_epoch_source_attesting_gwei_total", - "Total effective balance (gwei) of validators who attested to the source in the previous epoch" - ) + "beacon_participation_prev_epoch_source_attesting_gwei_total", + "Total effective balance (gwei) of validators who attested to the source in the previous epoch", + ) }); pub static PARTICIPATION_CURRENT_EPOCH_TOTAL_ACTIVE_GWEI_TOTAL: LazyLock> = LazyLock::new(|| { @@ -63,7 +63,7 @@ pub static PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Laz > = LazyLock::new(|| { try_create_int_gauge( "beacon_participation_prev_epoch_target_attesting_gwei_progressive_total", - "Progressive total effective balance (gwei) of validators who attested to the target in the previous epoch" + "Progressive total effective balance (gwei) of validators who attested to the target in the previous epoch", ) }); pub static PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: LazyLock< @@ -71,6 +71,6 @@ pub static PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Laz > = LazyLock::new(|| { try_create_int_gauge( "beacon_participation_curr_epoch_target_attesting_gwei_progressive_total", - "Progressive total effective balance (gwei) of validators who attested to the target in the current epoch" + "Progressive total effective balance (gwei) of validators who attested to the target in the current epoch", ) }); diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 57cb412040..c24ddce829 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -5,6 +5,7 @@ use safe_arith::{ArithError, SafeArith, SafeArithIter}; use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; use std::borrow::Cow; use tree_hash::TreeHash; +use typenum::Unsigned; use types::*; pub use self::verify_attester_slashing::{ @@ -40,13 +41,13 @@ mod verify_exit; mod verify_proposer_slashing; use crate::common::decrease_balance; - use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_metrics, }; use crate::epoch_cache::initialize_epoch_cache; #[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; +use tracing::instrument; /// The strategy to be used when validating the block's signatures. #[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] @@ -97,6 +98,7 @@ pub enum VerifyBlockRoot { /// re-calculating the root when it is already known. Note `block_root` should be equal to the /// tree hash root of the block, NOT the signing root of the block. This function takes /// care of mixing in the domain. +#[instrument(skip_all)] pub fn per_block_processing>( state: &mut BeaconState, signed_block: &SignedBeaconBlock, @@ -170,10 +172,14 @@ pub fn per_block_processing>( // previous block. if is_execution_enabled(state, block.body()) { let body = block.body(); + // TODO(EIP-7732): build out process_withdrawals variant for gloas process_withdrawals::(state, body.execution_payload()?, spec)?; process_execution_payload::(state, body, spec)?; } + // TODO(EIP-7732): build out process_execution_bid + // process_execution_bid(state, block, verify_signatures, spec)?; + process_randao(state, block, verify_randao, ctxt, spec)?; process_eth1_data(state, block.body().eth1_data())?; process_operations(state, block.body(), verify_signatures, ctxt, spec)?; @@ -467,6 +473,7 @@ pub fn process_execution_payload>( /// repeatedly write code to treat these errors as false. /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_merge_transition_complete pub fn is_merge_transition_complete(state: &BeaconState) -> bool { + // TODO(EIP7732): check this cause potuz modified this function for god knows what reason if state.fork_name_unchecked().capella_enabled() { true } else if state.fork_name_unchecked().bellatrix_enabled() { @@ -626,10 +633,16 @@ pub fn get_expected_withdrawals( .safe_rem(state.validators().len() as u64)?; } - Ok((withdrawals.into(), processed_partial_withdrawals_count)) + Ok(( + withdrawals + .try_into() + .map_err(BlockProcessingError::SszTypesError)?, + processed_partial_withdrawals_count, + )) } /// Apply withdrawals to the state. +/// TODO(EIP-7732): abstract this out and create gloas variant pub fn process_withdrawals>( state: &mut BeaconState, payload: Payload::Ref<'_>, diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index 08cfd9cba8..8cc9de42db 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -1,12 +1,12 @@ use crate::common::{altair::BaseRewardPerIncrement, decrease_balance, increase_balance}; use crate::per_block_processing::errors::{BlockProcessingError, SyncAggregateInvalid}; -use crate::{signature_sets::sync_aggregate_signature_set, VerifySignatures}; +use crate::{VerifySignatures, signature_sets::sync_aggregate_signature_set}; +use bls::PublicKeyBytes; use safe_arith::SafeArith; use std::borrow::Cow; +use typenum::Unsigned; use types::consts::altair::{PROPOSER_WEIGHT, SYNC_REWARD_WEIGHT, WEIGHT_DENOMINATOR}; -use types::{ - BeaconState, BeaconStateError, ChainSpec, EthSpec, PublicKeyBytes, SyncAggregate, Unsigned, -}; +use types::{BeaconState, BeaconStateError, ChainSpec, EthSpec, SyncAggregate}; pub fn process_sync_aggregate( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 8d4a544196..9aa44137d8 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -3,7 +3,7 @@ use super::signature_sets::{Error as SignatureSetError, *}; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; use crate::{ConsensusContext, ContextError}; -use bls::{verify_signature_sets, PublicKey, PublicKeyBytes, SignatureSet}; +use bls::{PublicKey, PublicKeyBytes, SignatureSet, verify_signature_sets}; use std::borrow::Cow; use types::{ AbstractExecPayload, BeaconState, BeaconStateError, ChainSpec, EthSpec, Hash256, @@ -324,17 +324,17 @@ where &mut self, block: &'a SignedBeaconBlock, ) -> Result<()> { - if let Ok(sync_aggregate) = block.message().body().sync_aggregate() { - if let Some(signature_set) = sync_aggregate_signature_set( + if let Ok(sync_aggregate) = block.message().body().sync_aggregate() + && let Some(signature_set) = sync_aggregate_signature_set( &self.decompressor, sync_aggregate, block.slot(), block.parent_root(), self.state, self.spec, - )? { - self.sets.push(signature_set); - } + )? + { + self.sets.push(signature_set); } Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/deneb.rs b/consensus/state_processing/src/per_block_processing/deneb.rs index 217c2ea30b..a57c080c03 100644 --- a/consensus/state_processing/src/per_block_processing/deneb.rs +++ b/consensus/state_processing/src/per_block_processing/deneb.rs @@ -1,5 +1,5 @@ use ethereum_hashing::hash_fixed; -use types::{KzgCommitment, VersionedHash, VERSIONED_HASH_VERSION_KZG}; +use types::{KzgCommitment, VERSIONED_HASH_VERSION_KZG, VersionedHash}; pub fn kzg_commitment_to_versioned_hash(kzg_commitment: &KzgCommitment) -> VersionedHash { let mut hashed_commitment = hash_fixed(&kzg_commitment.0); diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 82dd616724..8afeeb685b 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,12 +1,13 @@ use super::*; +use crate::VerifySignatures; use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; -use crate::VerifySignatures; +use ssz_types::FixedVector; +use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; -use types::typenum::U33; pub fn process_operations>( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 39f438f97f..0e936007ee 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -2,17 +2,18 @@ //! validated individually, or alongside in others in a potentially cheaper bulk operation. //! //! This module exposes one function to extract each type of `SignatureSet` from a `BeaconBlock`. -use bls::SignatureSet; +use bls::{AggregateSignature, PublicKey, PublicKeyBytes, Signature, SignatureSet}; use ssz::DecodeError; use std::borrow::Cow; use tree_hash::TreeHash; +use typenum::Unsigned; use types::{ - AbstractExecPayload, AggregateSignature, AttesterSlashingRef, BeaconBlockRef, BeaconState, - BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, - InconsistentFork, IndexedAttestation, IndexedAttestationRef, ProposerSlashing, PublicKey, - PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, - SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, + AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, + ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, + IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, + SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, + SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, + SyncAggregatorSelectionData, }; pub type Result = std::result::Result; @@ -56,7 +57,7 @@ impl From for Error { pub fn get_pubkey_from_state( state: &BeaconState, validator_index: usize, -) -> Option> +) -> Option> where E: EthSpec, { diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 34e9ff120d..739717b33f 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -5,13 +5,16 @@ use crate::per_block_processing::errors::{ DepositInvalid, HeaderInvalid, IndexedAttestationInvalid, IntoWithIndex, ProposerSlashingInvalid, }; -use crate::{per_block_processing, BlockReplayError, BlockReplayer}; +use crate::{BlockReplayError, BlockReplayer, per_block_processing}; use crate::{ - per_block_processing::{process_operations, verify_exit::verify_exit}, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, + per_block_processing::{process_operations, verify_exit::verify_exit}, }; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; +use fixed_bytes::FixedBytesExtended; use ssz_types::Bitfield; +use ssz_types::VariableList; use std::sync::{Arc, LazyLock}; use test_utils::generate_deterministic_keypairs; use types::*; @@ -213,7 +216,7 @@ async fn valid_4_deposits() { let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 4, None, None); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -237,7 +240,7 @@ async fn invalid_deposit_deposit_count_too_big() { let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -269,7 +272,7 @@ async fn invalid_deposit_count_too_small() { let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -301,7 +304,7 @@ async fn invalid_deposit_bad_merkle_proof() { let mut state = harness.get_current_state(); let (deposits, state) = harness.make_deposits(&mut state, 1, None, None); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -336,7 +339,7 @@ async fn invalid_deposit_wrong_sig() { let (deposits, state) = harness.make_deposits(&mut state, 1, None, Some(SignatureBytes::empty())); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -360,7 +363,7 @@ async fn invalid_deposit_invalid_pub_key() { let (deposits, state) = harness.make_deposits(&mut state, 1, Some(PublicKeyBytes::empty()), None); - let deposits = VariableList::from(deposits); + let deposits = VariableList::try_from(deposits).unwrap(); let mut head_block = harness .chain @@ -717,10 +720,10 @@ async fn invalid_attester_slashing_not_slashable() { let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { - AttesterSlashing::Base(ref mut attester_slashing) => { + AttesterSlashing::Base(attester_slashing) => { attester_slashing.attestation_1 = attester_slashing.attestation_2.clone(); } - AttesterSlashing::Electra(ref mut attester_slashing) => { + AttesterSlashing::Electra(attester_slashing) => { attester_slashing.attestation_1 = attester_slashing.attestation_2.clone(); } } @@ -752,11 +755,13 @@ async fn invalid_attester_slashing_1_invalid() { let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { - AttesterSlashing::Base(ref mut attester_slashing) => { - attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]); + AttesterSlashing::Base(attester_slashing) => { + attester_slashing.attestation_1.attesting_indices = + VariableList::try_from(vec![2, 1]).unwrap(); } - AttesterSlashing::Electra(ref mut attester_slashing) => { - attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]); + AttesterSlashing::Electra(attester_slashing) => { + attester_slashing.attestation_1.attesting_indices = + VariableList::try_from(vec![2, 1]).unwrap(); } } @@ -790,11 +795,13 @@ async fn invalid_attester_slashing_2_invalid() { let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); match &mut attester_slashing { - AttesterSlashing::Base(ref mut attester_slashing) => { - attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]); + AttesterSlashing::Base(attester_slashing) => { + attester_slashing.attestation_2.attesting_indices = + VariableList::try_from(vec![2, 1]).unwrap(); } - AttesterSlashing::Electra(ref mut attester_slashing) => { - attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]); + AttesterSlashing::Electra(attester_slashing) => { + attester_slashing.attestation_2.attesting_indices = + VariableList::try_from(vec![2, 1]).unwrap(); } } @@ -906,7 +913,7 @@ async fn invalid_proposer_slashing_duplicate_slashing() { let mut ctxt = ConsensusContext::new(state.slot()); let result_1 = process_operations::process_proposer_slashings( &mut state, - &[proposer_slashing.clone()], + std::slice::from_ref(&proposer_slashing), VerifySignatures::False, &mut ctxt, &spec, @@ -915,7 +922,7 @@ async fn invalid_proposer_slashing_duplicate_slashing() { let result_2 = process_operations::process_proposer_slashings( &mut state, - &[proposer_slashing], + std::slice::from_ref(&proposer_slashing), VerifySignatures::False, &mut ctxt, &spec, diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 6b4a394c73..0d1fd17768 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -1,7 +1,7 @@ -use super::errors::{AttestationInvalid as Invalid, BlockOperationError}; use super::VerifySignatures; -use crate::per_block_processing::is_valid_indexed_attestation; +use super::errors::{AttestationInvalid as Invalid, BlockOperationError}; use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_attestation; use safe_arith::SafeArith; use types::*; diff --git a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs index 24024fa899..a1dd4f8eac 100644 --- a/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs +++ b/consensus/state_processing/src/per_block_processing/verify_bls_to_execution_change.rs @@ -1,6 +1,6 @@ use super::errors::{BlockOperationError, BlsExecutionChangeInvalid as Invalid}; -use crate::per_block_processing::signature_sets::bls_execution_change_signature_set; use crate::VerifySignatures; +use crate::per_block_processing::signature_sets::bls_execution_change_signature_set; use ethereum_hashing::hash; use types::*; diff --git a/consensus/state_processing/src/per_block_processing/verify_deposit.rs b/consensus/state_processing/src/per_block_processing/verify_deposit.rs index c996e580a7..d403bfa82b 100644 --- a/consensus/state_processing/src/per_block_processing/verify_deposit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_deposit.rs @@ -1,5 +1,6 @@ use super::errors::{BlockOperationError, DepositInvalid}; use crate::per_block_processing::signature_sets::deposit_pubkey_signature_message; +use bls::PublicKeyBytes; use merkle_proof::verify_merkle_proof; use safe_arith::SafeArith; use tree_hash::TreeHash; diff --git a/consensus/state_processing/src/per_block_processing/verify_exit.rs b/consensus/state_processing/src/per_block_processing/verify_exit.rs index dea17dbc0c..bdf42dab98 100644 --- a/consensus/state_processing/src/per_block_processing/verify_exit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_exit.rs @@ -1,7 +1,7 @@ use super::errors::{BlockOperationError, ExitInvalid}; use crate::per_block_processing::{ - signature_sets::{exit_signature_set, get_pubkey_from_state}, VerifySignatures, + signature_sets::{exit_signature_set, get_pubkey_from_state}, }; use safe_arith::SafeArith; use types::*; diff --git a/consensus/state_processing/src/per_epoch_processing.rs b/consensus/state_processing/src/per_epoch_processing.rs index 41c30c4931..8de6054bd2 100644 --- a/consensus/state_processing/src/per_epoch_processing.rs +++ b/consensus/state_processing/src/per_epoch_processing.rs @@ -5,6 +5,7 @@ pub use epoch_processing_summary::{EpochProcessingSummary, ParticipationEpochSum use errors::EpochProcessingError as Error; pub use justification_and_finalization_state::JustificationAndFinalizationState; use safe_arith::SafeArith; +use tracing::instrument; use types::{BeaconState, ChainSpec, EthSpec}; pub use registry_updates::{process_registry_updates, process_registry_updates_slow}; @@ -30,6 +31,7 @@ pub mod weigh_justification_and_finalization; /// /// Mutates the given `BeaconState`, returning early if an error is encountered. If an error is /// returned, a state might be "half-processed" and therefore in an invalid state. +#[instrument(skip_all)] pub fn process_epoch( state: &mut BeaconState, spec: &ChainSpec, diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index dc4dbe7cbc..d9e6964730 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -3,7 +3,7 @@ use crate::common::update_progressive_balances_cache::{ initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, }; use crate::epoch_cache::initialize_epoch_cache; -use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; +use crate::per_epoch_processing::single_pass::{SinglePassConfig, process_epoch_single_pass}; use crate::per_epoch_processing::{ capella::process_historical_summaries_update, historical_roots_update::process_historical_roots_update, diff --git a/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs index 698e88b83f..9e8a36b6d5 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/inactivity_updates.rs @@ -1,5 +1,5 @@ -use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; use crate::EpochProcessingError; +use crate::per_epoch_processing::single_pass::{SinglePassConfig, process_epoch_single_pass}; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; use types::eth_spec::EthSpec; diff --git a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs index 61b5c1ed5a..7fb30fee1d 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/justification_and_finalization.rs @@ -1,6 +1,6 @@ use crate::per_epoch_processing::Error; use crate::per_epoch_processing::{ - weigh_justification_and_finalization, JustificationAndFinalizationState, + JustificationAndFinalizationState, weigh_justification_and_finalization, }; use safe_arith::SafeArith; use types::{BeaconState, EthSpec}; diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs index fc55fb1114..5e177c5d2b 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_flag_updates.rs @@ -1,8 +1,8 @@ use crate::EpochProcessingError; +use milhouse::List; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; use types::participation_flags::ParticipationFlags; -use types::List; pub fn process_participation_flag_updates( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs b/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs index c4059f94af..dff445feb0 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/rewards_and_penalties.rs @@ -1,6 +1,6 @@ use crate::per_epoch_processing::{ - single_pass::{process_epoch_single_pass, SinglePassConfig}, Error, + single_pass::{SinglePassConfig, process_epoch_single_pass}, }; use types::consts::altair::PARTICIPATION_FLAG_WEIGHTS; use types::{BeaconState, ChainSpec, EthSpec}; diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index e468a8ddd6..4f7451cae6 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -1,4 +1,4 @@ -use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use super::{EpochProcessingSummary, Error, process_registry_updates, process_slashings}; use crate::epoch_cache::initialize_epoch_cache; use crate::per_epoch_processing::{ effective_balance_updates::process_effective_balance_updates, diff --git a/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs b/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs index db64808a80..1b34d51545 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/justification_and_finalization.rs @@ -1,7 +1,7 @@ -use crate::per_epoch_processing::base::TotalBalances; use crate::per_epoch_processing::Error; +use crate::per_epoch_processing::base::TotalBalances; use crate::per_epoch_processing::{ - weigh_justification_and_finalization, JustificationAndFinalizationState, + JustificationAndFinalizationState, weigh_justification_and_finalization, }; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec}; diff --git a/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs b/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs index a316c55bef..e17caeb7ba 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/rewards_and_penalties.rs @@ -1,10 +1,10 @@ use crate::common::{ - base::{get_base_reward, SqrtTotalActiveBalance}, + base::{SqrtTotalActiveBalance, get_base_reward}, decrease_balance, increase_balance, }; use crate::per_epoch_processing::{ - base::{TotalBalances, ValidatorStatus, ValidatorStatuses}, Delta, Error, + base::{TotalBalances, ValidatorStatus, ValidatorStatuses}, }; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec}; @@ -190,16 +190,15 @@ fn get_attestation_deltas( .combine(inactivity_penalty_delta)?; } - if let ProposerRewardCalculation::Include = proposer_reward { - if let Some((proposer_index, proposer_delta)) = proposer_delta { - if include_validator_delta(proposer_index) { - deltas - .get_mut(proposer_index) - .ok_or(Error::ValidatorStatusesInconsistent)? - .inclusion_delay_delta - .combine(proposer_delta)?; - } - } + if let ProposerRewardCalculation::Include = proposer_reward + && let Some((proposer_index, proposer_delta)) = proposer_delta + && include_validator_delta(proposer_index) + { + deltas + .get_mut(proposer_index) + .ok_or(Error::ValidatorStatusesInconsistent)? + .inclusion_delay_delta + .combine(proposer_delta)?; } } diff --git a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs index 73881e932b..8daad83a15 100644 --- a/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/effective_balance_updates.rs @@ -1,5 +1,5 @@ use super::errors::EpochProcessingError; -use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; +use crate::per_epoch_processing::single_pass::{SinglePassConfig, process_epoch_single_pass}; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; diff --git a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs index b2228a5a1d..a818e08775 100644 --- a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs +++ b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs @@ -1,10 +1,11 @@ -use super::base::{validator_statuses::InclusionInfo, TotalBalances, ValidatorStatus}; +use super::base::{TotalBalances, ValidatorStatus, validator_statuses::InclusionInfo}; use crate::metrics; +use milhouse::List; use std::sync::Arc; use types::{ + BeaconStateError, Epoch, EthSpec, ParticipationFlags, ProgressiveBalancesCache, SyncCommittee, + Validator, consts::altair::{TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX}, - BeaconStateError, Epoch, EthSpec, List, ParticipationFlags, ProgressiveBalancesCache, - SyncCommittee, Validator, }; /// Provides a summary of validator participation during the epoch. diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index 7485e365ec..4818dcbf67 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -1,4 +1,5 @@ -use types::{milhouse, BeaconStateError, EpochCacheError, InconsistentFork}; +use milhouse; +use types::{BeaconStateError, EpochCacheError, InconsistentFork}; #[derive(Debug, PartialEq)] pub enum EpochProcessingError { @@ -30,6 +31,7 @@ pub enum EpochProcessingError { MissingEarliestExitEpoch, MissingExitBalanceToConsume, PendingDepositsLogicError, + ProposerLookaheadOutOfBounds(usize), } impl From for EpochProcessingError { diff --git a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs index 7686932192..9172d954bc 100644 --- a/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs +++ b/consensus/state_processing/src/per_epoch_processing/historical_roots_update.rs @@ -1,9 +1,9 @@ use super::errors::EpochProcessingError; use safe_arith::SafeArith; use tree_hash::TreeHash; +use typenum::Unsigned; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; -use types::Unsigned; pub fn process_historical_roots_update( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs b/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs index 66d68804e1..8d712fd19b 100644 --- a/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs +++ b/consensus/state_processing/src/per_epoch_processing/justification_and_finalization_state.rs @@ -1,4 +1,5 @@ -use types::{BeaconState, BeaconStateError, BitVector, Checkpoint, Epoch, EthSpec, Hash256}; +use ssz_types::BitVector; +use types::{BeaconState, BeaconStateError, Checkpoint, Epoch, EthSpec, Hash256}; /// This is a subset of the `BeaconState` which is used to compute justification and finality /// without modifying the `BeaconState`. diff --git a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs index 3d02d79736..91250ca30c 100644 --- a/consensus/state_processing/src/per_epoch_processing/registry_updates.rs +++ b/consensus/state_processing/src/per_epoch_processing/registry_updates.rs @@ -1,4 +1,4 @@ -use crate::per_epoch_processing::single_pass::{process_epoch_single_pass, SinglePassConfig}; +use crate::per_epoch_processing::single_pass::{SinglePassConfig, process_epoch_single_pass}; use crate::{common::initiate_validator_exit, per_epoch_processing::Error}; use safe_arith::SafeArith; use types::{BeaconState, ChainSpec, EthSpec, Validator}; diff --git a/consensus/state_processing/src/per_epoch_processing/resets.rs b/consensus/state_processing/src/per_epoch_processing/resets.rs index c9f69c3c95..e05fb30c33 100644 --- a/consensus/state_processing/src/per_epoch_processing/resets.rs +++ b/consensus/state_processing/src/per_epoch_processing/resets.rs @@ -1,8 +1,9 @@ use super::errors::EpochProcessingError; +use milhouse::List; use safe_arith::SafeArith; +use typenum::Unsigned; use types::beacon_state::BeaconState; use types::eth_spec::EthSpec; -use types::{List, Unsigned}; pub fn process_eth1_data_reset( state: &mut BeaconState, diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index af6a0936e2..914e025f2f 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -3,23 +3,25 @@ use crate::{ decrease_balance, increase_balance, update_progressive_balances_cache::initialize_progressive_balances_cache, }, - epoch_cache::{initialize_epoch_cache, PreEpochCache}, + epoch_cache::{PreEpochCache, initialize_epoch_cache}, per_block_processing::is_valid_deposit_signature, per_epoch_processing::{Delta, Error, ParticipationEpochSummary}, }; use itertools::izip; +use milhouse::{Cow, List, Vector}; use safe_arith::{SafeArith, SafeArithIter}; use std::cmp::{max, min}; use std::collections::{BTreeSet, HashMap}; +use tracing::instrument; +use typenum::Unsigned; use types::{ + ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, + EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit, ProgressiveBalancesCache, + RelativeEpoch, Validator, consts::altair::{ NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, }, - milhouse::Cow, - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, ExitCache, ForkName, List, ParticipationFlags, PendingDeposit, - ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, }; pub struct SinglePassConfig { @@ -30,6 +32,7 @@ pub struct SinglePassConfig { pub pending_deposits: bool, pub pending_consolidations: bool, pub effective_balance_updates: bool, + pub proposer_lookahead: bool, } impl Default for SinglePassConfig { @@ -48,6 +51,7 @@ impl SinglePassConfig { pending_deposits: true, pending_consolidations: true, effective_balance_updates: true, + proposer_lookahead: true, } } @@ -60,6 +64,7 @@ impl SinglePassConfig { pending_deposits: false, pending_consolidations: false, effective_balance_updates: false, + proposer_lookahead: false, } } } @@ -131,6 +136,7 @@ impl ValidatorInfo { } } +#[instrument(skip_all)] pub fn process_epoch_single_pass( state: &mut BeaconState, spec: &ChainSpec, @@ -460,9 +466,43 @@ pub fn process_epoch_single_pass( next_epoch_cache.into_epoch_cache(next_epoch_activation_queue, spec)?; } + if conf.proposer_lookahead && fork_name.fulu_enabled() { + process_proposer_lookahead(state, spec)?; + } + Ok(summary) } +// TOOO(EIP-7917): use balances cache +pub fn process_proposer_lookahead( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let mut lookahead = state.proposer_lookahead()?.clone().to_vec(); + + // Shift out proposers in the first epoch + lookahead.copy_within((E::slots_per_epoch() as usize).., 0); + + let next_epoch = state + .current_epoch() + .safe_add(spec.min_seed_lookahead.as_u64())? + .safe_add(1)?; + let last_epoch_proposers = state.get_beacon_proposer_indices(next_epoch, spec)?; + + // Fill in the last epoch with new proposer indices + let last_epoch_start = E::proposer_lookahead_slots().safe_sub(E::slots_per_epoch() as usize)?; + for (i, proposer) in last_epoch_proposers.into_iter().enumerate() { + let index = last_epoch_start.safe_add(i)?; + *lookahead + .get_mut(index) + .ok_or(Error::ProposerLookaheadOutOfBounds(index))? = proposer as u64; + } + + *state.proposer_lookahead_mut()? = Vector::new(lookahead)?; + + Ok(()) +} + fn process_single_inactivity_update( inactivity_score: &mut Cow, validator_info: &ValidatorInfo, diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index 6104208ee6..6008276d15 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -1,10 +1,11 @@ use crate::common::decrease_balance; use crate::per_epoch_processing::{ - single_pass::{process_epoch_single_pass, SinglePassConfig}, Error, + single_pass::{SinglePassConfig, process_epoch_single_pass}, }; use safe_arith::{SafeArith, SafeArithIter}; -use types::{BeaconState, ChainSpec, EthSpec, Unsigned}; +use typenum::Unsigned; +use types::{BeaconState, ChainSpec, EthSpec}; /// Process slashings. pub fn process_slashings( diff --git a/consensus/state_processing/src/per_epoch_processing/tests.rs b/consensus/state_processing/src/per_epoch_processing/tests.rs index b93ede248c..f042e8766c 100644 --- a/consensus/state_processing/src/per_epoch_processing/tests.rs +++ b/consensus/state_processing/src/per_epoch_processing/tests.rs @@ -3,13 +3,10 @@ use crate::per_epoch_processing::process_epoch; use beacon_chain::test_utils::BeaconChainHarness; use beacon_chain::types::{EthSpec, MinimalEthSpec}; use bls::{FixedBytesExtended, Hash256}; -use env_logger::{Builder, Env}; use types::Slot; #[tokio::test] async fn runs_without_error() { - Builder::from_env(Env::default().default_filter_or("error")).init(); - let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .deterministic_keypairs(8) @@ -42,7 +39,7 @@ async fn runs_without_error() { mod release_tests { use super::*; use crate::{ - per_slot_processing::per_slot_processing, EpochProcessingError, SlotProcessingError, + EpochProcessingError, SlotProcessingError, per_slot_processing::per_slot_processing, }; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; use std::sync::Arc; diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 7dae5fcbb6..a8dc8c389f 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -1,9 +1,11 @@ use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_eip7805, upgrade_to_electra, upgrade_to_fulu, + upgrade_to_eip7805, upgrade_to_electra, upgrade_to_fulu, upgrade_to_gloas, }; use crate::{per_epoch_processing::EpochProcessingSummary, *}; +use fixed_bytes::FixedBytesExtended; use safe_arith::{ArithError, SafeArith}; +use tracing::instrument; use types::*; #[derive(Debug, PartialEq)] @@ -25,6 +27,7 @@ impl From for Error { /// If the root of the supplied `state` is known, then it can be passed as `state_root`. If /// `state_root` is `None`, the root of `state` will be computed using a cached tree hash. /// Providing the `state_root` makes this function several orders of magnitude faster. +#[instrument(level = "debug", skip_all)] pub fn per_slot_processing( state: &mut BeaconState, state_root: Option, @@ -70,13 +73,17 @@ pub fn per_slot_processing( if spec.electra_fork_epoch == Some(state.current_epoch()) { upgrade_to_electra(state, spec)?; } + // Fulu. + if spec.fulu_fork_epoch == Some(state.current_epoch()) { + upgrade_to_fulu(state, spec)?; + } // Eip7805. if spec.eip7805_fork_epoch == Some(state.current_epoch()) { upgrade_to_eip7805(state, spec)?; } - // Fulu. - if spec.fulu_fork_epoch == Some(state.current_epoch()) { - upgrade_to_fulu(state, spec)?; + // Gloas. + if spec.gloas_fork_epoch == Some(state.current_epoch()) { + upgrade_to_gloas(state, spec)?; } // Additionally build all caches so that all valid states that are advanced always have @@ -88,6 +95,7 @@ pub fn per_slot_processing( Ok(summary) } +#[instrument(skip_all)] fn cache_state( state: &mut BeaconState, state_root: Option, diff --git a/consensus/state_processing/src/state_advance.rs b/consensus/state_processing/src/state_advance.rs index 4d38e7797e..19b21dad19 100644 --- a/consensus/state_processing/src/state_advance.rs +++ b/consensus/state_processing/src/state_advance.rs @@ -5,7 +5,8 @@ //! duplication and protect against some easy-to-make mistakes when performing state advances. use crate::*; -use types::{BeaconState, ChainSpec, EthSpec, FixedBytesExtended, Hash256, Slot}; +use fixed_bytes::FixedBytesExtended; +use types::{BeaconState, ChainSpec, EthSpec, Hash256, Slot}; #[derive(Debug, PartialEq)] pub enum Error { diff --git a/consensus/state_processing/src/upgrade.rs b/consensus/state_processing/src/upgrade.rs index 286b982bc7..85fce5695f 100644 --- a/consensus/state_processing/src/upgrade.rs +++ b/consensus/state_processing/src/upgrade.rs @@ -5,6 +5,7 @@ pub mod deneb; pub mod eip7805; pub mod electra; pub mod fulu; +pub mod gloas; pub use altair::upgrade_to_altair; pub use bellatrix::upgrade_to_bellatrix; @@ -13,3 +14,4 @@ pub use deneb::upgrade_to_deneb; pub use eip7805::upgrade_to_eip7805; pub use electra::upgrade_to_electra; pub use fulu::upgrade_to_fulu; +pub use gloas::upgrade_to_gloas; diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 3006da25ae..022175ff99 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -2,11 +2,12 @@ use crate::common::update_progressive_balances_cache::initialize_progressive_bal use crate::common::{ attesting_indices_base::get_attesting_indices, get_attestation_participation_flag_indices, }; +use milhouse::List; use std::mem; use std::sync::Arc; use types::{ BeaconState, BeaconStateAltair, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, - Fork, List, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, + Fork, ParticipationFlags, PendingAttestation, RelativeEpoch, SyncCommittee, }; /// Translate the participation information from the epoch prior to the fork into Altair's format. diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index ae0dbde767..948fa511b7 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -1,7 +1,8 @@ +use milhouse::List; use std::mem; use types::{ BeaconState, BeaconStateCapella, BeaconStateError as Error, ChainSpec, EpochCache, EthSpec, - Fork, List, + Fork, }; /// Transform a `Bellatrix` state into an `Capella` state. diff --git a/consensus/state_processing/src/upgrade/eip7805.rs b/consensus/state_processing/src/upgrade/eip7805.rs index a40c1fbbe7..14896fef1a 100644 --- a/consensus/state_processing/src/upgrade/eip7805.rs +++ b/consensus/state_processing/src/upgrade/eip7805.rs @@ -20,7 +20,7 @@ pub fn upgrade_state_to_eip7805( spec: &ChainSpec, ) -> Result, Error> { let epoch = pre_state.current_epoch(); - let pre = pre_state.as_electra_mut()?; + let pre = pre_state.as_fulu_mut()?; // Where possible, use something like `mem::take` to move fields from behind the &mut // reference. For other fields that don't have a good default value, use `clone`. // @@ -89,6 +89,7 @@ pub fn upgrade_state_to_eip7805( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), + proposer_lookahead: pre.proposer_lookahead.clone(), }); Ok(post) } diff --git a/consensus/state_processing/src/upgrade/fulu.rs b/consensus/state_processing/src/upgrade/fulu.rs index 6a25d30104..c14c1edbec 100644 --- a/consensus/state_processing/src/upgrade/fulu.rs +++ b/consensus/state_processing/src/upgrade/fulu.rs @@ -1,3 +1,5 @@ +use milhouse::Vector; +use safe_arith::SafeArith; use std::mem; use types::{BeaconState, BeaconStateError as Error, BeaconStateFulu, ChainSpec, EthSpec, Fork}; @@ -15,12 +17,31 @@ pub fn upgrade_to_fulu( Ok(()) } +fn initialize_proposer_lookahead( + state: &BeaconState, + spec: &ChainSpec, +) -> Result, Error> { + let current_epoch = state.current_epoch(); + let mut lookahead = Vec::with_capacity(E::proposer_lookahead_slots()); + for i in 0..(spec.min_seed_lookahead.safe_add(1)?.as_u64()) { + let target_epoch = current_epoch.safe_add(i)?; + lookahead.extend( + state + .get_beacon_proposer_indices(target_epoch, spec) + .map(|vec| vec.into_iter().map(|x| x as u64))?, + ); + } + + Vector::new(lookahead).map_err(|e| e.into()) +} + pub fn upgrade_state_to_fulu( pre_state: &mut BeaconState, spec: &ChainSpec, ) -> Result, Error> { let epoch = pre_state.current_epoch(); - let pre = pre_state.as_eip7805_mut()?; + let proposer_lookahead = initialize_proposer_lookahead(pre_state, spec)?; + let pre = pre_state.as_electra_mut()?; // Where possible, use something like `mem::take` to move fields from behind the &mut // reference. For other fields that don't have a good default value, use `clone`. // @@ -89,6 +110,7 @@ pub fn upgrade_state_to_fulu( exit_cache: mem::take(&mut pre.exit_cache), slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), + proposer_lookahead, }); Ok(post) } diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs new file mode 100644 index 0000000000..d6c353cc2a --- /dev/null +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -0,0 +1,108 @@ +use bls::Hash256; +use milhouse::{List, Vector}; +use ssz_types::BitVector; +use std::mem; +use types::{ + BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, + EthSpec, ExecutionPayloadBid, Fork, +}; + +/// Transform a `Fulu` state into a `Gloas` state. +pub fn upgrade_to_gloas( + pre_state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let post = upgrade_state_to_gloas(pre_state, spec)?; + + *pre_state = post; + + Ok(()) +} + +pub fn upgrade_state_to_gloas( + pre_state: &mut BeaconState, + spec: &ChainSpec, +) -> Result, Error> { + let epoch = pre_state.current_epoch(); + let pre = pre_state.as_fulu_mut()?; + // Where possible, use something like `mem::take` to move fields from behind the &mut + // reference. For other fields that don't have a good default value, use `clone`. + // + // Fixed size vectors get cloned because replacing them would require the same size + // allocation as cloning. + let post = BeaconState::Gloas(BeaconStateGloas { + // Versioning + genesis_time: pre.genesis_time, + genesis_validators_root: pre.genesis_validators_root, + slot: pre.slot, + fork: Fork { + previous_version: pre.fork.current_version, + current_version: spec.gloas_fork_version, + epoch, + }, + // History + latest_block_header: pre.latest_block_header.clone(), + block_roots: pre.block_roots.clone(), + state_roots: pre.state_roots.clone(), + historical_roots: mem::take(&mut pre.historical_roots), + // Eth1 + eth1_data: pre.eth1_data.clone(), + eth1_data_votes: mem::take(&mut pre.eth1_data_votes), + eth1_deposit_index: pre.eth1_deposit_index, + // Registry + validators: mem::take(&mut pre.validators), + balances: mem::take(&mut pre.balances), + // Randomness + randao_mixes: pre.randao_mixes.clone(), + // Slashings + slashings: pre.slashings.clone(), + // `Participation + previous_epoch_participation: mem::take(&mut pre.previous_epoch_participation), + current_epoch_participation: mem::take(&mut pre.current_epoch_participation), + // Finality + justification_bits: pre.justification_bits.clone(), + previous_justified_checkpoint: pre.previous_justified_checkpoint, + current_justified_checkpoint: pre.current_justified_checkpoint, + finalized_checkpoint: pre.finalized_checkpoint, + // Inactivity + inactivity_scores: mem::take(&mut pre.inactivity_scores), + // Sync committees + current_sync_committee: pre.current_sync_committee.clone(), + next_sync_committee: pre.next_sync_committee.clone(), + // Execution Bid + latest_execution_payload_bid: ExecutionPayloadBid::default(), + // Capella + next_withdrawal_index: pre.next_withdrawal_index, + next_withdrawal_validator_index: pre.next_withdrawal_validator_index, + historical_summaries: pre.historical_summaries.clone(), + // Electra + deposit_requests_start_index: pre.deposit_requests_start_index, + deposit_balance_to_consume: pre.deposit_balance_to_consume, + exit_balance_to_consume: pre.exit_balance_to_consume, + earliest_exit_epoch: pre.earliest_exit_epoch, + consolidation_balance_to_consume: pre.consolidation_balance_to_consume, + earliest_consolidation_epoch: pre.earliest_consolidation_epoch, + pending_deposits: pre.pending_deposits.clone(), + pending_partial_withdrawals: pre.pending_partial_withdrawals.clone(), + pending_consolidations: pre.pending_consolidations.clone(), + // Gloas + execution_payload_availability: BitVector::default(), // All bits set to false initially + builder_pending_payments: Vector::new(vec![ + BuilderPendingPayment::default(); + E::builder_pending_payments_limit() + ])?, + builder_pending_withdrawals: List::default(), // Empty list initially, + latest_block_hash: pre.latest_execution_payload_header.block_hash, + latest_withdrawals_root: Hash256::default(), + // Caches + total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), + committee_caches: mem::take(&mut pre.committee_caches), + pubkey_cache: mem::take(&mut pre.pubkey_cache), + exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: mem::take(&mut pre.epoch_cache), + proposer_lookahead: mem::take(&mut pre.proposer_lookahead), + }); + Ok(post) +} diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index 3b20c67b4d..1f76f19586 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -1,3 +1,4 @@ +use crate::VerifySignatures; use crate::per_block_processing::{ errors::{ AttesterSlashingValidationError, BlsExecutionChangeValidationError, ExitValidationError, @@ -6,18 +7,17 @@ use crate::per_block_processing::{ verify_attester_slashing, verify_bls_to_execution_change, verify_exit, verify_proposer_slashing, }; -use crate::VerifySignatures; use arbitrary::Arbitrary; -use derivative::Derivative; -use smallvec::{smallvec, SmallVec}; +use educe::Educe; +use smallvec::{SmallVec, smallvec}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::marker::PhantomData; use test_random_derive::TestRandom; use types::{ - test_utils::TestRandom, AttesterSlashing, AttesterSlashingBase, AttesterSlashingOnDisk, - AttesterSlashingRefOnDisk, BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, - ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingOnDisk, AttesterSlashingRefOnDisk, + BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, ProposerSlashing, + SignedBlsToExecutionChange, SignedVoluntaryExit, test_utils::TestRandom, }; const MAX_FORKS_VERIFIED_AGAINST: usize = 2; @@ -39,11 +39,11 @@ pub trait TransformPersist { /// /// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. -#[derive(Derivative, Debug, Clone, Arbitrary)] -#[derivative( +#[derive(Educe, Debug, Clone, Arbitrary)] +#[educe( PartialEq, Eq, - Hash(bound = "T: TransformPersist + std::hash::Hash, E: EthSpec") + Hash(bound(T: TransformPersist + std::hash::Hash, E: EthSpec)) )] #[arbitrary(bound = "T: TransformPersist + Arbitrary<'arbitrary>, E: EthSpec")] pub struct SigVerifiedOp { @@ -260,11 +260,12 @@ impl VerifyOperation for ProposerSlashing { #[allow(clippy::arithmetic_side_effects)] fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { // Only need a single epoch because the slots of the two headers must be equal. - smallvec![self - .signed_header_1 - .message - .slot - .epoch(E::slots_per_epoch())] + smallvec![ + self.signed_header_1 + .message + .slot + .epoch(E::slots_per_epoch()) + ] } } @@ -417,8 +418,8 @@ impl TransformPersist for SignedBlsToExecutionChange { mod test { use super::*; use types::{ - test_utils::{SeedableRng, TestRandom, XorShiftRng}, MainnetEthSpec, + test_utils::{SeedableRng, TestRandom, XorShiftRng}, }; type E = MainnetEthSpec; diff --git a/consensus/swap_or_not_shuffle/Cargo.toml b/consensus/swap_or_not_shuffle/Cargo.toml index dac83e7553..b6fdc1a728 100644 --- a/consensus/swap_or_not_shuffle/Cargo.toml +++ b/consensus/swap_or_not_shuffle/Cargo.toml @@ -4,17 +4,17 @@ version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } -[[bench]] -name = "benches" -harness = false - -[dev-dependencies] -criterion = { workspace = true } +[features] +arbitrary = ["alloy-primitives/arbitrary"] [dependencies] alloy-primitives = { workspace = true } ethereum_hashing = { workspace = true } fixed_bytes = { workspace = true } -[features] -arbitrary = ["alloy-primitives/arbitrary"] +[dev-dependencies] +criterion = { workspace = true } + +[[bench]] +name = "benches" +harness = false diff --git a/consensus/swap_or_not_shuffle/benches/benches.rs b/consensus/swap_or_not_shuffle/benches/benches.rs index 2909ff1ac6..f33556be38 100644 --- a/consensus/swap_or_not_shuffle/benches/benches.rs +++ b/consensus/swap_or_not_shuffle/benches/benches.rs @@ -1,4 +1,4 @@ -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; use swap_or_not_shuffle::{compute_shuffled_index, shuffle_list as fast_shuffle}; const SHUFFLE_ROUND_COUNT: u8 = 90; diff --git a/consensus/swap_or_not_shuffle/src/compute_shuffled_index.rs b/consensus/swap_or_not_shuffle/src/compute_shuffled_index.rs index a7f25ea65f..199dd0ef3a 100644 --- a/consensus/swap_or_not_shuffle/src/compute_shuffled_index.rs +++ b/consensus/swap_or_not_shuffle/src/compute_shuffled_index.rs @@ -46,11 +46,7 @@ fn do_round(seed: &[u8], index: usize, pivot: usize, round: u8, list_size: usize let source = hash_with_round_and_position(seed, round, position); let byte = source[(position % 256) / 8]; let bit = (byte >> (position % 8)) % 2; - if bit == 1 { - flip - } else { - index - } + if bit == 1 { flip } else { index } } fn hash_with_round_and_position(seed: &[u8], round: u8, position: usize) -> Hash256 { diff --git a/consensus/swap_or_not_shuffle/src/shuffle_list.rs b/consensus/swap_or_not_shuffle/src/shuffle_list.rs index 3e93974fe0..8202b35cde 100644 --- a/consensus/swap_or_not_shuffle/src/shuffle_list.rs +++ b/consensus/swap_or_not_shuffle/src/shuffle_list.rs @@ -96,8 +96,7 @@ pub fn shuffle_list( loop { buf.set_round(r); - let pivot = buf.raw_pivot() as usize % list_size; - + let pivot = (buf.raw_pivot() % list_size as u64) as usize; let mirror = (pivot + 1) >> 1; buf.mix_in_position(pivot >> 8); diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index b58d4ef96f..78c6f871cb 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -1,29 +1,40 @@ [package] name = "types" version = "0.2.1" -authors = ["Paul Hauner ", "Age Manning "] +authors = [ + "Paul Hauner ", + "Age Manning ", +] edition = { workspace = true } -[[bench]] -name = "benches" -harness = false +[features] +default = ["sqlite", "legacy-arith"] +# Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. +legacy-arith = [] +sqlite = ["dep:rusqlite"] +arbitrary = [ + "dep:arbitrary", + "bls/arbitrary", + "ethereum_ssz/arbitrary", + "milhouse/arbitrary", + "ssz_types/arbitrary", + "swap_or_not_shuffle/arbitrary", +] +arbitrary-fuzz = ["arbitrary"] +portable = ["bls/supranational-portable"] [dependencies] alloy-primitives = { workspace = true } -alloy-rlp = { version = "0.3.4", features = ["derive"] } -# The arbitrary dependency is enabled by default since Capella to avoid complexity introduced by -# `AbstractExecPayload` -arbitrary = { workspace = true, features = ["derive"] } -bls = { workspace = true, features = ["arbitrary"] } +alloy-rlp = { workspace = true, features = ["derive"] } +arbitrary = { workspace = true, features = ["derive"], optional = true } +bls = { workspace = true } compare_fields = { workspace = true } -compare_fields_derive = { workspace = true } context_deserialize = { workspace = true } -context_deserialize_derive = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } -ethereum_ssz = { workspace = true, features = ["arbitrary"] } +ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } fixed_bytes = { workspace = true } hex = { workspace = true } @@ -36,7 +47,7 @@ metastruct = "0.1.0" milhouse = { workspace = true } parking_lot = { workspace = true } rand = { workspace = true } -rand_xorshift = "0.3.0" +rand_xorshift = "0.4.0" rayon = { workspace = true } regex = { workspace = true } rpds = { workspace = true } @@ -46,14 +57,15 @@ serde = { workspace = true, features = ["rc"] } serde_json = { workspace = true } serde_yaml = { workspace = true } smallvec = { workspace = true } -ssz_types = { workspace = true, features = ["arbitrary"] } +ssz_types = { workspace = true } superstruct = { workspace = true } -swap_or_not_shuffle = { workspace = true, features = ["arbitrary"] } +swap_or_not_shuffle = { workspace = true } tempfile = { workspace = true } test_random_derive = { path = "../../common/test_random_derive" } tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } +typenum = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } @@ -62,12 +74,9 @@ paste = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } -[features] -default = ["sqlite", "legacy-arith"] -# Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. -legacy-arith = [] -sqlite = ["dep:rusqlite"] -# The `arbitrary-fuzz` feature is a no-op provided for backwards compatibility. -# For simplicity `Arbitrary` is now derived regardless of the feature's presence. -arbitrary-fuzz = [] -portable = ["bls/supranational-portable"] +[lints.clippy] +module_inception = "allow" + +[[bench]] +name = "benches" +harness = false diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index 0c8bf36c81..397c33163e 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -1,11 +1,12 @@ -use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; +use criterion::{BatchSize, BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; +use fixed_bytes::FixedBytesExtended; use milhouse::List; use rayon::prelude::*; use ssz::Encode; use std::sync::Arc; use types::{ - test_utils::generate_deterministic_keypair, BeaconState, Epoch, Eth1Data, EthSpec, - FixedBytesExtended, Hash256, MainnetEthSpec, Validator, + BeaconState, Epoch, Eth1Data, EthSpec, Hash256, MainnetEthSpec, Validator, + test_utils::generate_deterministic_keypair, }; fn get_state(validator_count: usize) -> BeaconState { diff --git a/consensus/types/presets/gnosis/electra.yaml b/consensus/types/presets/gnosis/electra.yaml index 42afbb233e..6885667c6e 100644 --- a/consensus/types/presets/gnosis/electra.yaml +++ b/consensus/types/presets/gnosis/electra.yaml @@ -41,8 +41,7 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 # Withdrawals processing # --------------------------------------------------------------- -# 2**3 ( = 8) pending withdrawals -MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 +MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 6 # Pending deposits processing # --------------------------------------------------------------- diff --git a/consensus/types/presets/gnosis/fulu.yaml b/consensus/types/presets/gnosis/fulu.yaml index e5f3ce0212..b25d88f191 100644 --- a/consensus/types/presets/gnosis/fulu.yaml +++ b/consensus/types/presets/gnosis/fulu.yaml @@ -8,3 +8,7 @@ FIELD_ELEMENTS_PER_CELL: 64 FIELD_ELEMENTS_PER_EXT_BLOB: 8192 # uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# FIELD_ELEMENTS_PER_EXT_BLOB // FIELD_ELEMENTS_PER_CELL (= 128) +CELLS_PER_EXT_BLOB: 128 +# CELLS_PER_EXT_BLOB (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/gnosis/gloas.yaml b/consensus/types/presets/gnosis/gloas.yaml new file mode 100644 index 0000000000..170accaac3 --- /dev/null +++ b/consensus/types/presets/gnosis/gloas.yaml @@ -0,0 +1 @@ +# Gnosis preset - Gloas diff --git a/consensus/types/presets/mainnet/fulu.yaml b/consensus/types/presets/mainnet/fulu.yaml index 394f335f90..b7cad6239e 100644 --- a/consensus/types/presets/mainnet/fulu.yaml +++ b/consensus/types/presets/mainnet/fulu.yaml @@ -8,3 +8,7 @@ FIELD_ELEMENTS_PER_CELL: 64 FIELD_ELEMENTS_PER_EXT_BLOB: 8192 # uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# FIELD_ELEMENTS_PER_EXT_BLOB // FIELD_ELEMENTS_PER_CELL (= 128) +CELLS_PER_EXT_BLOB: 128 +# CELLS_PER_EXT_BLOB (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/mainnet/gloas.yaml b/consensus/types/presets/mainnet/gloas.yaml new file mode 100644 index 0000000000..45b7b8ae96 --- /dev/null +++ b/consensus/types/presets/mainnet/gloas.yaml @@ -0,0 +1 @@ +# Mainnet preset - Gloas diff --git a/consensus/types/presets/minimal/deneb.yaml b/consensus/types/presets/minimal/deneb.yaml index c101de3162..3096c605ab 100644 --- a/consensus/types/presets/minimal/deneb.yaml +++ b/consensus/types/presets/minimal/deneb.yaml @@ -4,7 +4,7 @@ # --------------------------------------------------------------- # `uint64(4096)` FIELD_ELEMENTS_PER_BLOB: 4096 -# [customized] -MAX_BLOB_COMMITMENTS_PER_BLOCK: 32 -# [customized] `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 5 = 10 -KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 10 +# `uint64(4096)` +MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 +# `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 +KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 diff --git a/consensus/types/presets/minimal/electra.yaml b/consensus/types/presets/minimal/electra.yaml index f99effe0f1..22e26e4025 100644 --- a/consensus/types/presets/minimal/electra.yaml +++ b/consensus/types/presets/minimal/electra.yaml @@ -32,10 +32,10 @@ MAX_ATTESTATIONS_ELECTRA: 8 # Execution # --------------------------------------------------------------- -# [customized] 2**2 (= 4) deposit requests -MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 4 -# [customized] 2**1 (= 2) withdrawal requests -MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2 +# 2**13 (= 8,192) deposit requests +MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 +# 2**4 (= 16) withdrawal requests +MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 # 2**1 (= 2) consolidation requests MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 diff --git a/consensus/types/presets/minimal/fulu.yaml b/consensus/types/presets/minimal/fulu.yaml index c961eb7f3c..d4b32eb876 100644 --- a/consensus/types/presets/minimal/fulu.yaml +++ b/consensus/types/presets/minimal/fulu.yaml @@ -8,3 +8,7 @@ FIELD_ELEMENTS_PER_CELL: 64 FIELD_ELEMENTS_PER_EXT_BLOB: 8192 # uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# FIELD_ELEMENTS_PER_EXT_BLOB // FIELD_ELEMENTS_PER_CELL (= 128) +CELLS_PER_EXT_BLOB: 128 +# CELLS_PER_EXT_BLOB (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/minimal/gloas.yaml b/consensus/types/presets/minimal/gloas.yaml new file mode 100644 index 0000000000..51b3f04857 --- /dev/null +++ b/consensus/types/presets/minimal/gloas.yaml @@ -0,0 +1 @@ +# Minimal preset - Gloas diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/attestation/aggregate_and_proof.rs similarity index 86% rename from consensus/types/src/aggregate_and_proof.rs rename to consensus/types/src/attestation/aggregate_and_proof.rs index a280afeaae..4c6e775e56 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/attestation/aggregate_and_proof.rs @@ -1,22 +1,24 @@ -use super::{AttestationBase, AttestationElectra, AttestationRef}; -use super::{ - ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, PublicKey, SecretKey, SelectionProof, - Signature, SignedRoot, -}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::Attestation; +use bls::{PublicKey, SecretKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + attestation::{ + Attestation, AttestationBase, AttestationElectra, AttestationRef, SelectionProof, + }, + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot}, + fork::{Fork, ForkName}, + test_utils::TestRandom, +}; + #[superstruct( variants(Base, Electra), variant_attributes( derive( - arbitrary::Arbitrary, Debug, Clone, PartialEq, @@ -29,23 +31,29 @@ use tree_hash_derive::TreeHash; ), context_deserialize(ForkName), serde(bound = "E: EthSpec"), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ), ref_attributes( - derive(Debug, PartialEq, TreeHash, Serialize,), + derive(Debug, PartialEq, TreeHash, Serialize), serde(untagged, bound = "E: EthSpec"), tree_hash(enum_behaviour = "transparent") ), map_ref_into(AttestationRef) )] -#[derive( - arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct AggregateAndProof { /// The index of the validator that created the attestation. #[serde(with = "serde_utils::quoted_u64")] diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation/attestation.rs similarity index 86% rename from consensus/types/src/attestation.rs rename to consensus/types/src/attestation/attestation.rs index 286e4622f8..693b5889f5 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -1,20 +1,26 @@ -use crate::context_deserialize; -use crate::slot_data::SlotData; -use crate::{test_utils::TestRandom, Hash256, Slot}; -use crate::{Checkpoint, ContextDeserialize, ForkName}; -use derivative::Derivative; +use std::{ + collections::HashSet, + hash::{Hash, Hasher}, +}; + +use bls::{AggregateSignature, SecretKey, Signature}; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::BitVector; -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; +use ssz_types::{BitList, BitVector}; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; -use super::{ - AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey, - Signature, SignedRoot, +use crate::{ + attestation::{ + AttestationData, Checkpoint, IndexedAttestation, IndexedAttestationBase, + IndexedAttestationElectra, + }, + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot, SlotData}, + fork::{Fork, ForkName}, + test_utils::TestRandom, }; #[derive(Debug, PartialEq, Clone)] @@ -44,35 +50,33 @@ impl From for Error { Decode, Encode, TestRandom, - Derivative, - arbitrary::Arbitrary, + Educe, TreeHash, ), context_deserialize(ForkName), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") + ) ), ref_attributes(derive(TreeHash), tree_hash(enum_behaviour = "transparent")), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] -#[derive( - Debug, - Clone, - Serialize, - TreeHash, - Encode, - Derivative, - Deserialize, - arbitrary::Arbitrary, - PartialEq, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, Serialize, TreeHash, Encode, Educe, Deserialize)] +#[educe(PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct Attestation { #[superstruct(only(Base), partial_getter(rename = "aggregation_bits_base"))] pub aggregation_bits: BitList, @@ -246,10 +250,17 @@ impl Attestation { attester_index: u64, ) -> Result { match self { - Self::Base(_) => Err(Error::IncorrectStateVariant), + Self::Base(attn) => attn.to_single_attestation_with_attester_index(attester_index), Self::Electra(attn) => attn.to_single_attestation_with_attester_index(attester_index), } } + + pub fn get_aggregation_bits(&self) -> Vec { + match self { + Self::Base(attn) => attn.get_aggregation_bits(), + Self::Electra(attn) => attn.get_aggregation_bits(), + } + } } impl AttestationRef<'_, E> { @@ -461,6 +472,26 @@ impl AttestationBase { ) -> Result, ssz::BitfieldError> { self.aggregation_bits.resize::() } + + pub fn get_aggregation_bits(&self) -> Vec { + self.aggregation_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect() + } + + pub fn to_single_attestation_with_attester_index( + &self, + attester_index: u64, + ) -> Result { + Ok(SingleAttestation { + committee_index: self.data.index, + attester_index, + data: self.data.clone(), + signature: self.signature.clone(), + }) + } } impl SlotData for Attestation { @@ -483,7 +514,7 @@ pub enum AttestationOnDisk { } impl AttestationOnDisk { - pub fn to_ref(&self) -> AttestationRefOnDisk { + pub fn to_ref(&self) -> AttestationRefOnDisk<'_, E> { match self { AttestationOnDisk::Base(att) => AttestationRefOnDisk::Base(att), AttestationOnDisk::Electra(att) => AttestationRefOnDisk::Electra(att), @@ -573,19 +604,8 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for Vec> } */ -#[derive( - Debug, - Clone, - Serialize, - Deserialize, - Decode, - Encode, - TestRandom, - Derivative, - arbitrary::Arbitrary, - TreeHash, - PartialEq, -)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, Clone, Serialize, Deserialize, Decode, Encode, TestRandom, TreeHash, PartialEq)] #[context_deserialize(ForkName)] pub struct SingleAttestation { #[serde(with = "serde_utils::quoted_u64")] @@ -596,6 +616,27 @@ pub struct SingleAttestation { pub signature: AggregateSignature, } +impl SingleAttestation { + pub fn to_indexed( + &self, + fork_name: ForkName, + ) -> Result, ssz_types::Error> { + if fork_name.electra_enabled() { + Ok(IndexedAttestation::Electra(IndexedAttestationElectra { + attesting_indices: vec![self.attester_index].try_into()?, + data: self.data.clone(), + signature: self.signature.clone(), + })) + } else { + Ok(IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices: vec![self.attester_index].try_into()?, + data: self.data.clone(), + signature: self.signature.clone(), + })) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -615,12 +656,12 @@ mod tests { let attestation_data = size_of::(); let signature = size_of::(); - assert_eq!(aggregation_bits, 152); + assert_eq!(aggregation_bits, 144); assert_eq!(attestation_data, 128); assert_eq!(signature, 288 + 16); let attestation_expected = aggregation_bits + attestation_data + signature; - assert_eq!(attestation_expected, 584); + assert_eq!(attestation_expected, 576); assert_eq!( size_of::>(), attestation_expected @@ -638,13 +679,13 @@ mod tests { size_of::::MaxCommitteesPerSlot>>(); let signature = size_of::(); - assert_eq!(aggregation_bits, 152); - assert_eq!(committee_bits, 152); + assert_eq!(aggregation_bits, 144); + assert_eq!(committee_bits, 144); assert_eq!(attestation_data, 128); assert_eq!(signature, 288 + 16); let attestation_expected = aggregation_bits + committee_bits + attestation_data + signature; - assert_eq!(attestation_expected, 736); + assert_eq!(attestation_expected, 720); assert_eq!( size_of::>(), attestation_expected diff --git a/consensus/types/src/attestation_data.rs b/consensus/types/src/attestation/attestation_data.rs similarity index 77% rename from consensus/types/src/attestation_data.rs rename to consensus/types/src/attestation/attestation_data.rs index d0d4dcc553..f3fceb9b70 100644 --- a/consensus/types/src/attestation_data.rs +++ b/consensus/types/src/attestation/attestation_data.rs @@ -1,16 +1,21 @@ -use crate::slot_data::SlotData; -use crate::test_utils::TestRandom; -use crate::{Checkpoint, ForkName, Hash256, SignedRoot, Slot}; -use context_deserialize_derive::context_deserialize; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; + +use crate::{ + attestation::Checkpoint, + core::{Hash256, SignedRoot, Slot, SlotData}, + fork::ForkName, + test_utils::TestRandom, +}; + /// The data upon which an attestation is based. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, Debug, Clone, PartialEq, diff --git a/consensus/types/src/attestation_duty.rs b/consensus/types/src/attestation/attestation_duty.rs similarity index 74% rename from consensus/types/src/attestation_duty.rs rename to consensus/types/src/attestation/attestation_duty.rs index 22b03dda61..fe3da79a2b 100644 --- a/consensus/types/src/attestation_duty.rs +++ b/consensus/types/src/attestation/attestation_duty.rs @@ -1,7 +1,9 @@ -use crate::*; use serde::{Deserialize, Serialize}; -#[derive(arbitrary::Arbitrary, Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)] +use crate::{attestation::CommitteeIndex, core::Slot}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)] pub struct AttestationDuty { /// The slot during which the attester must attest. pub slot: Slot, diff --git a/consensus/types/src/beacon_committee.rs b/consensus/types/src/attestation/beacon_committee.rs similarity index 76% rename from consensus/types/src/beacon_committee.rs rename to consensus/types/src/attestation/beacon_committee.rs index bdb91cd6e6..2dba30bad3 100644 --- a/consensus/types/src/beacon_committee.rs +++ b/consensus/types/src/attestation/beacon_committee.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{attestation::CommitteeIndex, core::Slot}; #[derive(Default, Clone, Debug, PartialEq)] pub struct BeaconCommittee<'a> { @@ -17,7 +17,8 @@ impl BeaconCommittee<'_> { } } -#[derive(arbitrary::Arbitrary, Default, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Default, Clone, Debug, PartialEq)] pub struct OwnedBeaconCommittee { pub slot: Slot, pub index: CommitteeIndex, diff --git a/consensus/types/src/checkpoint.rs b/consensus/types/src/attestation/checkpoint.rs similarity index 73% rename from consensus/types/src/checkpoint.rs rename to consensus/types/src/attestation/checkpoint.rs index c3cb1d5c36..f5a95f0ad9 100644 --- a/consensus/types/src/checkpoint.rs +++ b/consensus/types/src/attestation/checkpoint.rs @@ -1,16 +1,20 @@ -use crate::test_utils::TestRandom; -use crate::{Epoch, ForkName, Hash256}; -use context_deserialize_derive::context_deserialize; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Epoch, Hash256}, + fork::ForkName, + test_utils::TestRandom, +}; + /// Casper FFG checkpoint, used in attestations. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, Debug, Clone, Copy, diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/attestation/indexed_attestation.rs similarity index 91% rename from consensus/types/src/indexed_attestation.rs rename to consensus/types/src/attestation/indexed_attestation.rs index ea65d78504..272b015d90 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/attestation/indexed_attestation.rs @@ -1,17 +1,21 @@ -use crate::context_deserialize; -use crate::{ - test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, ForkName, VariableList, +use std::{ + hash::{Hash, Hasher}, + slice::Iter, }; -use core::slice::Iter; -use derivative::Derivative; + +use bls::AggregateSignature; +use context_deserialize::context_deserialize; +use educe::Educe; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use std::hash::{Hash, Hasher}; +use ssz_types::VariableList; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{attestation::AttestationData, core::EthSpec, fork::ForkName, test_utils::TestRandom}; + /// Details an attestation that can be slashable. /// /// To be included in an `AttesterSlashing`. @@ -28,32 +32,30 @@ use tree_hash_derive::TreeHash; Decode, Encode, TestRandom, - Derivative, - arbitrary::Arbitrary, + Educe, TreeHash, ), context_deserialize(ForkName), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ) )] -#[derive( - Debug, - Clone, - Serialize, - TreeHash, - Encode, - Derivative, - Deserialize, - arbitrary::Arbitrary, - PartialEq, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, Serialize, TreeHash, Encode, Educe, Deserialize)] +#[educe(PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct IndexedAttestation { /// Lists validator registry indices, not committee indices. #[superstruct(only(Base), partial_getter(rename = "attesting_indices_base"))] @@ -210,9 +212,10 @@ impl Hash for IndexedAttestation { #[cfg(test)] mod tests { use super::*; - use crate::slot_epoch::Epoch; - use crate::test_utils::{SeedableRng, XorShiftRng}; - use crate::MainnetEthSpec; + use crate::{ + core::{Epoch, MainnetEthSpec}, + test_utils::{SeedableRng, XorShiftRng}, + }; #[test] pub fn test_is_double_vote_true() { diff --git a/consensus/types/src/attestation/indexed_payload_attestation.rs b/consensus/types/src/attestation/indexed_payload_attestation.rs new file mode 100644 index 0000000000..4de805570c --- /dev/null +++ b/consensus/types/src/attestation/indexed_payload_attestation.rs @@ -0,0 +1,36 @@ +use crate::test_utils::TestRandom; +use crate::{EthSpec, ForkName, PayloadAttestationData}; +use bls::AggregateSignature; +use context_deserialize::context_deserialize; +use core::slice::Iter; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(TestRandom, TreeHash, Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[cfg_attr(feature = "arbitrary", arbitrary(bound = "E: EthSpec"))] +#[context_deserialize(ForkName)] +pub struct IndexedPayloadAttestation { + #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] + pub attesting_indices: VariableList, + pub data: PayloadAttestationData, + pub signature: AggregateSignature, +} + +impl IndexedPayloadAttestation { + pub fn attesting_indices_iter(&self) -> Iter<'_, u64> { + self.attesting_indices.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(IndexedPayloadAttestation); +} diff --git a/consensus/types/src/attestation/mod.rs b/consensus/types/src/attestation/mod.rs new file mode 100644 index 0000000000..586d99bd90 --- /dev/null +++ b/consensus/types/src/attestation/mod.rs @@ -0,0 +1,47 @@ +mod aggregate_and_proof; +mod attestation; +mod attestation_data; +mod attestation_duty; +mod beacon_committee; +mod checkpoint; +mod indexed_attestation; +mod indexed_payload_attestation; +mod participation_flags; +mod payload_attestation; +mod payload_attestation_data; +mod payload_attestation_message; +mod pending_attestation; +mod selection_proof; +mod shuffling_id; +mod signed_aggregate_and_proof; +mod subnet_id; + +pub use aggregate_and_proof::{ + AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, +}; +pub use attestation::{ + Attestation, AttestationBase, AttestationElectra, AttestationOnDisk, AttestationRef, + AttestationRefMut, AttestationRefOnDisk, Error as AttestationError, SingleAttestation, +}; +pub use attestation_data::AttestationData; +pub use attestation_duty::AttestationDuty; +pub use beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; +pub use checkpoint::Checkpoint; +pub use indexed_attestation::{ + IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, +}; +pub use indexed_payload_attestation::IndexedPayloadAttestation; +pub use participation_flags::ParticipationFlags; +pub use payload_attestation::PayloadAttestation; +pub use payload_attestation_data::PayloadAttestationData; +pub use payload_attestation_message::PayloadAttestationMessage; +pub use pending_attestation::PendingAttestation; +pub use selection_proof::SelectionProof; +pub use shuffling_id::AttestationShufflingId; +pub use signed_aggregate_and_proof::{ + SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, + SignedAggregateAndProofRefMut, +}; +pub use subnet_id::SubnetId; + +pub type CommitteeIndex = u64; diff --git a/consensus/types/src/participation_flags.rs b/consensus/types/src/attestation/participation_flags.rs similarity index 93% rename from consensus/types/src/participation_flags.rs rename to consensus/types/src/attestation/participation_flags.rs index e94e56f0cd..66831abfac 100644 --- a/consensus/types/src/participation_flags.rs +++ b/consensus/types/src/attestation/participation_flags.rs @@ -1,13 +1,17 @@ -use crate::{consts::altair::NUM_FLAG_INDICES, test_utils::TestRandom, Hash256}; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; use test_random_derive::TestRandom; use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; +use crate::{ + core::{Hash256, consts::altair::NUM_FLAG_INDICES}, + test_utils::TestRandom, +}; + #[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize, TestRandom)] #[serde(transparent)] -#[derive(arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct ParticipationFlags { #[serde(with = "serde_utils::quoted_u8")] bits: u8, diff --git a/consensus/types/src/attestation/payload_attestation.rs b/consensus/types/src/attestation/payload_attestation.rs new file mode 100644 index 0000000000..192a4a8fea --- /dev/null +++ b/consensus/types/src/attestation/payload_attestation.rs @@ -0,0 +1,31 @@ +use crate::attestation::payload_attestation_data::PayloadAttestationData; +use crate::test_utils::TestRandom; +use crate::{EthSpec, ForkName}; +use bls::AggregateSignature; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz::BitList; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +#[cfg_attr(feature = "arbitrary", arbitrary(bound = "E: EthSpec"))] +#[educe(PartialEq, Hash)] +#[context_deserialize(ForkName)] +pub struct PayloadAttestation { + pub aggregation_bits: BitList, + pub data: PayloadAttestationData, + pub signature: AggregateSignature, +} + +#[cfg(test)] +mod payload_attestation_tests { + use super::*; + use crate::MinimalEthSpec; + + ssz_and_tree_hash_tests!(PayloadAttestation); +} diff --git a/consensus/types/src/attestation/payload_attestation_data.rs b/consensus/types/src/attestation/payload_attestation_data.rs new file mode 100644 index 0000000000..58d36fd01d --- /dev/null +++ b/consensus/types/src/attestation/payload_attestation_data.rs @@ -0,0 +1,28 @@ +use crate::test_utils::TestRandom; +use crate::{ForkName, Hash256, SignedRoot, Slot}; +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive( + TestRandom, TreeHash, Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Hash, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[context_deserialize(ForkName)] +pub struct PayloadAttestationData { + pub beacon_block_root: Hash256, + pub slot: Slot, + pub payload_present: bool, + pub blob_data_available: bool, +} + +impl SignedRoot for PayloadAttestationData {} + +#[cfg(test)] +mod payload_attestation_data_tests { + use super::*; + + ssz_and_tree_hash_tests!(PayloadAttestationData); +} diff --git a/consensus/types/src/attestation/payload_attestation_message.rs b/consensus/types/src/attestation/payload_attestation_message.rs new file mode 100644 index 0000000000..82e2137b09 --- /dev/null +++ b/consensus/types/src/attestation/payload_attestation_message.rs @@ -0,0 +1,26 @@ +use crate::ForkName; +use crate::attestation::payload_attestation_data::PayloadAttestationData; +use crate::test_utils::TestRandom; +use bls::Signature; +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(TestRandom, TreeHash, Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[context_deserialize(ForkName)] +pub struct PayloadAttestationMessage { + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub data: PayloadAttestationData, + pub signature: Signature, +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(PayloadAttestationMessage); +} diff --git a/consensus/types/src/pending_attestation.rs b/consensus/types/src/attestation/pending_attestation.rs similarity index 65% rename from consensus/types/src/pending_attestation.rs rename to consensus/types/src/attestation/pending_attestation.rs index b7b4a19f4b..84353ac118 100644 --- a/consensus/types/src/pending_attestation.rs +++ b/consensus/types/src/attestation/pending_attestation.rs @@ -1,27 +1,21 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{AttestationData, BitList, EthSpec, ForkName}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use ssz_types::BitList; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{attestation::AttestationData, core::EthSpec, fork::ForkName, test_utils::TestRandom}; + /// An attestation that has been included in the state but not yet fully processed. /// /// Spec v0.12.1 -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[arbitrary(bound = "E: EthSpec")] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[context_deserialize(ForkName)] pub struct PendingAttestation { pub aggregation_bits: BitList, diff --git a/consensus/types/src/selection_proof.rs b/consensus/types/src/attestation/selection_proof.rs similarity index 88% rename from consensus/types/src/selection_proof.rs rename to consensus/types/src/attestation/selection_proof.rs index c80a00c3d1..b4c48d0078 100644 --- a/consensus/types/src/selection_proof.rs +++ b/consensus/types/src/attestation/selection_proof.rs @@ -1,12 +1,19 @@ -use crate::{ - ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, Signature, SignedRoot, Slot, -}; -use ethereum_hashing::hash; -use safe_arith::{ArithError, SafeArith}; -use ssz::Encode; use std::cmp; -#[derive(arbitrary::Arbitrary, PartialEq, Debug, Clone)] +use bls::{PublicKey, SecretKey, Signature}; +use ethereum_hashing::hash; +use safe_arith::{ArithError, SafeArith}; +use serde::{Deserialize, Serialize}; +use ssz::Encode; + +use crate::{ + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot}, + fork::Fork, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct SelectionProof(Signature); impl SelectionProof { diff --git a/consensus/types/src/shuffling_id.rs b/consensus/types/src/attestation/shuffling_id.rs similarity index 93% rename from consensus/types/src/shuffling_id.rs rename to consensus/types/src/attestation/shuffling_id.rs index df16f605ed..25217288f6 100644 --- a/consensus/types/src/shuffling_id.rs +++ b/consensus/types/src/attestation/shuffling_id.rs @@ -1,7 +1,12 @@ -use crate::*; +use std::hash::Hash; + use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use std::hash::Hash; + +use crate::{ + core::{Epoch, EthSpec, Hash256, RelativeEpoch}, + state::{BeaconState, BeaconStateError}, +}; /// Can be used to key (ID) the shuffling in some chain, in some epoch. /// diff --git a/consensus/types/src/signed_aggregate_and_proof.rs b/consensus/types/src/attestation/signed_aggregate_and_proof.rs similarity index 82% rename from consensus/types/src/signed_aggregate_and_proof.rs rename to consensus/types/src/attestation/signed_aggregate_and_proof.rs index 7b1f97e521..48c3f4c567 100644 --- a/consensus/types/src/signed_aggregate_and_proof.rs +++ b/consensus/types/src/attestation/signed_aggregate_and_proof.rs @@ -1,18 +1,21 @@ -use super::{ - AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, -}; -use super::{ - Attestation, AttestationRef, ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, - SelectionProof, Signature, SignedRoot, -}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; +use bls::{SecretKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + attestation::{ + AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, + Attestation, AttestationRef, SelectionProof, + }, + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot}, + fork::{Fork, ForkName}, + test_utils::TestRandom, +}; + /// A Validators signed aggregate proof to publish on the `beacon_aggregate_and_proof` /// gossipsub topic. /// @@ -21,7 +24,6 @@ use tree_hash_derive::TreeHash; variants(Base, Electra), variant_attributes( derive( - arbitrary::Arbitrary, Debug, Clone, PartialEq, @@ -34,19 +36,25 @@ use tree_hash_derive::TreeHash; ), context_deserialize(ForkName), serde(bound = "E: EthSpec"), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ), map_into(Attestation), map_ref_into(AggregateAndProofRef) )] -#[derive( - arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct SignedAggregateAndProof { /// The `AggregateAndProof` that was signed. #[superstruct(flatten)] diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/attestation/subnet_id.rs similarity index 93% rename from consensus/types/src/subnet_id.rs rename to consensus/types/src/attestation/subnet_id.rs index 7a5357c6cc..9585d077b5 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/attestation/subnet_id.rs @@ -1,17 +1,23 @@ //! Identifies each shard by an integer identifier. -use crate::SingleAttestation; -use crate::{AttestationRef, ChainSpec, CommitteeIndex, EthSpec, Slot}; -use alloy_primitives::{bytes::Buf, U256}; +use std::{ + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + +use alloy_primitives::{U256, bytes::Buf}; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; -use std::ops::{Deref, DerefMut}; -use std::sync::LazyLock; + +use crate::{ + attestation::{AttestationRef, CommitteeIndex, SingleAttestation}, + core::{ChainSpec, EthSpec, Slot}, +}; const MAX_SUBNET_ID: usize = 64; /// The number of bits in a Discovery `NodeId`. This is used for binary operations on the node-id /// data. -const NODE_ID_BITS: u64 = 256; +const NODE_ID_BITS: u32 = 256; static SUBNET_ID_TO_STRING: LazyLock> = LazyLock::new(|| { let mut v = Vec::with_capacity(MAX_SUBNET_ID); @@ -22,7 +28,8 @@ static SUBNET_ID_TO_STRING: LazyLock> = LazyLock::new(|| { v }); -#[derive(arbitrary::Arbitrary, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct SubnetId(#[serde(with = "serde_utils::quoted_u64")] u64); @@ -101,7 +108,7 @@ impl SubnetId { spec: &ChainSpec, ) -> impl Iterator { // The bits of the node-id we are using to define the subnets. - let prefix_bits = spec.attestation_subnet_prefix_bits as u64; + let prefix_bits = spec.attestation_subnet_prefix_bits as u32; let node_id = U256::from_be_slice(&raw_node_id); // calculate the prefixes used to compute the subnet and shuffling diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/block/beacon_block.rs similarity index 85% rename from consensus/types/src/beacon_block.rs rename to consensus/types/src/block/beacon_block.rs index 545f395d19..96999188f3 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/block/beacon_block.rs @@ -1,22 +1,45 @@ -use crate::attestation::AttestationBase; -use crate::test_utils::TestRandom; -use crate::*; -use derivative::Derivative; +use std::{fmt, marker::PhantomData}; + +use bls::{AggregateSignature, PublicKeyBytes, SecretKey, Signature, SignatureBytes}; +use context_deserialize::ContextDeserialize; +use educe::Educe; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; -use std::fmt; -use std::marker::PhantomData; +use ssz_types::{BitList, BitVector, FixedVector, VariableList}; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use typenum::Unsigned; -use self::indexed_attestation::IndexedAttestationBase; +use crate::{ + SignedExecutionPayloadBid, + attestation::{AttestationBase, AttestationData, IndexedAttestationBase}, + block::{ + BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, + BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyEip7805, + BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconBlockBodyGloas, BeaconBlockBodyRef, + BeaconBlockBodyRefMut, BeaconBlockHeader, SignedBeaconBlock, SignedBeaconBlockHeader, + }, + core::{ChainSpec, Domain, Epoch, EthSpec, Graffiti, Hash256, SignedRoot, Slot}, + deposit::{Deposit, DepositData}, + execution::{ + AbstractExecPayload, BlindedPayload, Eth1Data, ExecutionPayload, ExecutionRequests, + FullPayload, + }, + exit::{SignedVoluntaryExit, VoluntaryExit}, + fork::{Fork, ForkName, InconsistentFork, map_fork_name}, + slashing::{AttesterSlashingBase, ProposerSlashing}, + state::BeaconStateError, + sync_committee::SyncAggregate, + test_utils::TestRandom, +}; /// A block of the `BeaconChain`. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive( Debug, @@ -27,15 +50,18 @@ use self::indexed_attestation::IndexedAttestationBase; Decode, TreeHash, TestRandom, - Derivative, - arbitrary::Arbitrary + Educe, ), - derivative(PartialEq, Hash(bound = "E: EthSpec, Payload: AbstractExecPayload")), + educe(PartialEq, Hash(bound(E: EthSpec, Payload: AbstractExecPayload))), serde( bound = "E: EthSpec, Payload: AbstractExecPayload", deny_unknown_fields ), - arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload") + ) ), ref_attributes( derive(Debug, PartialEq, TreeHash), @@ -44,13 +70,15 @@ use self::indexed_attestation::IndexedAttestationBase; map_ref_into(BeaconBlockBodyRef, BeaconBlock), map_ref_mut_into(BeaconBlockBodyRefMut) )] -#[derive( - Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload") )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(untagged)] #[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] -#[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] pub struct BeaconBlock = FullPayload> { @@ -75,10 +103,12 @@ pub struct BeaconBlock = FullPayload pub body: BeaconBlockBodyDeneb, #[superstruct(only(Electra), partial_getter(rename = "body_electra"))] pub body: BeaconBlockBodyElectra, - #[superstruct(only(Eip7805), partial_getter(rename = "body_eip7805"))] - pub body: BeaconBlockBodyEip7805, #[superstruct(only(Fulu), partial_getter(rename = "body_fulu"))] pub body: BeaconBlockBodyFulu, + #[superstruct(only(Eip7805), partial_getter(rename = "body_eip7805"))] + pub body: BeaconBlockBodyEip7805, + #[superstruct(only(Gloas), partial_getter(rename = "body_gloas"))] + pub body: BeaconBlockBodyGloas, } pub type BlindedBeaconBlock = BeaconBlock>; @@ -131,9 +161,10 @@ impl> BeaconBlock { /// Usually it's better to prefer `from_ssz_bytes` which will decode the correct variant based /// on the fork slot. pub fn any_from_ssz_bytes(bytes: &[u8]) -> Result { - BeaconBlockFulu::from_ssz_bytes(bytes) - .map(BeaconBlock::Fulu) + BeaconBlockGloas::from_ssz_bytes(bytes) + .map(BeaconBlock::Gloas) .or_else(|_| BeaconBlockEip7805::from_ssz_bytes(bytes).map(BeaconBlock::Eip7805)) + .or_else(|_| BeaconBlockFulu::from_ssz_bytes(bytes).map(BeaconBlock::Fulu)) .or_else(|_| BeaconBlockElectra::from_ssz_bytes(bytes).map(BeaconBlock::Electra)) .or_else(|_| BeaconBlockDeneb::from_ssz_bytes(bytes).map(BeaconBlock::Deneb)) .or_else(|_| BeaconBlockCapella::from_ssz_bytes(bytes).map(BeaconBlock::Capella)) @@ -232,8 +263,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, E, Payl BeaconBlockRef::Capella { .. } => ForkName::Capella, BeaconBlockRef::Deneb { .. } => ForkName::Deneb, BeaconBlockRef::Electra { .. } => ForkName::Electra, - BeaconBlockRef::Eip7805 { .. } => ForkName::Eip7805, BeaconBlockRef::Fulu { .. } => ForkName::Fulu, + BeaconBlockRef::Eip7805 { .. } => ForkName::Eip7805, + BeaconBlockRef::Gloas { .. } => ForkName::Gloas, } } @@ -278,7 +310,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, E, Payl /// Extracts a reference to an execution payload from a block, returning an error if the block /// is pre-merge. - pub fn execution_payload(&self) -> Result, Error> { + pub fn execution_payload(&self) -> Result, BeaconStateError> { self.body().execution_payload() } } @@ -418,7 +450,10 @@ impl> EmptyBlock for BeaconBlockAlta /// Returns an empty Altair block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockAltair { - slot: spec.genesis_slot, + slot: spec + .altair_fork_epoch + .expect("altair enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -451,7 +486,10 @@ impl> BeaconBlockAltair sync_committee_bits: BitVector::default(), }; BeaconBlockAltair { - slot: spec.genesis_slot, + slot: spec + .altair_fork_epoch + .expect("altair enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -479,7 +517,10 @@ impl> EmptyBlock for BeaconBlockBell /// Returns an empty Bellatrix block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockBellatrix { - slot: spec.genesis_slot, + slot: spec + .bellatrix_fork_epoch + .expect("bellatrix enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -507,7 +548,10 @@ impl> EmptyBlock for BeaconBlockCape /// Returns an empty Capella block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockCapella { - slot: spec.genesis_slot, + slot: spec + .capella_fork_epoch + .expect("capella enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -536,7 +580,10 @@ impl> EmptyBlock for BeaconBlockDene /// Returns an empty Deneb block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockDeneb { - slot: spec.genesis_slot, + slot: spec + .deneb_fork_epoch + .expect("deneb enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -566,7 +613,10 @@ impl> EmptyBlock for BeaconBlockElec /// Returns an empty Electra block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockElectra { - slot: spec.genesis_slot, + slot: spec + .electra_fork_epoch + .expect("electra enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -628,7 +678,10 @@ impl> EmptyBlock for BeaconBlockFulu /// Returns an empty Fulu block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { BeaconBlockFulu { - slot: spec.genesis_slot, + slot: spec + .fulu_fork_epoch + .expect("fulu enabled") + .start_slot(E::slots_per_epoch()), proposer_index: 0, parent_root: Hash256::zero(), state_root: Hash256::zero(), @@ -655,6 +708,63 @@ impl> EmptyBlock for BeaconBlockFulu } } +impl> EmptyBlock for BeaconBlockGloas { + /// Returns an empty Gloas block to be used during genesis. + fn empty(spec: &ChainSpec) -> Self { + BeaconBlockGloas { + slot: spec.genesis_slot, + proposer_index: 0, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body: BeaconBlockBodyGloas { + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::zero(), + block_hash: Hash256::zero(), + deposit_count: 0, + }, + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + bls_to_execution_changes: VariableList::empty(), + signed_execution_payload_bid: SignedExecutionPayloadBid::empty(), + payload_attestations: VariableList::empty(), + _phantom: PhantomData, + }, + } + } +} + +// TODO(EIP-7732) Mark's branch had the following implementation but not sure if it's needed so will just add header below for reference +// impl> BeaconBlockEIP7732 { + +// TODO(EIP-7732) Look into whether we can remove this in the future since no blinded blocks post-gloas +impl From>> + for BeaconBlockGloas> +{ + fn from(block: BeaconBlockGloas>) -> Self { + let BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root, + body, + } = block; + + BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root, + body: body.into(), + } + } +} + // We can convert pre-Bellatrix blocks without payloads into blocks "with" payloads. impl From>> for BeaconBlockBase> @@ -738,6 +848,7 @@ impl_from!(BeaconBlockDeneb, >, >, |body: impl_from!(BeaconBlockElectra, >, >, |body: BeaconBlockBodyElectra<_, _>| body.into()); impl_from!(BeaconBlockEip7805, >, >, |body: BeaconBlockBodyEip7805<_, _>| body.into()); impl_from!(BeaconBlockFulu, >, >, |body: BeaconBlockBodyFulu<_, _>| body.into()); +impl_from!(BeaconBlockGloas, >, >, |body: BeaconBlockBodyGloas<_, _>| body.into()); // We can clone blocks with payloads to blocks without payloads, without cloning the payload. macro_rules! impl_clone_as_blinded { @@ -771,8 +882,9 @@ impl_clone_as_blinded!(BeaconBlockBellatrix, >, >, >); impl_clone_as_blinded!(BeaconBlockDeneb, >, >); impl_clone_as_blinded!(BeaconBlockElectra, >, >); -impl_clone_as_blinded!(BeaconBlockEip7805, >, >); impl_clone_as_blinded!(BeaconBlockFulu, >, >); +impl_clone_as_blinded!(BeaconBlockEip7805, >, >); +impl_clone_as_blinded!(BeaconBlockGloas, >, >); // A reference to a full beacon block can be cloned into a blinded beacon block, without cloning the // execution payload. @@ -817,6 +929,7 @@ impl<'de, E: EthSpec, Payload: AbstractExecPayload> ContextDeserialize<'de, F } } +#[derive(Clone, Copy)] pub enum BlockImportSource { Gossip, Lookup, @@ -838,7 +951,10 @@ impl fmt::Display for BlockImportSource { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::{test_ssz_tree_hash_pair_with, SeedableRng, XorShiftRng}; + use crate::{ + core::MainnetEthSpec, + test_utils::{SeedableRng, XorShiftRng, test_ssz_tree_hash_pair_with}, + }; use ssz::Encode; type BeaconBlock = super::BeaconBlock; @@ -981,6 +1097,26 @@ mod tests { }); } + #[test] + fn roundtrip_gloas_block() { + let rng = &mut XorShiftRng::from_seed([42; 16]); + let spec = &ForkName::Gloas.make_genesis_spec(MainnetEthSpec::default_spec()); + + let inner_block = BeaconBlockGloas { + slot: Slot::random_for_test(rng), + proposer_index: u64::random_for_test(rng), + parent_root: Hash256::random_for_test(rng), + state_root: Hash256::random_for_test(rng), + body: BeaconBlockBodyGloas::random_for_test(rng), + }; + + let block = BeaconBlock::Gloas(inner_block.clone()); + + test_ssz_tree_hash_pair_with(&block, &inner_block, |bytes| { + BeaconBlock::from_ssz_bytes(bytes, spec) + }); + } + #[test] fn decode_base_and_altair() { type E = MainnetEthSpec; @@ -1004,6 +1140,8 @@ mod tests { let eip7805_slot = eip7805_epoch.start_slot(E::slots_per_epoch()); let fulu_epoch = eip7805_epoch + 1; let fulu_slot = fulu_epoch.start_slot(E::slots_per_epoch()); + let gloas_epoch = fulu_epoch + 1; + let gloas_slot = gloas_epoch.start_slot(E::slots_per_epoch()); spec.altair_fork_epoch = Some(altair_epoch); spec.capella_fork_epoch = Some(capella_epoch); @@ -1011,6 +1149,7 @@ mod tests { spec.electra_fork_epoch = Some(electra_epoch); spec.eip7805_fork_epoch = Some(eip7805_epoch); spec.fulu_fork_epoch = Some(fulu_epoch); + spec.gloas_fork_epoch = Some(gloas_epoch); // BeaconBlockBase { @@ -1150,22 +1289,37 @@ mod tests { slot: fulu_slot, ..<_>::random_for_test(rng) }); - // It's invalid to have a Fulu block with a epoch lower than the fork epoch. - let _bad_block = { - let mut bad = good_block.clone(); - *bad.slot_mut() = electra_slot; - bad - }; assert_eq!( BeaconBlock::from_ssz_bytes(&good_block.as_ssz_bytes(), &spec) .expect("good fulu block can be decoded"), good_block ); - // TODO(fulu): Uncomment once Fulu has features since without features - // and with an Electra slot it decodes successfully to Electra. + } + + // BeaconBlockGloas + { + let good_block = BeaconBlock::Gloas(BeaconBlockGloas { + slot: gloas_slot, + ..<_>::random_for_test(rng) + }); + // It's invalid to have a Fulu block with a epoch lower than the fork epoch. + let _bad_block = { + let mut bad = good_block.clone(); + *bad.slot_mut() = fulu_slot; + bad + }; + + assert_eq!( + BeaconBlock::from_ssz_bytes(&good_block.as_ssz_bytes(), &spec) + .expect("good gloas block can be decoded"), + good_block + ); + + // TODO(gloas): Uncomment once Gloas has features since without features + // and with a Fulu slot it decodes successfully to Fulu. //BeaconBlock::from_ssz_bytes(&bad_block.as_ssz_bytes(), &spec) - // .expect_err("bad fulu block cannot be decoded"); + // .expect_err("bad gloas block cannot be decoded"); } } } diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs similarity index 82% rename from consensus/types/src/beacon_block_body.rs rename to consensus/types/src/block/beacon_block_body.rs index bf4b28e972..2bd5574765 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -1,18 +1,44 @@ -use crate::test_utils::TestRandom; -use crate::*; -use derivative::Derivative; +use std::marker::PhantomData; + +use bls::Signature; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use merkle_proof::{MerkleTree, MerkleTreeError}; use metastruct::metastruct; use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; -use std::marker::PhantomData; +use ssz_types::{FixedVector, VariableList}; use superstruct::superstruct; use test_random_derive::TestRandom; -use tree_hash::{TreeHash, BYTES_PER_CHUNK}; +use tree_hash::{BYTES_PER_CHUNK, TreeHash}; use tree_hash_derive::TreeHash; -pub type KzgCommitments = - VariableList::MaxBlobCommitmentsPerBlock>; +use crate::payload_attestation::PayloadAttestation; +use crate::{ + SignedExecutionPayloadBid, + attestation::{AttestationBase, AttestationElectra, AttestationRef, AttestationRefMut}, + core::{EthSpec, Graffiti, Hash256}, + deposit::Deposit, + execution::{ + AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, + BlindedPayloadDeneb, BlindedPayloadEip7805, BlindedPayloadElectra, BlindedPayloadFulu, + Eth1Data, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, + ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests, FullPayload, + FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, FullPayloadEip7805, + FullPayloadElectra, FullPayloadFulu, SignedBlsToExecutionChange, + }, + exit::SignedVoluntaryExit, + fork::{ForkName, map_fork_name}, + kzg_ext::KzgCommitments, + light_client::consts::{EXECUTION_PAYLOAD_INDEX, EXECUTION_PAYLOAD_PROOF_LEN}, + slashing::{ + AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingRef, ProposerSlashing, + }, + state::BeaconStateError, + sync_committee::SyncAggregate, + test_utils::TestRandom, +}; /// The number of leaves (including padding) on the `BeaconBlockBody` Merkle tree. /// @@ -28,7 +54,7 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; /// /// This *superstruct* abstracts over the hard-fork. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive( Debug, @@ -39,15 +65,18 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; Decode, TreeHash, TestRandom, - Derivative, - arbitrary::Arbitrary + Educe, ), - derivative(PartialEq, Hash(bound = "E: EthSpec, Payload: AbstractExecPayload")), + educe(PartialEq, Hash(bound(E: EthSpec, Payload: AbstractExecPayload))), serde( bound = "E: EthSpec, Payload: AbstractExecPayload", deny_unknown_fields ), - arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + ), context_deserialize(ForkName), ), specific_variant_attributes( @@ -57,18 +86,29 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; Capella(metastruct(mappings(beacon_block_body_capella_fields(groups(fields))))), Deneb(metastruct(mappings(beacon_block_body_deneb_fields(groups(fields))))), Electra(metastruct(mappings(beacon_block_body_electra_fields(groups(fields))))), + Fulu(metastruct(mappings(beacon_block_body_fulu_fields(groups(fields))))), Eip7805(metastruct(mappings(beacon_block_body_eip7805_fields(groups(fields))))), - Fulu(metastruct(mappings(beacon_block_body_fulu_fields(groups(fields))))) + Gloas(metastruct(mappings(beacon_block_body_gloas_fields(groups(fields))))), ), - cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) )] -#[derive(Debug, Clone, Serialize, Deserialize, Derivative, TreeHash, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload") +)] +#[derive(Debug, Clone, Serialize, Deserialize, Educe, TreeHash)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(untagged)] #[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] #[tree_hash(enum_behaviour = "transparent")] -#[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] pub struct BeaconBlockBody = FullPayload> { pub randao_reveal: Signature, pub eth1_data: Eth1Data, @@ -80,7 +120,7 @@ pub struct BeaconBlockBody = FullPay )] pub attester_slashings: VariableList, E::MaxAttesterSlashings>, #[superstruct( - only(Electra, Eip7805, Fulu), + only(Electra, Fulu, Eip7805, Gloas), partial_getter(rename = "attester_slashings_electra") )] pub attester_slashings: @@ -91,13 +131,13 @@ pub struct BeaconBlockBody = FullPay )] pub attestations: VariableList, E::MaxAttestations>, #[superstruct( - only(Electra, Eip7805, Fulu), + only(Electra, Fulu, Eip7805, Gloas), partial_getter(rename = "attestations_electra") )] pub attestations: VariableList, E::MaxAttestationsElectra>, pub deposits: VariableList, pub voluntary_exits: VariableList, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] pub sync_aggregate: SyncAggregate, // We flatten the execution payload so that serde can use the name of the inner type, // either `execution_payload` for full payloads, or `execution_payload_header` for blinded @@ -123,24 +163,28 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] #[serde(flatten)] pub execution_payload: Payload::Fulu, - #[superstruct(only(Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] pub bls_to_execution_changes: VariableList, #[superstruct(only(Deneb, Electra, Eip7805, Fulu))] pub blob_kzg_commitments: KzgCommitments, #[superstruct(only(Electra, Eip7805, Fulu))] pub execution_requests: ExecutionRequests, - #[superstruct(only(Base, Altair))] + #[superstruct(only(Gloas))] + pub signed_execution_payload_bid: SignedExecutionPayloadBid, + #[superstruct(only(Gloas))] + pub payload_attestations: VariableList, E::MaxPayloadAttestations>, + #[superstruct(only(Base, Altair, Gloas))] #[metastruct(exclude_from(fields))] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[serde(skip)] - #[arbitrary(default)] + #[cfg_attr(feature = "arbitrary", arbitrary(default))] pub _phantom: PhantomData, } impl> BeaconBlockBody { - pub fn execution_payload(&self) -> Result, Error> { + pub fn execution_payload(&self) -> Result, BeaconStateError> { self.to_ref().execution_payload() } @@ -151,19 +195,20 @@ impl> BeaconBlockBody { } impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { - pub fn execution_payload(&self) -> Result, Error> { + pub fn execution_payload(&self) -> Result, BeaconStateError> { match self { - Self::Base(_) | Self::Altair(_) => Err(Error::IncorrectStateVariant), + Self::Base(_) | Self::Altair(_) => Err(BeaconStateError::IncorrectStateVariant), Self::Bellatrix(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Capella(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Deneb(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Electra(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Eip7805(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Fulu(body) => Ok(Payload::Ref::from(&body.execution_payload)), + Self::Gloas(_) => Err(BeaconStateError::IncorrectStateVariant), } } - pub(crate) fn body_merkle_leaves(&self) -> Vec { + pub fn body_merkle_leaves(&self) -> Vec { let mut leaves = vec![]; match self { Self::Base(body) => { @@ -198,6 +243,10 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, beacon_block_body_fulu_fields!(body, |_, field| leaves .push(field.tree_hash_root())); } + Self::Gloas(body) => { + beacon_block_body_gloas_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } } leaves } @@ -209,7 +258,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, pub fn kzg_commitment_merkle_proof( &self, index: usize, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { let kzg_commitments_proof = self.kzg_commitments_merkle_proof()?; let proof = self.complete_kzg_commitment_merkle_proof(index, &kzg_commitments_proof)?; Ok(proof) @@ -217,16 +266,19 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, /// Produces the proof of inclusion for a `KzgCommitment` in `self.blob_kzg_commitments` /// at `index` using an existing proof for the `blob_kzg_commitments` field. + /// TODO(EIP7732) Investigate calling functions since this will no longer work for glas since no block_kzg_commitments in the body anymore pub fn complete_kzg_commitment_merkle_proof( &self, index: usize, kzg_commitments_proof: &[Hash256], - ) -> Result, Error> { + ) -> Result, BeaconStateError> { match self { - Self::Base(_) | Self::Altair(_) | Self::Bellatrix(_) | Self::Capella(_) => { - Err(Error::IncorrectStateVariant) - } - Self::Deneb(_) | Self::Electra(_) | Self::Eip7805(_) | Self::Fulu(_) => { + Self::Base(_) + | Self::Altair(_) + | Self::Bellatrix(_) + | Self::Capella(_) + | Self::Gloas(_) => Err(BeaconStateError::IncorrectStateVariant), + Self::Deneb(_) | Self::Electra(_) | Self::Fulu(_) | Self::Eip7805(_) => { // We compute the branches by generating 2 merkle trees: // 1. Merkle tree for the `blob_kzg_commitments` List object // 2. Merkle tree for the `BeaconBlockBody` container @@ -246,7 +298,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, let tree = MerkleTree::create(&blob_leaves, depth as usize); let (_, mut proof) = tree .generate_proof(index, depth as usize) - .map_err(Error::MerkleTreeError)?; + .map_err(BeaconStateError::MerkleTreeError)?; // Add the branch corresponding to the length mix-in. let length = blob_leaves.len(); @@ -254,7 +306,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, let mut length_bytes = [0; BYTES_PER_CHUNK]; length_bytes .get_mut(0..usize_len) - .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? + .ok_or(BeaconStateError::MerkleTreeError( + MerkleTreeError::PleaseNotifyTheDevs, + ))? .copy_from_slice(&length.to_le_bytes()); let length_root = Hash256::from_slice(length_bytes.as_slice()); proof.push(length_root); @@ -272,32 +326,41 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, /// Produces the proof of inclusion for `self.blob_kzg_commitments`. pub fn kzg_commitments_merkle_proof( &self, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { let body_leaves = self.body_merkle_leaves(); let beacon_block_body_depth = body_leaves.len().next_power_of_two().ilog2() as usize; let tree = MerkleTree::create(&body_leaves, beacon_block_body_depth); let (_, proof) = tree .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) - .map_err(Error::MerkleTreeError)?; + .map_err(BeaconStateError::MerkleTreeError)?; Ok(FixedVector::new(proof)?) } - pub fn block_body_merkle_proof(&self, generalized_index: usize) -> Result, Error> { + pub fn block_body_merkle_proof( + &self, + generalized_index: usize, + ) -> Result, BeaconStateError> { let field_index = match generalized_index { - light_client_update::EXECUTION_PAYLOAD_INDEX => { + EXECUTION_PAYLOAD_INDEX => { // Execution payload is a top-level field, subtract off the generalized indices // for the internal nodes. Result should be 9, the field offset of the execution // payload in the `BeaconBlockBody`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#beaconblockbody generalized_index .checked_sub(NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES) - .ok_or(Error::GeneralizedIndexNotSupported(generalized_index))? + .ok_or(BeaconStateError::GeneralizedIndexNotSupported( + generalized_index, + ))? + } + _ => { + return Err(BeaconStateError::GeneralizedIndexNotSupported( + generalized_index, + )); } - _ => return Err(Error::GeneralizedIndexNotSupported(generalized_index)), }; let leaves = self.body_merkle_leaves(); - let depth = light_client_update::EXECUTION_PAYLOAD_PROOF_LEN; + let depth = EXECUTION_PAYLOAD_PROOF_LEN; let tree = merkle_proof::MerkleTree::create(&leaves, depth); let (_, proof) = tree.generate_proof(field_index, depth)?; @@ -311,29 +374,17 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } pub fn attestations_len(&self) -> usize { - match self { - Self::Base(body) => body.attestations.len(), - Self::Altair(body) => body.attestations.len(), - Self::Bellatrix(body) => body.attestations.len(), - Self::Capella(body) => body.attestations.len(), - Self::Deneb(body) => body.attestations.len(), - Self::Electra(body) => body.attestations.len(), - Self::Eip7805(body) => body.attestations.len(), - Self::Fulu(body) => body.attestations.len(), - } + map_beacon_block_body_ref!(&'a _, self, |inner, cons| { + cons(inner); + inner.attestations.len() + }) } pub fn attester_slashings_len(&self) -> usize { - match self { - Self::Base(body) => body.attester_slashings.len(), - Self::Altair(body) => body.attester_slashings.len(), - Self::Bellatrix(body) => body.attester_slashings.len(), - Self::Capella(body) => body.attester_slashings.len(), - Self::Deneb(body) => body.attester_slashings.len(), - Self::Electra(body) => body.attester_slashings.len(), - Self::Eip7805(body) => body.attester_slashings.len(), - Self::Fulu(body) => body.attester_slashings.len(), - } + map_beacon_block_body_ref!(&'a _, self, |inner, cons| { + cons(inner); + inner.attester_slashings.len() + }) } pub fn attestations(&self) -> Box> + 'a> { @@ -346,6 +397,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Electra(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), Self::Eip7805(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), Self::Fulu(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), + Self::Gloas(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), } } @@ -391,6 +443,11 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, .iter() .map(AttesterSlashingRef::Electra), ), + Self::Gloas(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Electra), + ), } } } @@ -422,6 +479,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRefMut<'a, Self::Fulu(body) => { Box::new(body.attestations.iter_mut().map(AttestationRefMut::Electra)) } + Self::Gloas(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Electra)) + } } } } @@ -438,6 +498,7 @@ impl> BeaconBlockBodyRef<'_, E, Payl BeaconBlockBodyRef::Electra { .. } => ForkName::Electra, BeaconBlockBodyRef::Eip7805 { .. } => ForkName::Eip7805, BeaconBlockBodyRef::Fulu { .. } => ForkName::Fulu, + BeaconBlockBodyRef::Gloas { .. } => ForkName::Gloas, } } } @@ -505,6 +566,46 @@ impl From>> } } +// Post-Fulu block bodies without payloads can be converted into block bodies with payloads +// TODO(EIP-7732) Look into whether we can remove this in the future since no blinded blocks post-gloas +impl From>> + for BeaconBlockBodyGloas> +{ + fn from(body: BeaconBlockBodyGloas>) -> Self { + let BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + bls_to_execution_changes, + signed_execution_payload_bid, + payload_attestations, + _phantom, + } = body; + + BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + bls_to_execution_changes, + signed_execution_payload_bid, + payload_attestations, + _phantom: PhantomData, + } + } +} + // Likewise bodies with payloads can be transformed into bodies without. impl From>> for ( @@ -844,6 +945,50 @@ impl From>> } } +impl From>> + for ( + BeaconBlockBodyGloas>, + Option>, + ) +{ + fn from(body: BeaconBlockBodyGloas>) -> Self { + let BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + bls_to_execution_changes, + signed_execution_payload_bid, + payload_attestations, + _phantom, + } = body; + + ( + BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + bls_to_execution_changes, + signed_execution_payload_bid, + payload_attestations, + _phantom: PhantomData, + }, + None, + ) + } +} + // We can clone a full block into a blinded block, without cloning the payload. impl BeaconBlockBodyBase> { pub fn clone_as_blinded(&self) -> BeaconBlockBodyBase> { @@ -1075,6 +1220,13 @@ impl BeaconBlockBodyFulu> { } } +impl BeaconBlockBodyGloas> { + pub fn clone_as_blinded(&self) -> BeaconBlockBodyGloas> { + let (block_body, _payload) = self.clone().into(); + block_body + } +} + impl From>> for ( BeaconBlockBody>, @@ -1105,22 +1257,16 @@ impl<'de, E: EthSpec, Payload: AbstractExecPayload> ContextDeserialize<'de, F } } -/// Util method helpful for logging. -pub fn format_kzg_commitments(commitments: &[KzgCommitment]) -> String { - let commitment_strings: Vec = commitments.iter().map(|x| x.to_string()).collect(); - let commitments_joined = commitment_strings.join(", "); - let surrounded_commitments = format!("[{}]", commitments_joined); - surrounded_commitments -} - #[cfg(test)] mod tests { mod base { use super::super::*; + use crate::core::MainnetEthSpec; ssz_and_tree_hash_tests!(BeaconBlockBodyBase); } mod altair { use super::super::*; + use crate::core::MainnetEthSpec; ssz_and_tree_hash_tests!(BeaconBlockBodyAltair); } } diff --git a/consensus/types/src/beacon_block_header.rs b/consensus/types/src/block/beacon_block_header.rs similarity index 80% rename from consensus/types/src/beacon_block_header.rs rename to consensus/types/src/block/beacon_block_header.rs index 8416f975db..06e1023d91 100644 --- a/consensus/types/src/beacon_block_header.rs +++ b/consensus/types/src/block/beacon_block_header.rs @@ -1,29 +1,24 @@ -use crate::test_utils::TestRandom; -use crate::*; - -use context_deserialize_derive::context_deserialize; +use bls::SecretKey; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + block::SignedBeaconBlockHeader, + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot}, + fork::{Fork, ForkName}, + test_utils::TestRandom, +}; + /// A header of a `BeaconBlock`. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct BeaconBlockHeader { diff --git a/consensus/types/src/block/mod.rs b/consensus/types/src/block/mod.rs new file mode 100644 index 0000000000..94c4a1da8d --- /dev/null +++ b/consensus/types/src/block/mod.rs @@ -0,0 +1,26 @@ +mod beacon_block; +mod beacon_block_body; +mod beacon_block_header; +mod signed_beacon_block; +mod signed_beacon_block_header; + +pub use beacon_block::{ + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockCapella, + BeaconBlockDeneb, BeaconBlockEip7805, BeaconBlockElectra, BeaconBlockFulu, BeaconBlockGloas, + BeaconBlockRef, BeaconBlockRefMut, BlindedBeaconBlock, BlockImportSource, EmptyBlock, +}; +pub use beacon_block_body::{ + BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockBody, BeaconBlockBodyAltair, BeaconBlockBodyBase, + BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyEip7805, + BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconBlockBodyGloas, BeaconBlockBodyRef, + BeaconBlockBodyRefMut, NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES, +}; +pub use beacon_block_header::BeaconBlockHeader; + +pub use signed_beacon_block::{ + SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, + SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, + SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockGloas, SignedBeaconBlockHash, + SignedBlindedBeaconBlock, ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc, +}; +pub use signed_beacon_block_header::SignedBeaconBlockHeader; diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs similarity index 88% rename from consensus/types/src/signed_beacon_block.rs rename to consensus/types/src/block/signed_beacon_block.rs index 73f86827c0..6444ecd415 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -1,17 +1,44 @@ -use crate::beacon_block_body::{format_kzg_commitments, BLOB_KZG_COMMITMENTS_INDEX}; -use crate::test_utils::TestRandom; -use crate::*; -use derivative::Derivative; +use std::fmt; + +use bls::{PublicKey, Signature}; +use context_deserialize::ContextDeserialize; +use educe::Educe; use merkle_proof::MerkleTree; use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; -use std::fmt; +use ssz_types::FixedVector; use superstruct::superstruct; use test_random_derive::TestRandom; +use tracing::instrument; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -#[derive(arbitrary::Arbitrary, PartialEq, Eq, Hash, Clone, Copy)] +use crate::{ + block::{ + BLOB_KZG_COMMITMENTS_INDEX, BeaconBlock, BeaconBlockAltair, BeaconBlockBase, + BeaconBlockBellatrix, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, + BeaconBlockBodyDeneb, BeaconBlockBodyEip7805, BeaconBlockBodyElectra, BeaconBlockBodyFulu, + BeaconBlockCapella, BeaconBlockDeneb, BeaconBlockEip7805, BeaconBlockElectra, + BeaconBlockFulu, BeaconBlockGloas, BeaconBlockHeader, BeaconBlockRef, BeaconBlockRefMut, + SignedBeaconBlockHeader, + }, + core::{ChainSpec, Domain, Epoch, EthSpec, Hash256, SignedRoot, SigningData, Slot}, + execution::{ + AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, + BlindedPayloadDeneb, BlindedPayloadEip7805, BlindedPayloadElectra, BlindedPayloadFulu, + ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, + ExecutionPayloadFulu, FullPayload, FullPayloadBellatrix, FullPayloadCapella, + FullPayloadDeneb, FullPayloadEip7805, FullPayloadElectra, FullPayloadFulu, + }, + fork::{Fork, ForkName, ForkVersionDecode, InconsistentFork, map_fork_name}, + kzg_ext::format_kzg_commitments, + state::BeaconStateError, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Eq, Hash, Clone, Copy)] pub struct SignedBeaconBlockHash(Hash256); impl fmt::Debug for SignedBeaconBlockHash { @@ -40,7 +67,7 @@ impl From for Hash256 { /// A `BeaconBlock` and a signature from its proposer. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive( Debug, @@ -50,25 +77,30 @@ impl From for Hash256 { Encode, Decode, TreeHash, - Derivative, - arbitrary::Arbitrary, + Educe, TestRandom ), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec, Payload: AbstractExecPayload"), - arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), + ), ), map_into(BeaconBlock), map_ref_into(BeaconBlockRef), map_ref_mut_into(BeaconBlockRefMut) )] -#[derive( - Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload") )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(untagged)] #[serde(bound = "E: EthSpec, Payload: AbstractExecPayload")] -#[arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] pub struct SignedBeaconBlock = FullPayload> { @@ -88,6 +120,8 @@ pub struct SignedBeaconBlock = FullP pub message: BeaconBlockEip7805, #[superstruct(only(Fulu), partial_getter(rename = "message_fulu"))] pub message: BeaconBlockFulu, + #[superstruct(only(Gloas), partial_getter(rename = "message_gloas"))] + pub message: BeaconBlockGloas, pub signature: Signature, } @@ -177,6 +211,9 @@ impl> SignedBeaconBlock BeaconBlock::Fulu(message) => { SignedBeaconBlock::Fulu(SignedBeaconBlockFulu { message, signature }) } + BeaconBlock::Gloas(message) => { + SignedBeaconBlock::Gloas(SignedBeaconBlockGloas { message, signature }) + } } } @@ -247,6 +284,7 @@ impl> SignedBeaconBlock } /// Produce a signed beacon block header corresponding to this block. + #[instrument(level = "debug", skip_all)] pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { SignedBeaconBlockHeader { message: self.message().block_header(), @@ -264,7 +302,7 @@ impl> SignedBeaconBlock SignedBeaconBlockHeader, FixedVector, ), - Error, + BeaconStateError, > { // Create the block body merkle tree let body_leaves = self.message().body().body_merkle_leaves(); @@ -274,7 +312,7 @@ impl> SignedBeaconBlock // Compute the KZG commitments inclusion proof let (_, proof) = body_merkle_tree .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) - .map_err(Error::MerkleTreeError)?; + .map_err(BeaconStateError::MerkleTreeError)?; let kzg_commitments_inclusion_proof = FixedVector::new(proof)?; let block_header = BeaconBlockHeader { @@ -700,6 +738,20 @@ impl SignedBeaconBlockFulu> { } } +// We can convert gloas blocks without payloads into blocks "with" payloads. +// TODO(EIP-7732) Look into whether we can remove this in the future since no blinded blocks post-gloas +impl From>> + for SignedBeaconBlockGloas> +{ + fn from(signed_block: SignedBeaconBlockGloas>) -> Self { + let SignedBeaconBlockGloas { message, signature } = signed_block; + SignedBeaconBlockGloas { + message: message.into(), + signature, + } + } +} + impl SignedBeaconBlock> { pub fn try_into_full_block( self, @@ -726,6 +778,7 @@ impl SignedBeaconBlock> { (SignedBeaconBlock::Fulu(block), Some(ExecutionPayload::Fulu(payload))) => { SignedBeaconBlock::Fulu(block.into_full_block(payload)) } + (SignedBeaconBlock::Gloas(block), _) => SignedBeaconBlock::Gloas(block.into()), // avoid wildcard matching forks so that compiler will // direct us here when a new fork has been added (SignedBeaconBlock::Bellatrix(_), _) => return None, @@ -734,6 +787,7 @@ impl SignedBeaconBlock> { (SignedBeaconBlock::Electra(_), _) => return None, (SignedBeaconBlock::Eip7805(_), _) => return None, (SignedBeaconBlock::Fulu(_), _) => return None, + // TODO(EIP-7732) Determine if need a match arm for gloas here }; Some(full_block) } @@ -882,6 +936,9 @@ pub mod ssz_tagged_signed_beacon_block { ForkName::Fulu => Ok(SignedBeaconBlock::Fulu( SignedBeaconBlockFulu::from_ssz_bytes(body)?, )), + ForkName::Gloas => Ok(SignedBeaconBlock::Gloas( + SignedBeaconBlockGloas::from_ssz_bytes(body)?, + )), } } } @@ -911,6 +968,7 @@ pub mod ssz_tagged_signed_beacon_block_arc { #[cfg(test)] mod test { use super::*; + use crate::{block::EmptyBlock, core::MainnetEthSpec}; #[test] fn add_remove_payload_roundtrip() { @@ -953,11 +1011,26 @@ mod test { } } + fn spec_with_all_forks_enabled() -> ChainSpec { + let mut chain_spec = E::default_spec(); + chain_spec.altair_fork_epoch = Some(Epoch::new(1)); + chain_spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + chain_spec.capella_fork_epoch = Some(Epoch::new(3)); + chain_spec.deneb_fork_epoch = Some(Epoch::new(4)); + chain_spec.electra_fork_epoch = Some(Epoch::new(5)); + chain_spec.fulu_fork_epoch = Some(Epoch::new(6)); + chain_spec.gloas_fork_epoch = Some(Epoch::new(7)); + + // check that we have all forks covered + assert!(chain_spec.fork_epoch(ForkName::latest()).is_some()); + chain_spec + } + #[test] fn test_ssz_tagged_signed_beacon_block() { type E = MainnetEthSpec; - let spec = &E::default_spec(); + let spec = &spec_with_all_forks_enabled::(); let sig = Signature::empty(); let blocks = vec![ SignedBeaconBlock::::from_block( @@ -984,11 +1057,15 @@ mod test { BeaconBlock::Electra(BeaconBlockElectra::empty(spec)), sig.clone(), ), + SignedBeaconBlock::from_block( + BeaconBlock::Fulu(BeaconBlockFulu::empty(spec)), + sig.clone(), + ), SignedBeaconBlock::from_block( BeaconBlock::Eip7805(BeaconBlockEip7805::empty(spec)), sig.clone(), ), - SignedBeaconBlock::from_block(BeaconBlock::Fulu(BeaconBlockFulu::empty(spec)), sig), + SignedBeaconBlock::from_block(BeaconBlock::Gloas(BeaconBlockGloas::empty(spec)), sig), ]; for block in blocks { diff --git a/consensus/types/src/signed_beacon_block_header.rs b/consensus/types/src/block/signed_beacon_block_header.rs similarity index 73% rename from consensus/types/src/signed_beacon_block_header.rs rename to consensus/types/src/block/signed_beacon_block_header.rs index 9106fa8372..2fcd8a705f 100644 --- a/consensus/types/src/signed_beacon_block_header.rs +++ b/consensus/types/src/block/signed_beacon_block_header.rs @@ -1,29 +1,23 @@ -use crate::context_deserialize; -use crate::{ - test_utils::TestRandom, BeaconBlockHeader, ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, - PublicKey, Signature, SignedRoot, -}; +use bls::{PublicKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + block::BeaconBlockHeader, + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot}, + fork::{Fork, ForkName}, + test_utils::TestRandom, +}; + /// A signed header of a `BeaconBlock`. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Eq, - Hash, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct SignedBeaconBlockHeader { diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder/builder_bid.rs similarity index 91% rename from consensus/types/src/builder_bid.rs rename to consensus/types/src/builder/builder_bid.rs index 775902f78f..ceb700e5d3 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder/builder_bid.rs @@ -1,14 +1,6 @@ -use crate::beacon_block_body::KzgCommitments; -use crate::test_utils::TestRandom; -use crate::{ - ChainSpec, ContextDeserialize, EthSpec, ExecutionPayloadHeaderBellatrix, - ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, - ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, SignedRoot, - Uint256, -}; use bls::PublicKeyBytes; use bls::Signature; +use context_deserialize::ContextDeserialize; use serde::{Deserialize, Deserializer, Serialize}; use ssz::Decode; use ssz_derive::{Decode, Encode}; @@ -16,6 +8,19 @@ use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, EthSpec, SignedRoot, Uint256}, + execution::{ + ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, ExecutionPayloadHeaderElectra, + ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, + ExecutionRequests, + }, + fork::{ForkName, ForkVersionDecode}, + kzg_ext::KzgCommitments, + test_utils::TestRandom, +}; + #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), variant_attributes( @@ -87,10 +92,10 @@ impl ForkVersionDecode for BuilderBid { /// SSZ decode with explicit fork variant. fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { let builder_bid = match fork_name { - ForkName::Altair | ForkName::Base => { + ForkName::Altair | ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "unsupported fork for ExecutionPayloadHeader: {fork_name}", - ))) + ))); } ForkName::Bellatrix => { BuilderBid::Bellatrix(BuilderBidBellatrix::from_ssz_bytes(bytes)?) @@ -158,7 +163,7 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for BuilderBid { ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } - ForkName::Base | ForkName::Altair => { + ForkName::Base | ForkName::Altair | ForkName::Gloas => { return Err(serde::de::Error::custom(format!( "BuilderBid failed to deserialize: unsupported fork '{}'", context diff --git a/consensus/types/src/builder/builder_pending_payment.rs b/consensus/types/src/builder/builder_pending_payment.rs new file mode 100644 index 0000000000..0f1b68ad97 --- /dev/null +++ b/consensus/types/src/builder/builder_pending_payment.rs @@ -0,0 +1,36 @@ +use crate::test_utils::TestRandom; +use crate::{BuilderPendingWithdrawal, ForkName}; +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive( + Debug, + PartialEq, + Eq, + Hash, + Clone, + Default, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + TestRandom, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[context_deserialize(ForkName)] +pub struct BuilderPendingPayment { + #[serde(with = "serde_utils::quoted_u64")] + pub weight: u64, + pub withdrawal: BuilderPendingWithdrawal, +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(BuilderPendingPayment); +} diff --git a/consensus/types/src/withdrawal.rs b/consensus/types/src/builder/builder_pending_withdrawal.rs similarity index 61% rename from consensus/types/src/withdrawal.rs rename to consensus/types/src/builder/builder_pending_withdrawal.rs index 9ca50fccfb..436d331c00 100644 --- a/consensus/types/src/withdrawal.rs +++ b/consensus/types/src/builder/builder_pending_withdrawal.rs @@ -1,17 +1,18 @@ use crate::test_utils::TestRandom; -use crate::*; +use crate::{Address, Epoch, ForkName}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[derive( - arbitrary::Arbitrary, Debug, PartialEq, Eq, Hash, Clone, + Default, Serialize, Deserialize, Encode, @@ -19,21 +20,21 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[context_deserialize(ForkName)] -pub struct Withdrawal { - #[serde(with = "serde_utils::quoted_u64")] - pub index: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub validator_index: u64, +pub struct BuilderPendingWithdrawal { #[serde(with = "serde_utils::address_hex")] - pub address: Address, + pub fee_recipient: Address, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub builder_index: u64, + pub withdrawable_epoch: Epoch, } #[cfg(test)] mod tests { use super::*; - ssz_and_tree_hash_tests!(Withdrawal); + ssz_and_tree_hash_tests!(BuilderPendingWithdrawal); } diff --git a/consensus/types/src/builder/mod.rs b/consensus/types/src/builder/mod.rs new file mode 100644 index 0000000000..ef17566cae --- /dev/null +++ b/consensus/types/src/builder/mod.rs @@ -0,0 +1,10 @@ +mod builder_bid; +mod builder_pending_payment; +mod builder_pending_withdrawal; + +pub use builder_bid::{ + BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidEip7805, + BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, +}; +pub use builder_pending_payment::BuilderPendingPayment; +pub use builder_pending_withdrawal::BuilderPendingWithdrawal; diff --git a/consensus/types/src/consolidation_request.rs b/consensus/types/src/consolidation/consolidation_request.rs similarity index 70% rename from consensus/types/src/consolidation_request.rs rename to consensus/types/src/consolidation/consolidation_request.rs index c7375dab84..3f09517a90 100644 --- a/consensus/types/src/consolidation_request.rs +++ b/consensus/types/src/consolidation/consolidation_request.rs @@ -1,24 +1,20 @@ -use crate::context_deserialize; -use crate::{test_utils::TestRandom, Address, ForkName, PublicKeyBytes, SignedRoot}; +use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Address, SignedRoot}, + fork::ForkName, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct ConsolidationRequest { diff --git a/consensus/types/src/consolidation/mod.rs b/consensus/types/src/consolidation/mod.rs new file mode 100644 index 0000000000..a6a2f4a331 --- /dev/null +++ b/consensus/types/src/consolidation/mod.rs @@ -0,0 +1,5 @@ +mod consolidation_request; +mod pending_consolidation; + +pub use consolidation_request::ConsolidationRequest; +pub use pending_consolidation::PendingConsolidation; diff --git a/consensus/types/src/pending_consolidation.rs b/consensus/types/src/consolidation/pending_consolidation.rs similarity index 63% rename from consensus/types/src/pending_consolidation.rs rename to consensus/types/src/consolidation/pending_consolidation.rs index 9a513f2744..fcd76e43b6 100644 --- a/consensus/types/src/pending_consolidation.rs +++ b/consensus/types/src/consolidation/pending_consolidation.rs @@ -1,24 +1,14 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::ForkName; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct PendingConsolidation { diff --git a/consensus/types/src/application_domain.rs b/consensus/types/src/core/application_domain.rs similarity index 100% rename from consensus/types/src/application_domain.rs rename to consensus/types/src/core/application_domain.rs diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/core/chain_spec.rs similarity index 71% rename from consensus/types/src/chain_spec.rs rename to consensus/types/src/core/chain_spec.rs index c36a9209a0..e423fd46fd 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -1,17 +1,27 @@ -use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER}; -use crate::blob_sidecar::BlobIdentifier; -use crate::data_column_sidecar::DataColumnsByRootIdentifier; -use crate::*; +use std::{fs::File, path::Path, time::Duration}; + +use educe::Educe; +use ethereum_hashing::hash; +use fixed_bytes::FixedBytesExtended; use int_to_bytes::int_to_bytes4; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_utils::quoted_u64::MaybeQuoted; use ssz::Encode; -use std::fs::File; -use std::path::Path; -use std::time::Duration; +use ssz_types::{RuntimeVariableList, VariableList}; use tree_hash::TreeHash; +use crate::{ + core::{ + APPLICATION_DOMAIN_BUILDER, Address, ApplicationDomain, EnrForkId, Epoch, EthSpec, + EthSpecId, Hash256, MainnetEthSpec, Slot, Uint256, + }, + data::{BlobIdentifier, DataColumnSubnetId, DataColumnsByRootIdentifier}, + execution::ExecutionBlockHash, + fork::{Fork, ForkData, ForkName}, + state::BeaconState, +}; + /// Each of the BLS signature domains. #[derive(Debug, PartialEq, Clone, Copy)] pub enum Domain { @@ -26,6 +36,8 @@ pub enum Domain { SyncCommittee, ContributionAndProof, SyncCommitteeSelectionProof, + BeaconBuilder, + PTCAttester, ApplicationMask(ApplicationDomain), InclusionListCommittee, } @@ -33,7 +45,8 @@ pub enum Domain { /// Lighthouse's internal configuration struct. /// /// Contains a mixture of "preset" and "config" values w.r.t to the EF definitions. -#[derive(arbitrary::Arbitrary, PartialEq, Debug, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Debug, Clone)] pub struct ChainSpec { /* * Config name @@ -79,18 +92,25 @@ pub struct ChainSpec { pub bls_withdrawal_prefix_byte: u8, pub eth1_address_withdrawal_prefix_byte: u8, pub compounding_withdrawal_prefix_byte: u8, + pub builder_withdrawal_prefix_byte: u8, /* * Time parameters */ pub genesis_delay: u64, pub seconds_per_slot: u64, + pub slot_duration_ms: u64, pub min_attestation_inclusion_delay: u64, pub min_seed_lookahead: Epoch, pub max_seed_lookahead: Epoch, pub min_epochs_to_inactivity_penalty: u64, pub min_validator_withdrawability_delay: Epoch, pub shard_committee_period: u64, + pub proposer_reorg_cutoff_bps: u64, + pub attestation_due_bps: u64, + pub aggregate_due_bps: u64, + pub sync_message_due_bps: u64, + pub contribution_due_bps: u64, /* * Reward and penalty quotients @@ -111,6 +131,8 @@ pub struct ChainSpec { pub(crate) domain_voluntary_exit: u32, pub(crate) domain_selection_proof: u32, pub(crate) domain_aggregate_and_proof: u32, + pub(crate) domain_beacon_builder: u32, + pub(crate) domain_ptc_attester: u32, /* * Fork choice @@ -207,11 +229,21 @@ pub struct ChainSpec { pub fulu_fork_version: [u8; 4], /// The Fulu fork epoch is optional, with `None` representing "Fulu never happens". pub fulu_fork_epoch: Option, - pub number_of_columns: u64, pub number_of_custody_groups: u64, pub data_column_sidecar_subnet_count: u64, pub samples_per_slot: u64, pub custody_requirement: u64, + pub validator_custody_requirement: u64, + pub balance_per_additional_custody_group: u64, + + /* + * Gloas hard fork params + */ + pub gloas_fork_version: [u8; 4], + /// The Gloas fork epoch is optional, with `None` representing "Gloas never happens". + pub gloas_fork_epoch: Option, + pub builder_payment_threshold_numerator: u64, + pub builder_payment_threshold_denominator: u64, /* * Networking @@ -225,7 +257,7 @@ pub struct ChainSpec { pub ttfb_timeout: u64, pub resp_timeout: u64, pub attestation_propagation_slot_range: u64, - pub maximum_gossip_clock_disparity_millis: u64, + pub maximum_gossip_clock_disparity: u64, pub message_domain_invalid_snappy: [u8; 4], pub message_domain_valid_snappy: [u8; 4], pub subnets_per_node: u8, @@ -252,7 +284,12 @@ pub struct ChainSpec { /* * Networking Fulu */ - max_blobs_per_block_fulu: u64, + pub(crate) blob_schedule: BlobSchedule, + pub min_epochs_for_data_column_sidecars_requests: u64, + + /* + * Networking Gloas + */ /* * Networking Derived @@ -289,27 +326,15 @@ impl ChainSpec { genesis_validators_root: Hash256, ) -> EnrForkId { EnrForkId { - fork_digest: self.fork_digest::(slot, genesis_validators_root), + fork_digest: self + .compute_fork_digest(genesis_validators_root, slot.epoch(E::slots_per_epoch())), next_fork_version: self.next_fork_version::(slot), next_fork_epoch: self - .next_fork_epoch::(slot) - .map(|(_, e)| e) + .next_digest_epoch(slot.epoch(E::slots_per_epoch())) .unwrap_or(self.far_future_epoch), } } - /// Returns the `ForkDigest` for the given slot. - /// - /// If `self.altair_fork_epoch == None`, then this function returns the genesis fork digest - /// otherwise, returns the fork digest based on the slot. - pub fn fork_digest(&self, slot: Slot, genesis_validators_root: Hash256) -> [u8; 4] { - let fork_name = self.fork_name_at_slot::(slot); - Self::compute_fork_digest( - self.fork_version_for_name(fork_name), - genesis_validators_root, - ) - } - /// Returns the `next_fork_version`. /// /// `next_fork_version = current_fork_version` if no future fork is planned, @@ -337,29 +362,27 @@ impl ChainSpec { /// Returns the name of the fork which is active at `epoch`. pub fn fork_name_at_epoch(&self, epoch: Epoch) -> ForkName { - match self.fulu_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Fulu, - _ => match self.eip7805_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Eip7805, + let forks = [ + (self.gloas_fork_epoch, ForkName::Gloas), + (self.eip7805_fork_epoch, ForkName::Eip7805), + (self.fulu_fork_epoch, ForkName::Fulu), + (self.electra_fork_epoch, ForkName::Electra), + (self.deneb_fork_epoch, ForkName::Deneb), + (self.capella_fork_epoch, ForkName::Capella), + (self.bellatrix_fork_epoch, ForkName::Bellatrix), + (self.altair_fork_epoch, ForkName::Altair), + ]; - _ => match self.electra_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Electra, - _ => match self.deneb_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Deneb, - _ => match self.capella_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Capella, - _ => match self.bellatrix_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Bellatrix, - _ => match self.altair_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair, - _ => ForkName::Base, - }, - }, - }, - }, - }, - }, + // Find the first fork where `epoch` is >= `fork_epoch`. + for (fork_epoch_opt, fork_name) in forks.iter() { + if let Some(fork_epoch) = fork_epoch_opt + && epoch >= *fork_epoch + { + return *fork_name; + } } + + ForkName::Base } /// Returns the fork version for a named fork. @@ -373,9 +396,15 @@ impl ChainSpec { ForkName::Electra => self.electra_fork_version, ForkName::Eip7805 => self.eip7805_fork_version, ForkName::Fulu => self.fulu_fork_version, + ForkName::Gloas => self.gloas_fork_version, } } + // This is `compute_fork_version` in the spec + pub fn fork_version_for_epoch(&self, epoch: Epoch) -> [u8; 4] { + self.fork_version_for_name(self.fork_name_at_epoch(epoch)) + } + /// For a given fork name, return the epoch at which it activates. pub fn fork_epoch(&self, fork_name: ForkName) -> Option { match fork_name { @@ -387,6 +416,7 @@ impl ChainSpec { ForkName::Electra => self.electra_fork_epoch, ForkName::Eip7805 => self.eip7805_fork_epoch, ForkName::Fulu => self.fulu_fork_epoch, + ForkName::Gloas => self.gloas_fork_epoch, } } @@ -459,8 +489,13 @@ impl ChainSpec { .is_some_and(|fulu_fork_epoch| block_epoch >= fulu_fork_epoch) } - /// Returns true if `FULU_FORK_EPOCH` is set and is not set to `FAR_FUTURE_EPOCH`. + /// Returns true if PeerDAS is scheduled. Alias for [`Self::is_fulu_scheduled`] pub fn is_peer_das_scheduled(&self) -> bool { + self.is_fulu_scheduled() + } + + /// Returns true if `FULU_FORK_EPOCH` is set and is not set to `FAR_FUTURE_EPOCH`. + pub fn is_fulu_scheduled(&self) -> bool { self.fulu_fork_epoch .is_some_and(|fulu_fork_epoch| fulu_fork_epoch != self.far_future_epoch) } @@ -477,18 +512,32 @@ impl ChainSpec { .is_some_and(|eip7805_fork_epoch| eip7805_fork_epoch != self.far_future_epoch) } + /// Returns true if `GLOAS_FORK_EPOCH` is set and is not set to `FAR_FUTURE_EPOCH`. + pub fn is_gloas_scheduled(&self) -> bool { + self.gloas_fork_epoch + .is_some_and(|gloas_fork_epoch| gloas_fork_epoch != self.far_future_epoch) + } + /// Returns a full `Fork` struct for a given epoch. pub fn fork_at_epoch(&self, epoch: Epoch) -> Fork { let current_fork_name = self.fork_name_at_epoch(epoch); - let previous_fork_name = current_fork_name.previous_fork().unwrap_or(ForkName::Base); - let epoch = self + + let fork_epoch = self .fork_epoch(current_fork_name) .unwrap_or_else(|| Epoch::new(0)); + // At genesis the Fork is initialised with two copies of the same value for both + // `previous_version` and `current_version` (see `initialize_beacon_state_from_eth1`). + let previous_fork_name = if fork_epoch == 0 { + current_fork_name + } else { + current_fork_name.previous_fork().unwrap_or(ForkName::Base) + }; + Fork { previous_version: self.fork_version_for_name(previous_fork_name), current_version: self.fork_version_for_name(current_fork_name), - epoch, + epoch: fork_epoch, } } @@ -517,6 +566,8 @@ impl ChainSpec { Domain::VoluntaryExit => self.domain_voluntary_exit, Domain::SelectionProof => self.domain_selection_proof, Domain::AggregateAndProof => self.domain_aggregate_and_proof, + Domain::BeaconBuilder => self.domain_beacon_builder, + Domain::PTCAttester => self.domain_ptc_attester, Domain::SyncCommittee => self.domain_sync_committee, Domain::ContributionAndProof => self.domain_contribution_and_proof, Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof, @@ -581,18 +632,69 @@ impl ChainSpec { /// /// This is a digest primarily used for domain separation on the p2p layer. /// 4-bytes suffices for practical separation of forks/chains. - pub fn compute_fork_digest( - current_version: [u8; 4], - genesis_validators_root: Hash256, - ) -> [u8; 4] { - let mut result = [0; 4]; - let root = Self::compute_fork_data_root(current_version, genesis_validators_root); - result.copy_from_slice( + pub fn compute_fork_digest(&self, genesis_validators_root: Hash256, epoch: Epoch) -> [u8; 4] { + let fork_version = self.fork_version_for_epoch(epoch); + let mut base_digest = [0u8; 4]; + let root = Self::compute_fork_data_root(fork_version, genesis_validators_root); + base_digest.copy_from_slice( root.as_slice() .get(0..4) .expect("root hash is at least 4 bytes"), ); - result + + let Some(blob_parameters) = self.get_blob_parameters(epoch) else { + return base_digest; + }; + + match self.fulu_fork_epoch { + Some(fulu_epoch) if epoch >= fulu_epoch => { + // Concatenate epoch and max_blobs_per_block as u64 bytes + let mut input = Vec::with_capacity(16); + input.extend_from_slice(&blob_parameters.epoch.as_u64().to_le_bytes()); + input.extend_from_slice(&blob_parameters.max_blobs_per_block.to_le_bytes()); + + // Hash the concatenated bytes + let hash = hash(&input); + + // XOR the base digest with the first 4 bytes of the hash + let mut masked_digest = [0u8; 4]; + for (i, (a, b)) in base_digest.iter().zip(hash.iter()).enumerate() { + if let Some(x) = masked_digest.get_mut(i) { + *x = a ^ b; + } + } + masked_digest + } + _ => base_digest, + } + } + + pub fn all_digest_epochs(&self) -> impl std::iter::Iterator { + let mut relevant_epochs = ForkName::list_all_fork_epochs(self) + .into_iter() + .filter_map(|(_, epoch)| epoch) + .collect::>(); + + if self.is_fulu_scheduled() { + for blob_parameters in &self.blob_schedule { + relevant_epochs.insert(blob_parameters.epoch); + } + } + let mut vec = relevant_epochs.into_iter().collect::>(); + vec.sort(); + vec.into_iter() + } + + pub fn next_digest_epoch(&self, epoch: Epoch) -> Option { + match self.fulu_fork_epoch { + Some(fulu_epoch) if epoch >= fulu_epoch => self + .all_digest_epochs() + .find(|digest_epoch| *digest_epoch > epoch), + _ => self + .fork_name_at_epoch(epoch) + .next_fork() + .and_then(|fork_name| self.fork_epoch(fork_name)), + } } /// Compute a domain by applying the given `fork_version`. @@ -624,7 +726,7 @@ impl ChainSpec { } pub fn maximum_gossip_clock_disparity(&self) -> Duration { - Duration::from_millis(self.maximum_gossip_clock_disparity_millis) + Duration::from_millis(self.maximum_gossip_clock_disparity) } pub fn ttfb_timeout(&self) -> Duration { @@ -651,17 +753,6 @@ impl ChainSpec { } } - /// Returns the highest possible value for max_request_blocks based on enabled forks. - /// - /// This is useful for upper bounds in testing. - pub fn max_request_blocks_upper_bound(&self) -> usize { - if self.deneb_fork_epoch.is_some() { - self.max_request_blocks_deneb as usize - } else { - self.max_request_blocks as usize - } - } - pub fn max_request_blob_sidecars(&self, fork_name: ForkName) -> usize { if fork_name.electra_enabled() { self.max_request_blob_sidecars_electra as usize @@ -681,19 +772,58 @@ impl ChainSpec { } } - /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for the fork at `epoch`. + /// Return the value of `MAX_BLOBS_PER_BLOCK` for the given `epoch`. + /// NOTE: this function is *technically* not spec compliant, but + /// I'm told this is what the other clients are doing for `devnet-0`.. pub fn max_blobs_per_block(&self, epoch: Epoch) -> u64 { - self.max_blobs_per_block_by_fork(self.fork_name_at_epoch(epoch)) + match self.fulu_fork_epoch { + Some(fulu_epoch) if epoch >= fulu_epoch => self + .blob_schedule + .max_blobs_for_epoch(epoch) + .unwrap_or(self.max_blobs_per_block_electra), + _ => match self.electra_fork_epoch { + Some(electra_epoch) if epoch >= electra_epoch => self.max_blobs_per_block_electra, + _ => self.max_blobs_per_block, + }, + } } - /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for `fork`. - pub fn max_blobs_per_block_by_fork(&self, fork_name: ForkName) -> u64 { - if fork_name.fulu_enabled() { - self.max_blobs_per_block_fulu - } else if fork_name.electra_enabled() { - self.max_blobs_per_block_electra + /// Return the blob parameters at a given epoch. + fn get_blob_parameters(&self, epoch: Epoch) -> Option { + match self.fulu_fork_epoch { + Some(fulu_epoch) if epoch >= fulu_epoch => self + .blob_schedule + .blob_parameters_for_epoch(epoch) + .or_else(|| { + Some(BlobParameters { + epoch: self + .electra_fork_epoch + .expect("electra fork epoch must be set if fulu epoch is set"), + max_blobs_per_block: self.max_blobs_per_block_electra, + }) + }), + _ => None, + } + } + + // TODO(EIP-7892): remove this once we have fork-version changes on BPO forks + pub fn max_blobs_per_block_within_fork(&self, fork_name: ForkName) -> u64 { + if !fork_name.fulu_enabled() { + if fork_name.electra_enabled() { + self.max_blobs_per_block_electra + } else { + self.max_blobs_per_block + } } else { - self.max_blobs_per_block + // Find the max blobs per block in the fork schedule + // This logic will need to be more complex once there are forks beyond Fulu + let mut max_blobs_per_block = self.max_blobs_per_block_electra; + for entry in &self.blob_schedule { + if entry.max_blobs_per_block > max_blobs_per_block { + max_blobs_per_block = entry.max_blobs_per_block; + } + } + max_blobs_per_block } } @@ -718,31 +848,43 @@ impl ChainSpec { } /// Returns the number of data columns per custody group. - pub fn data_columns_per_group(&self) -> u64 { - self.number_of_columns + pub fn data_columns_per_group(&self) -> u64 { + (E::number_of_columns() as u64) .safe_div(self.number_of_custody_groups) .expect("Custody group count must be greater than 0") } /// Returns the number of column sidecars to sample per slot. - pub fn sampling_size(&self, custody_group_count: u64) -> Result { - let columns_per_custody_group = self - .number_of_columns - .safe_div(self.number_of_custody_groups) - .map_err(|_| "number_of_custody_groups must be greater than 0")?; + pub fn sampling_size_columns( + &self, + custody_group_count: u64, + ) -> Result { + let sampling_size_groups = self.sampling_size_custody_groups(custody_group_count)?; + let columns_per_custody_group = self.data_columns_per_group::(); - let custody_column_count = columns_per_custody_group - .safe_mul(custody_group_count) + let sampling_size_columns = columns_per_custody_group + .safe_mul(sampling_size_groups) .map_err(|_| "Computing sampling size should not overflow")?; - Ok(std::cmp::max(custody_column_count, self.samples_per_slot)) + Ok(sampling_size_columns as usize) } - pub fn custody_group_count(&self, is_supernode: bool) -> u64 { - if is_supernode { - self.number_of_custody_groups - } else { - self.custody_requirement + /// Returns the number of custody groups to sample per slot. + pub fn sampling_size_custody_groups(&self, custody_group_count: u64) -> Result { + Ok(std::cmp::max(custody_group_count, self.samples_per_slot)) + } + + /// Returns the min epoch for blob / data column sidecar requests based on the current epoch. + /// Switch to use the column sidecar config once the `blob_retention_epoch` has passed Fulu fork epoch. + pub fn min_epoch_data_availability_boundary(&self, current_epoch: Epoch) -> Option { + let fork_epoch = self.deneb_fork_epoch?; + let blob_retention_epoch = + current_epoch.saturating_sub(self.min_epochs_for_blob_sidecars_requests); + match self.fulu_fork_epoch { + Some(fulu_fork_epoch) if blob_retention_epoch > fulu_fork_epoch => Some( + current_epoch.saturating_sub(self.min_epochs_for_data_column_sidecars_requests), + ), + _ => Some(std::cmp::max(fork_epoch, blob_retention_epoch)), } } @@ -779,6 +921,34 @@ impl ChainSpec { ) } + /// Returns the slot at which the proposer shuffling was decided. + /// + /// The block root at this slot can be used to key the proposer shuffling for the given epoch. + pub fn proposer_shuffling_decision_slot(&self, epoch: Epoch) -> Slot { + // At the Fulu fork epoch itself, the shuffling is computed "the old way" with no lookahead. + // Therefore for `epoch == fulu_fork_epoch` we must take the `else` branch. Checking if Fulu + // is enabled at `epoch - 1` accomplishes this neatly. + if self + .fork_name_at_epoch(epoch.saturating_sub(1_u64)) + .fulu_enabled() + { + // Post-Fulu the proposer shuffling decision slot for epoch N is the slot at the end + // of epoch N - 2 (note: min_seed_lookahead=1 in all current configs). + epoch + .saturating_sub(self.min_seed_lookahead) + .start_slot(E::slots_per_epoch()) + .saturating_sub(1_u64) + } else { + // Pre-Fulu the proposer shuffling decision slot for epoch N is the slot at the end of + // epoch N - 1 (note: +1 -1 for min_seed_lookahead=1 in all current configs). + epoch + .saturating_add(Epoch::new(1)) + .saturating_sub(self.min_seed_lookahead) + .start_slot(E::slots_per_epoch()) + .saturating_sub(1_u64) + } + } + /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. pub fn mainnet() -> Self { Self { @@ -836,18 +1006,25 @@ impl ChainSpec { bls_withdrawal_prefix_byte: 0x00, eth1_address_withdrawal_prefix_byte: 0x01, compounding_withdrawal_prefix_byte: 0x02, + builder_withdrawal_prefix_byte: 0x03, /* * Time parameters */ genesis_delay: 604800, // 7 days seconds_per_slot: 12, + slot_duration_ms: 12000, min_attestation_inclusion_delay: 1, min_seed_lookahead: Epoch::new(1), max_seed_lookahead: Epoch::new(4), min_epochs_to_inactivity_penalty: 4, min_validator_withdrawability_delay: Epoch::new(256), shard_committee_period: 256, + proposer_reorg_cutoff_bps: 1667, + attestation_due_bps: 3333, + aggregate_due_bps: 6667, + sync_message_due_bps: 3333, + contribution_due_bps: 6667, /* * Reward and penalty quotients @@ -869,6 +1046,8 @@ impl ChainSpec { domain_voluntary_exit: 4, domain_selection_proof: 5, domain_aggregate_and_proof: 6, + domain_beacon_builder: 0x1B, + domain_ptc_attester: 0x0C, /* * Fork choice @@ -983,13 +1162,22 @@ impl ChainSpec { /* * Fulu hard fork params */ - fulu_fork_version: [0x07, 0x00, 0x00, 0x00], - fulu_fork_epoch: None, + fulu_fork_version: [0x06, 0x00, 0x00, 0x00], + fulu_fork_epoch: Some(Epoch::new(411392)), custody_requirement: 4, number_of_custody_groups: 128, data_column_sidecar_subnet_count: 128, - number_of_columns: 128, samples_per_slot: 8, + validator_custody_requirement: 8, + balance_per_additional_custody_group: 32000000000, + + /* + * Gloas hard fork params + */ + gloas_fork_version: [0x07, 0x00, 0x00, 0x00], + gloas_fork_epoch: None, + builder_payment_threshold_numerator: 6, + builder_payment_threshold_denominator: 10, /* * Network specific @@ -999,7 +1187,7 @@ impl ChainSpec { attestation_propagation_slot_range: default_attestation_propagation_slot_range(), attestation_subnet_count: 64, subnets_per_node: 2, - maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), + maximum_gossip_clock_disparity: default_maximum_gossip_clock_disparity(), target_aggregators_per_committee: 16, max_payload_size: default_max_payload_size(), min_epochs_for_block_requests: default_min_epochs_for_block_requests(), @@ -1026,7 +1214,6 @@ impl ChainSpec { max_blocks_by_root_request: default_max_blocks_by_root_request(), max_blocks_by_root_request_deneb: default_max_blocks_by_root_request_deneb(), max_blobs_by_root_request: default_max_blobs_by_root_request(), - max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Networking Electra specific @@ -1038,7 +1225,19 @@ impl ChainSpec { /* * Networking Fulu specific */ - max_blobs_per_block_fulu: default_max_blobs_per_block_fulu(), + blob_schedule: BlobSchedule::new(vec![ + BlobParameters { + epoch: Epoch::new(412672), + max_blobs_per_block: 15, + }, + BlobParameters { + epoch: Epoch::new(419072), + max_blobs_per_block: 21, + }, + ]), + min_epochs_for_data_column_sidecars_requests: + default_min_epochs_for_data_column_sidecars_requests(), + max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Application specific @@ -1115,6 +1314,9 @@ impl ChainSpec { // Fulu fulu_fork_version: [0x07, 0x00, 0x00, 0x00], fulu_fork_epoch: None, + // Gloas + gloas_fork_version: [0x07, 0x00, 0x00, 0x00], + gloas_fork_epoch: None, // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -1181,18 +1383,25 @@ impl ChainSpec { bls_withdrawal_prefix_byte: 0x00, eth1_address_withdrawal_prefix_byte: 0x01, compounding_withdrawal_prefix_byte: 0x02, + builder_withdrawal_prefix_byte: 0x03, /* * Time parameters */ genesis_delay: 6000, // 100 minutes seconds_per_slot: 5, + slot_duration_ms: 5000, min_attestation_inclusion_delay: 1, min_seed_lookahead: Epoch::new(1), max_seed_lookahead: Epoch::new(4), min_epochs_to_inactivity_penalty: 4, min_validator_withdrawability_delay: Epoch::new(256), shard_committee_period: 256, + proposer_reorg_cutoff_bps: 1667, + attestation_due_bps: 3333, + aggregate_due_bps: 6667, + sync_message_due_bps: 3333, + contribution_due_bps: 6667, /* * Reward and penalty quotients @@ -1214,6 +1423,8 @@ impl ChainSpec { domain_voluntary_exit: 4, domain_selection_proof: 5, domain_aggregate_and_proof: 6, + domain_beacon_builder: 0x1B, + domain_ptc_attester: 0x0C, /* * Fork choice @@ -1306,8 +1517,7 @@ impl ChainSpec { .expect("pow does not overflow"), whistleblower_reward_quotient_electra: u64::checked_pow(2, 12) .expect("pow does not overflow"), - max_pending_partials_per_withdrawals_sweep: u64::checked_pow(2, 3) - .expect("pow does not overflow"), + max_pending_partials_per_withdrawals_sweep: 6, min_per_epoch_churn_limit_electra: option_wrapper(|| { u64::checked_pow(2, 7)?.checked_mul(u64::checked_pow(10, 9)?) }) @@ -1333,8 +1543,17 @@ impl ChainSpec { custody_requirement: 4, number_of_custody_groups: 128, data_column_sidecar_subnet_count: 128, - number_of_columns: 128, samples_per_slot: 8, + validator_custody_requirement: 8, + balance_per_additional_custody_group: 32000000000, + + /* + * Gloas hard fork params + */ + gloas_fork_version: [0x07, 0x00, 0x00, 0x64], + gloas_fork_epoch: None, + builder_payment_threshold_numerator: 6, + builder_payment_threshold_denominator: 10, /* * Network specific @@ -1344,7 +1563,7 @@ impl ChainSpec { attestation_propagation_slot_range: default_attestation_propagation_slot_range(), attestation_subnet_count: 64, subnets_per_node: 4, // Make this larger than usual to avoid network damage - maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), + maximum_gossip_clock_disparity: default_maximum_gossip_clock_disparity(), target_aggregators_per_committee: 16, max_payload_size: default_max_payload_size(), min_epochs_for_block_requests: 33024, @@ -1371,7 +1590,6 @@ impl ChainSpec { max_blocks_by_root_request: default_max_blocks_by_root_request(), max_blocks_by_root_request_deneb: default_max_blocks_by_root_request_deneb(), max_blobs_by_root_request: default_max_blobs_by_root_request(), - max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Networking Electra specific @@ -1383,7 +1601,10 @@ impl ChainSpec { /* * Networking Fulu specific */ - max_blobs_per_block_fulu: default_max_blobs_per_block_fulu(), + blob_schedule: BlobSchedule::default(), + min_epochs_for_data_column_sidecars_requests: + default_min_epochs_for_data_column_sidecars_requests(), + max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Application specific @@ -1404,6 +1625,119 @@ impl Default for ChainSpec { } } +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub struct BlobParameters { + pub epoch: Epoch, + #[serde(with = "serde_utils::quoted_u64")] + pub max_blobs_per_block: u64, +} + +// A wrapper around a vector of BlobParameters to ensure that the vector is reverse +// sorted by epoch. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, Educe, Clone)] +#[educe(PartialEq)] +pub struct BlobSchedule { + schedule: Vec, + // This is a hack to prevent the blob schedule being serialized on the /eth/v1/config/spec + // endpoint prior to the Fulu fork being scheduled. + // + // We can remove this once Fulu is live on mainnet. + #[educe(PartialEq(ignore))] + skip_serializing: bool, +} + +impl<'de> Deserialize<'de> for BlobSchedule { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let vec = Vec::::deserialize(deserializer)?; + Ok(BlobSchedule::new(vec)) + } +} + +impl BlobSchedule { + pub fn new(mut vec: Vec) -> Self { + // reverse sort by epoch + vec.sort_by(|a, b| b.epoch.cmp(&a.epoch)); + Self { + schedule: vec, + skip_serializing: false, + } + } + + pub fn is_empty(&self) -> bool { + self.schedule.is_empty() + } + + pub fn skip_serializing(&self) -> bool { + self.skip_serializing + } + + pub fn set_skip_serializing(&mut self) { + self.skip_serializing = true; + } + + pub fn max_blobs_for_epoch(&self, epoch: Epoch) -> Option { + self.schedule + .iter() + .find(|entry| epoch >= entry.epoch) + .map(|entry| entry.max_blobs_per_block) + } + + pub fn blob_parameters_for_epoch(&self, epoch: Epoch) -> Option { + self.schedule + .iter() + .find(|entry| epoch >= entry.epoch) + .cloned() + } + + pub const fn default() -> Self { + // TODO(EIP-7892): think about what the default should be + Self { + schedule: vec![], + skip_serializing: false, + } + } + + pub fn as_vec(&self) -> &Vec { + &self.schedule + } +} + +impl Serialize for BlobSchedule { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut schedule = self.schedule.clone(); + // reversing the list to get an ascending order + schedule.reverse(); + schedule.serialize(serializer) + } +} + +impl<'a> IntoIterator for &'a BlobSchedule { + type Item = &'a BlobParameters; + type IntoIter = std::slice::Iter<'a, BlobParameters>; + + fn into_iter(self) -> Self::IntoIter { + self.schedule.iter() + } +} + +impl IntoIterator for BlobSchedule { + type Item = BlobParameters; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.schedule.into_iter() + } +} + /// Exact implementation of the *config* object from the Ethereum spec (YAML/JSON). /// /// Fields relevant to hard forks after Altair should be optional so that we can continue @@ -1490,6 +1824,14 @@ pub struct Config { #[serde(deserialize_with = "deserialize_fork_epoch")] pub eip7805_fork_epoch: Option>, + #[serde(default = "default_gloas_fork_version")] + #[serde(with = "serde_utils::bytes_4_hex")] + gloas_fork_version: [u8; 4], + #[serde(default)] + #[serde(serialize_with = "serialize_fork_epoch")] + #[serde(deserialize_with = "deserialize_fork_epoch")] + pub gloas_fork_epoch: Option>, + #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -1550,9 +1892,9 @@ pub struct Config { #[serde(default = "default_attestation_propagation_slot_range")] #[serde(with = "serde_utils::quoted_u64")] attestation_propagation_slot_range: u64, - #[serde(default = "default_maximum_gossip_clock_disparity_millis")] + #[serde(default = "default_maximum_gossip_clock_disparity")] #[serde(with = "serde_utils::quoted_u64")] - maximum_gossip_clock_disparity_millis: u64, + maximum_gossip_clock_disparity: u64, #[serde(default = "default_message_domain_invalid_snappy")] #[serde(with = "serde_utils::bytes_4_hex")] message_domain_invalid_snappy: [u8; 4], @@ -1597,9 +1939,6 @@ pub struct Config { #[serde(with = "serde_utils::quoted_u64")] max_request_blob_sidecars_electra: u64, - #[serde(default = "default_number_of_columns")] - #[serde(with = "serde_utils::quoted_u64")] - number_of_columns: u64, #[serde(default = "default_number_of_custody_groups")] #[serde(with = "serde_utils::quoted_u64")] number_of_custody_groups: u64, @@ -1612,9 +1951,18 @@ pub struct Config { #[serde(default = "default_custody_requirement")] #[serde(with = "serde_utils::quoted_u64")] custody_requirement: u64, - #[serde(default = "default_max_blobs_per_block_fulu")] + #[serde(default = "BlobSchedule::default")] + #[serde(skip_serializing_if = "BlobSchedule::skip_serializing")] + pub blob_schedule: BlobSchedule, + #[serde(default = "default_validator_custody_requirement")] #[serde(with = "serde_utils::quoted_u64")] - max_blobs_per_block_fulu: u64, + validator_custody_requirement: u64, + #[serde(default = "default_balance_per_additional_custody_group")] + #[serde(with = "serde_utils::quoted_u64")] + balance_per_additional_custody_group: u64, + #[serde(default = "default_min_epochs_for_data_column_sidecars_requests")] + #[serde(with = "serde_utils::quoted_u64")] + min_epochs_for_data_column_sidecars_requests: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1623,7 +1971,6 @@ fn default_bellatrix_fork_version() -> [u8; 4] { } fn default_capella_fork_version() -> [u8; 4] { - // TODO: determine if the bellatrix example should be copied like this [0xff, 0xff, 0xff, 0xff] } @@ -1647,6 +1994,11 @@ fn default_fulu_fork_version() -> [u8; 4] { [0xff, 0xff, 0xff, 0xff] } +fn default_gloas_fork_version() -> [u8; 4] { + // This value shouldn't be used. + [0xff, 0xff, 0xff, 0xff] +} + /// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912). /// /// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16 @@ -1757,15 +2109,11 @@ const fn default_max_blobs_per_block_electra() -> u64 { 9 } -const fn default_max_blobs_per_block_fulu() -> u64 { - 12 -} - const fn default_attestation_propagation_slot_range() -> u64 { 32 } -const fn default_maximum_gossip_clock_disparity_millis() -> u64 { +const fn default_maximum_gossip_clock_disparity() -> u64 { 500 } @@ -1777,10 +2125,6 @@ const fn default_data_column_sidecar_subnet_count() -> u64 { 128 } -const fn default_number_of_columns() -> u64 { - 128 -} - const fn default_number_of_custody_groups() -> u64 { 128 } @@ -1789,12 +2133,25 @@ const fn default_samples_per_slot() -> u64 { 8 } +const fn default_validator_custody_requirement() -> u64 { + 8 +} + +const fn default_balance_per_additional_custody_group() -> u64 { + 32000000000 +} + +const fn default_min_epochs_for_data_column_sidecars_requests() -> u64 { + 4096 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; - RuntimeVariableList::::from_vec( + RuntimeVariableList::::new( vec![Hash256::zero(); max_request_blocks], max_request_blocks, ) + .expect("creating a RuntimeVariableList of size `max_request_blocks` should succeed") .as_ssz_bytes() .len() } @@ -1806,30 +2163,28 @@ fn max_blobs_by_root_request_common(max_request_blob_sidecars: u64) -> usize { index: 0, }; - RuntimeVariableList::::from_vec( + RuntimeVariableList::::new( vec![empty_blob_identifier; max_request_blob_sidecars], max_request_blob_sidecars, ) + .expect("creating a RuntimeVariableList of size `max_request_blob_sidecars` should succeed") .as_ssz_bytes() .len() } -fn max_data_columns_by_root_request_common( - max_request_blocks: u64, - number_of_columns: u64, -) -> usize { +fn max_data_columns_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; - let number_of_columns = number_of_columns as usize; let empty_data_columns_by_root_id = DataColumnsByRootIdentifier { block_root: Hash256::zero(), - columns: RuntimeVariableList::from_vec(vec![0; number_of_columns], number_of_columns), + columns: VariableList::repeat_full(0), }; - RuntimeVariableList::::from_vec( + RuntimeVariableList::>::new( vec![empty_data_columns_by_root_id; max_request_blocks], max_request_blocks, ) + .expect("creating a RuntimeVariableList of size `max_request_blocks` should succeed") .as_ssz_bytes() .len() } @@ -1847,10 +2202,7 @@ fn default_max_blobs_by_root_request() -> usize { } fn default_data_columns_by_root_request() -> usize { - max_data_columns_by_root_request_common( - default_max_request_blocks_deneb(), - default_number_of_columns(), - ) + max_data_columns_by_root_request_common::(default_max_request_blocks_deneb()) } impl Default for Config { @@ -1881,10 +2233,10 @@ where D: Deserializer<'de>, { let decoded: Option> = serde::de::Deserialize::deserialize(deserializer)?; - if let Some(fork_epoch) = decoded { - if fork_epoch.value != Epoch::max_value() { - return Ok(Some(fork_epoch)); - } + if let Some(fork_epoch) = decoded + && fork_epoch.value != Epoch::max_value() + { + return Ok(Some(fork_epoch)); } Ok(None) } @@ -1951,6 +2303,11 @@ impl Config { .eip7805_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + gloas_fork_version: spec.gloas_fork_version, + gloas_fork_epoch: spec + .gloas_fork_epoch + .map(|epoch| MaybeQuoted { value: epoch }), + seconds_per_slot: spec.seconds_per_slot, seconds_per_eth1_block: spec.seconds_per_eth1_block, min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, @@ -1980,7 +2337,7 @@ impl Config { ttfb_timeout: spec.ttfb_timeout, resp_timeout: spec.resp_timeout, attestation_propagation_slot_range: spec.attestation_propagation_slot_range, - maximum_gossip_clock_disparity_millis: spec.maximum_gossip_clock_disparity_millis, + maximum_gossip_clock_disparity: spec.maximum_gossip_clock_disparity, message_domain_invalid_snappy: spec.message_domain_invalid_snappy, message_domain_valid_snappy: spec.message_domain_valid_snappy, max_request_blocks_deneb: spec.max_request_blocks_deneb, @@ -1997,12 +2354,15 @@ impl Config { blob_sidecar_subnet_count_electra: spec.blob_sidecar_subnet_count_electra, max_request_blob_sidecars_electra: spec.max_request_blob_sidecars_electra, - number_of_columns: spec.number_of_columns, number_of_custody_groups: spec.number_of_custody_groups, data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, samples_per_slot: spec.samples_per_slot, custody_requirement: spec.custody_requirement, - max_blobs_per_block_fulu: spec.max_blobs_per_block_fulu, + blob_schedule: spec.blob_schedule.clone(), + validator_custody_requirement: spec.validator_custody_requirement, + balance_per_additional_custody_group: spec.balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests: spec + .min_epochs_for_data_column_sidecars_requests, } } @@ -2039,6 +2399,8 @@ impl Config { eip7805_fork_epoch, fulu_fork_epoch, fulu_fork_version, + gloas_fork_version, + gloas_fork_epoch, seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -2065,7 +2427,7 @@ impl Config { message_domain_valid_snappy, max_request_blocks, attestation_propagation_slot_range, - maximum_gossip_clock_disparity_millis, + maximum_gossip_clock_disparity, max_request_blocks_deneb, max_request_blob_sidecars, max_request_data_column_sidecars, @@ -2078,12 +2440,14 @@ impl Config { max_blobs_per_block_electra, blob_sidecar_subnet_count_electra, max_request_blob_sidecars_electra, - number_of_columns, number_of_custody_groups, data_column_sidecar_subnet_count, samples_per_slot, custody_requirement, - max_blobs_per_block_fulu, + ref blob_schedule, + validator_custody_requirement, + balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -2110,6 +2474,8 @@ impl Config { eip7805_fork_epoch: eip7805_fork_epoch.map(|q| q.value), fulu_fork_epoch: fulu_fork_epoch.map(|q| q.value), fulu_fork_version, + gloas_fork_version, + gloas_fork_epoch: gloas_fork_epoch.map(|q| q.value), seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -2139,7 +2505,7 @@ impl Config { attestation_subnet_prefix_bits, max_request_blocks, attestation_propagation_slot_range, - maximum_gossip_clock_disparity_millis, + maximum_gossip_clock_disparity, max_request_blocks_deneb, max_request_blob_sidecars, max_request_data_column_sidecars, @@ -2159,17 +2525,18 @@ impl Config { max_request_blocks_deneb, ), max_blobs_by_root_request: max_blobs_by_root_request_common(max_request_blob_sidecars), - max_data_columns_by_root_request: max_data_columns_by_root_request_common( + max_data_columns_by_root_request: max_data_columns_by_root_request_common::( max_request_blocks_deneb, - number_of_columns, ), - number_of_columns, number_of_custody_groups, data_column_sidecar_subnet_count, samples_per_slot, custody_requirement, - max_blobs_per_block_fulu, + blob_schedule: blob_schedule.clone(), + validator_custody_requirement, + balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests, ..chain_spec.clone() }) @@ -2235,6 +2602,8 @@ mod tests { &spec, ); test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec); + test_domain(Domain::BeaconBuilder, spec.domain_beacon_builder, &spec); + test_domain(Domain::PTCAttester, spec.domain_ptc_attester, &spec); // The builder domain index is zero let builder_domain_pre_mask = [0; 4]; @@ -2313,7 +2682,9 @@ mod tests { #[cfg(test)] mod yaml_tests { use super::*; + use crate::core::MinimalEthSpec; use paste::paste; + use std::sync::Arc; use tempfile::NamedTempFile; #[test] @@ -2362,6 +2733,237 @@ mod yaml_tests { assert_eq!(from, yamlconfig); } + #[test] + fn blob_schedule_max_blobs_per_block() { + let spec_contents = r#" + PRESET_BASE: 'mainnet' + MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 384 + MIN_GENESIS_TIME: 1748264340 + GENESIS_FORK_VERSION: 0x10355025 + GENESIS_DELAY: 60 + SECONDS_PER_SLOT: 12 + SECONDS_PER_ETH1_BLOCK: 12 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 + SHARD_COMMITTEE_PERIOD: 256 + ETH1_FOLLOW_DISTANCE: 2048 + INACTIVITY_SCORE_BIAS: 4 + INACTIVITY_SCORE_RECOVERY_RATE: 16 + EJECTION_BALANCE: 16000000000 + MIN_PER_EPOCH_CHURN_LIMIT: 4 + CHURN_LIMIT_QUOTIENT: 65536 + MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + PROPOSER_SCORE_BOOST: 40 + REORG_HEAD_WEIGHT_THRESHOLD: 20 + REORG_PARENT_WEIGHT_THRESHOLD: 160 + REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + DEPOSIT_CHAIN_ID: 7042643276 + DEPOSIT_NETWORK_ID: 7042643276 + DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + + ALTAIR_FORK_VERSION: 0x20355025 + ALTAIR_FORK_EPOCH: 0 + BELLATRIX_FORK_VERSION: 0x30355025 + BELLATRIX_FORK_EPOCH: 0 + CAPELLA_FORK_VERSION: 0x40355025 + CAPELLA_FORK_EPOCH: 0 + DENEB_FORK_VERSION: 0x50355025 + DENEB_FORK_EPOCH: 64 + ELECTRA_FORK_VERSION: 0x60355025 + ELECTRA_FORK_EPOCH: 128 + FULU_FORK_VERSION: 0x70355025 + FULU_FORK_EPOCH: 256 + GLOAS_FORK_VERSION: 0x80355025 + GLOAS_FORK_EPOCH: 512 + BLOB_SCHEDULE: + - EPOCH: 512 + MAX_BLOBS_PER_BLOCK: 12 + - EPOCH: 768 + MAX_BLOBS_PER_BLOCK: 15 + - EPOCH: 1024 + MAX_BLOBS_PER_BLOCK: 18 + - EPOCH: 1280 + MAX_BLOBS_PER_BLOCK: 9 + - EPOCH: 1584 + MAX_BLOBS_PER_BLOCK: 20 + "#; + let config: Config = + serde_yaml::from_str(spec_contents).expect("error while deserializing"); + let spec = + ChainSpec::from_config::(&config).expect("error while creating spec"); + + // test out max_blobs_per_block(epoch) + assert_eq!( + spec.max_blobs_per_block(Epoch::new(64)), + default_max_blobs_per_block() + ); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(127)), + default_max_blobs_per_block() + ); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(128)), + default_max_blobs_per_block_electra() + ); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(255)), + default_max_blobs_per_block_electra() + ); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(256)), + default_max_blobs_per_block_electra() + ); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(511)), + default_max_blobs_per_block_electra() + ); + assert_eq!(spec.max_blobs_per_block(Epoch::new(512)), 12); + assert_eq!(spec.max_blobs_per_block(Epoch::new(767)), 12); + assert_eq!(spec.max_blobs_per_block(Epoch::new(768)), 15); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1023)), 15); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1024)), 18); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1279)), 18); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1280)), 9); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1583)), 9); + assert_eq!(spec.max_blobs_per_block(Epoch::new(1584)), 20); + assert_eq!( + spec.max_blobs_per_block(Epoch::new(18446744073709551615)), + 20 + ); + + // blob schedule is reverse sorted by epoch + assert_eq!( + config.blob_schedule.as_vec(), + &vec![ + BlobParameters { + epoch: Epoch::new(1584), + max_blobs_per_block: 20 + }, + BlobParameters { + epoch: Epoch::new(1280), + max_blobs_per_block: 9 + }, + BlobParameters { + epoch: Epoch::new(1024), + max_blobs_per_block: 18 + }, + BlobParameters { + epoch: Epoch::new(768), + max_blobs_per_block: 15 + }, + BlobParameters { + epoch: Epoch::new(512), + max_blobs_per_block: 12 + }, + ] + ); + + // test max_blobs_per_block_within_fork + assert_eq!( + spec.max_blobs_per_block_within_fork(ForkName::Deneb), + default_max_blobs_per_block() + ); + assert_eq!( + spec.max_blobs_per_block_within_fork(ForkName::Electra), + default_max_blobs_per_block_electra() + ); + assert_eq!(spec.max_blobs_per_block_within_fork(ForkName::Fulu), 20); + + // Check that serialization is in ascending order + let yaml = serde_yaml::to_string(&spec.blob_schedule).expect("should serialize"); + + // Deserialize back to Vec to check order + let deserialized: Vec = + serde_yaml::from_str(&yaml).expect("should deserialize"); + + // Should be in ascending order by epoch + assert!( + deserialized.iter().map(|bp| bp.epoch.as_u64()).is_sorted(), + "BlobSchedule should serialize in ascending order by epoch" + ); + } + + #[test] + fn blob_schedule_fork_digest() { + let spec_contents = r#" + PRESET_BASE: 'mainnet' + MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 384 + MIN_GENESIS_TIME: 1748264340 + GENESIS_FORK_VERSION: 0x10355025 + GENESIS_DELAY: 60 + SECONDS_PER_SLOT: 12 + SECONDS_PER_ETH1_BLOCK: 12 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 + SHARD_COMMITTEE_PERIOD: 256 + ETH1_FOLLOW_DISTANCE: 2048 + INACTIVITY_SCORE_BIAS: 4 + INACTIVITY_SCORE_RECOVERY_RATE: 16 + EJECTION_BALANCE: 16000000000 + MIN_PER_EPOCH_CHURN_LIMIT: 4 + CHURN_LIMIT_QUOTIENT: 65536 + MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8 + PROPOSER_SCORE_BOOST: 40 + REORG_HEAD_WEIGHT_THRESHOLD: 20 + REORG_PARENT_WEIGHT_THRESHOLD: 160 + REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 + DEPOSIT_CHAIN_ID: 7042643276 + DEPOSIT_NETWORK_ID: 7042643276 + DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + + ALTAIR_FORK_VERSION: 0x20355025 + ALTAIR_FORK_EPOCH: 0 + BELLATRIX_FORK_VERSION: 0x30355025 + BELLATRIX_FORK_EPOCH: 0 + CAPELLA_FORK_VERSION: 0x40355025 + CAPELLA_FORK_EPOCH: 0 + DENEB_FORK_VERSION: 0x50355025 + DENEB_FORK_EPOCH: 0 + ELECTRA_FORK_VERSION: 0x60000000 + ELECTRA_FORK_EPOCH: 9 + FULU_FORK_VERSION: 0x06000000 + FULU_FORK_EPOCH: 100 + BLOB_SCHEDULE: + - EPOCH: 9 + MAX_BLOBS_PER_BLOCK: 9 + - EPOCH: 100 + MAX_BLOBS_PER_BLOCK: 100 + - EPOCH: 150 + MAX_BLOBS_PER_BLOCK: 175 + - EPOCH: 200 + MAX_BLOBS_PER_BLOCK: 200 + - EPOCH: 250 + MAX_BLOBS_PER_BLOCK: 275 + - EPOCH: 300 + MAX_BLOBS_PER_BLOCK: 300 + "#; + let config: Config = + serde_yaml::from_str(spec_contents).expect("error while deserializing"); + let spec = + ChainSpec::from_config::(&config).expect("error while creating spec"); + + let genesis_validators_root = Hash256::from_slice(&[0; 32]); + + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(100)); + assert_eq!(digest, [0xdf, 0x67, 0x55, 0x7b]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(101)); + assert_eq!(digest, [0xdf, 0x67, 0x55, 0x7b]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(150)); + assert_eq!(digest, [0x8a, 0xb3, 0x8b, 0x59]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(199)); + assert_eq!(digest, [0x8a, 0xb3, 0x8b, 0x59]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(200)); + assert_eq!(digest, [0xd9, 0xb8, 0x14, 0x38]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(201)); + assert_eq!(digest, [0xd9, 0xb8, 0x14, 0x38]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(250)); + assert_eq!(digest, [0x4e, 0xf3, 0x2a, 0x62]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(299)); + assert_eq!(digest, [0x4e, 0xf3, 0x2a, 0x62]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(300)); + assert_eq!(digest, [0xca, 0x10, 0x0d, 0x64]); + let digest = spec.compute_fork_digest(genesis_validators_root, Epoch::new(301)); + assert_eq!(digest, [0xca, 0x10, 0x0d, 0x64]); + } + #[test] fn apply_to_spec() { let mut spec = ChainSpec::minimal(); @@ -2417,7 +3019,6 @@ mod yaml_tests { DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa CUSTODY_REQUIREMENT: 1 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 - NUMBER_OF_COLUMNS: 128 SAMPLES_PER_SLOT: 8 "#; @@ -2479,4 +3080,95 @@ mod yaml_tests { let _ = spec.max_message_size(); let _ = spec.max_compressed_len(); } + + #[test] + fn min_epochs_for_data_sidecar_requests_deneb() { + type E = MainnetEthSpec; + let spec = Arc::new(ForkName::Deneb.make_genesis_spec(E::default_spec())); + let blob_retention_epochs = spec.min_epochs_for_blob_sidecars_requests; + + // `min_epochs_for_data_sidecar_requests` cannot be earlier than Deneb fork epoch. + assert_eq!( + spec.deneb_fork_epoch, + spec.min_epoch_data_availability_boundary(Epoch::new(blob_retention_epochs / 2)) + ); + + let current_epoch = Epoch::new(blob_retention_epochs * 2); + let expected_min_blob_epoch = current_epoch - blob_retention_epochs; + assert_eq!( + Some(expected_min_blob_epoch), + spec.min_epoch_data_availability_boundary(current_epoch) + ); + } + + #[test] + fn min_epochs_for_data_sidecar_requests_fulu() { + type E = MainnetEthSpec; + let spec = { + let mut spec = ForkName::Deneb.make_genesis_spec(E::default_spec()); + // 4096 * 2 = 8192 + spec.fulu_fork_epoch = Some(Epoch::new(spec.min_epochs_for_blob_sidecars_requests * 2)); + // set a different value for testing purpose, 4096 / 2 = 2048 + spec.min_epochs_for_data_column_sidecars_requests = + spec.min_epochs_for_blob_sidecars_requests / 2; + Arc::new(spec) + }; + let blob_retention_epochs = spec.min_epochs_for_blob_sidecars_requests; + let data_column_retention_epochs = spec.min_epochs_for_data_column_sidecars_requests; + + // `min_epochs_for_data_sidecar_requests` at fulu fork epoch still uses `min_epochs_for_blob_sidecars_requests` + let fulu_fork_epoch = spec.fulu_fork_epoch.unwrap(); + let expected_blob_retention_epoch = fulu_fork_epoch - blob_retention_epochs; + assert_eq!( + Some(expected_blob_retention_epoch), + spec.min_epoch_data_availability_boundary(fulu_fork_epoch) + ); + + // `min_epochs_for_data_sidecar_requests` at fulu fork epoch + min_epochs_for_blob_sidecars_request + let blob_retention_epoch_after_fulu = fulu_fork_epoch + blob_retention_epochs; + let expected_blob_retention_epoch = blob_retention_epoch_after_fulu - blob_retention_epochs; + assert_eq!( + Some(expected_blob_retention_epoch), + spec.min_epoch_data_availability_boundary(blob_retention_epoch_after_fulu) + ); + + // After the final blob retention epoch, `min_epochs_for_data_sidecar_requests` should be calculated + // using `min_epochs_for_data_column_sidecars_request` + let current_epoch = blob_retention_epoch_after_fulu + 1; + let expected_data_column_retention_epoch = current_epoch - data_column_retention_epochs; + assert_eq!( + Some(expected_data_column_retention_epoch), + spec.min_epoch_data_availability_boundary(current_epoch) + ); + } + + #[test] + fn proposer_shuffling_decision_root_around_epoch_boundary() { + type E = MainnetEthSpec; + let fulu_fork_epoch = 5; + let gloas_fork_epoch = 10; + let spec = { + let mut spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + spec.fulu_fork_epoch = Some(Epoch::new(fulu_fork_epoch)); + spec.gloas_fork_epoch = Some(Epoch::new(gloas_fork_epoch)); + Arc::new(spec) + }; + + // For epochs prior to AND including the Fulu fork epoch, the decision slot is the end + // of the previous epoch (i.e. only 1 slot lookahead). + for epoch in (0..=fulu_fork_epoch).map(Epoch::new) { + assert_eq!( + spec.proposer_shuffling_decision_slot::(epoch), + epoch.start_slot(E::slots_per_epoch()) - 1 + ); + } + + // For epochs after Fulu, the decision slot is the end of the epoch two epochs prior. + for epoch in ((fulu_fork_epoch + 1)..=(gloas_fork_epoch + 1)).map(Epoch::new) { + assert_eq!( + spec.proposer_shuffling_decision_slot::(epoch), + (epoch - 1).start_slot(E::slots_per_epoch()) - 1 + ); + } + } } diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/core/config_and_preset.rs similarity index 75% rename from consensus/types/src/config_and_preset.rs rename to consensus/types/src/core/config_and_preset.rs index fd37cfd7c4..28f66d361b 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/core/config_and_preset.rs @@ -1,18 +1,19 @@ -use crate::{ - consts::altair, consts::deneb, AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, - ChainSpec, Config, DenebPreset, Eip7805Preset, ElectraPreset, EthSpec, ForkName, FuluPreset, -}; use maplit::hashmap; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use superstruct::superstruct; +use crate::core::{ + AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, ChainSpec, Config, DenebPreset, + Eip7805Preset, ElectraPreset, EthSpec, FuluPreset, GloasPreset, consts, +}; + /// Fusion of a runtime-config with the compile-time preset values. /// /// Mostly useful for the API. #[superstruct( - variants(Deneb, Electra, Eip7805, Fulu), + variants(Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes(derive(Serialize, Deserialize, Debug, PartialEq, Clone)) )] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -31,24 +32,26 @@ pub struct ConfigAndPreset { pub capella_preset: CapellaPreset, #[serde(flatten)] pub deneb_preset: DenebPreset, - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] #[serde(flatten)] pub electra_preset: ElectraPreset, - #[superstruct(only(Eip7805, Fulu))] - #[serde(flatten)] - pub eip7805_preset: Eip7805Preset, - #[superstruct(only(Fulu))] + #[superstruct(only(Fulu, Eip7805, Gloas))] #[serde(flatten)] pub fulu_preset: FuluPreset, + #[superstruct(only(Eip7805, Gloas))] + #[serde(flatten)] + pub eip7805_preset: Eip7805Preset, + #[superstruct(only(Gloas))] + #[serde(flatten)] + pub gloas_preset: GloasPreset, /// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks. #[serde(flatten)] pub extra_fields: HashMap, } impl ConfigAndPreset { - // DEPRECATED: the `fork_name` argument is never used, we should remove it. - pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { - let config = Config::from_chain_spec::(spec); + pub fn from_chain_spec(spec: &ChainSpec) -> Self { + let mut config = Config::from_chain_spec::(spec); let base_preset = BasePreset::from_chain_spec::(spec); let altair_preset = AltairPreset::from_chain_spec::(spec); let bellatrix_preset = BellatrixPreset::from_chain_spec::(spec); @@ -56,12 +59,44 @@ impl ConfigAndPreset { let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - if spec.fulu_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Fulu) - { + if spec.is_gloas_scheduled() { + let electra_preset = ElectraPreset::from_chain_spec::(spec); + let fulu_preset = FuluPreset::from_chain_spec::(spec); + let eip7805_preset = Eip7805Preset::from_chain_spec(spec); + let gloas_preset = GloasPreset::from_chain_spec::(spec); + + ConfigAndPreset::Gloas(ConfigAndPresetGloas { + config, + base_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + electra_preset, + fulu_preset, + eip7805_preset, + gloas_preset, + extra_fields, + }) + } else if spec.is_focil_scheduled() { + let electra_preset = ElectraPreset::from_chain_spec::(spec); + let fulu_preset = FuluPreset::from_chain_spec::(spec); + let eip7805_preset = Eip7805Preset::from_chain_spec(spec); + + ConfigAndPreset::Eip7805(ConfigAndPresetEip7805 { + config, + base_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + electra_preset, + fulu_preset, + eip7805_preset, + extra_fields, + }) + } else if spec.is_fulu_scheduled() { let electra_preset = ElectraPreset::from_chain_spec::(spec); - let eip7805_preset = Eip7805Preset::from_chain_spec::(spec); let fulu_preset = FuluPreset::from_chain_spec::(spec); ConfigAndPreset::Fulu(ConfigAndPresetFulu { @@ -72,32 +107,13 @@ impl ConfigAndPreset { capella_preset, deneb_preset, electra_preset, - eip7805_preset, fulu_preset, extra_fields, }) - } else if spec.electra_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Eip7805) - { - let electra_preset = ElectraPreset::from_chain_spec::(spec); - let eip7805_preset = Eip7805Preset::from_chain_spec::(spec); + } else { + // Remove blob schedule for backwards-compatibility. + config.blob_schedule.set_skip_serializing(); - ConfigAndPreset::Eip7805(ConfigAndPresetEip7805 { - config, - base_preset, - altair_preset, - bellatrix_preset, - capella_preset, - deneb_preset, - electra_preset, - eip7805_preset, - extra_fields, - }) - } else if spec.electra_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Electra) - { let electra_preset = ElectraPreset::from_chain_spec::(spec); ConfigAndPreset::Electra(ConfigAndPresetElectra { @@ -110,16 +126,6 @@ impl ConfigAndPreset { electra_preset, extra_fields, }) - } else { - ConfigAndPreset::Deneb(ConfigAndPresetDeneb { - config, - base_preset, - altair_preset, - bellatrix_preset, - capella_preset, - deneb_preset, - extra_fields, - }) } } } @@ -148,11 +154,11 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap { "domain_sync_committee_selection_proof".to_uppercase() => u32_hex(spec.domain_sync_committee_selection_proof), "sync_committee_subnet_count".to_uppercase() => - altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(), + consts::altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(), "target_aggregators_per_sync_subcommittee".to_uppercase() => - altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE.to_string().into(), + consts::altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE.to_string().into(), // Deneb - "versioned_hash_version_kzg".to_uppercase() => deneb::VERSIONED_HASH_VERSION_KZG.to_string().into(), + "versioned_hash_version_kzg".to_uppercase() => consts::deneb::VERSIONED_HASH_VERSION_KZG.to_string().into(), // Electra "compounding_withdrawal_prefix".to_uppercase() => u8_hex(spec.compounding_withdrawal_prefix_byte), "unset_deposit_requests_start_index".to_uppercase() => spec.unset_deposit_requests_start_index.to_string().into(), @@ -163,7 +169,7 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap { #[cfg(test)] mod test { use super::*; - use crate::MainnetEthSpec; + use crate::{Epoch, MainnetEthSpec}; use std::fs::File; use tempfile::NamedTempFile; @@ -175,9 +181,10 @@ mod test { .write(true) .open(tmp_file.as_ref()) .expect("error opening file"); - let mainnet_spec = ChainSpec::mainnet(); - let mut yamlconfig = - ConfigAndPreset::from_chain_spec::(&mainnet_spec, None); + let mut mainnet_spec = ChainSpec::mainnet(); + // setting gloas_fork_epoch because we are roundtripping a gloas config + mainnet_spec.gloas_fork_epoch = Some(Epoch::new(42)); + let mut yamlconfig = ConfigAndPreset::from_chain_spec::(&mainnet_spec); let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789"); let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321"); let (k3, v3) = ("SAMPLE_HARDFORK_KEY3", 32); @@ -194,8 +201,8 @@ mod test { .write(false) .open(tmp_file.as_ref()) .expect("error while opening the file"); - let from: ConfigAndPresetFulu = + let from: ConfigAndPresetGloas = serde_yaml::from_reader(reader).expect("error while deserializing"); - assert_eq!(ConfigAndPreset::Fulu(from), yamlconfig); + assert_eq!(ConfigAndPreset::Gloas(from), yamlconfig); } } diff --git a/consensus/types/src/consts.rs b/consensus/types/src/core/consts.rs similarity index 94% rename from consensus/types/src/consts.rs rename to consensus/types/src/core/consts.rs index c20d5fe8f3..b6d63c47a8 100644 --- a/consensus/types/src/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -23,5 +23,5 @@ pub mod bellatrix { pub const INTERVALS_PER_SLOT: u64 = 3; } pub mod deneb { - pub use crate::VERSIONED_HASH_VERSION_KZG; + pub use kzg::VERSIONED_HASH_VERSION_KZG; } diff --git a/consensus/types/src/enr_fork_id.rs b/consensus/types/src/core/enr_fork_id.rs similarity index 50% rename from consensus/types/src/enr_fork_id.rs rename to consensus/types/src/core/enr_fork_id.rs index 3ae7c39cfe..c3b400cd13 100644 --- a/consensus/types/src/enr_fork_id.rs +++ b/consensus/types/src/core/enr_fork_id.rs @@ -1,33 +1,27 @@ -use crate::test_utils::TestRandom; -use crate::Epoch; - use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Epoch, test_utils::TestRandom}; + /// Specifies a fork which allows nodes to identify each other on the network. This fork is used in /// a nodes local ENR. /// /// Spec v0.11 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Default, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] pub struct EnrForkId { + /// Fork digest of the current fork computed from [`ChainSpec::compute_fork_digest`]. #[serde(with = "serde_utils::bytes_4_hex")] pub fork_digest: [u8; 4], + /// `next_fork_version` is the fork version corresponding to the next planned fork at a future + /// epoch. The fork version will only change for regular forks, not BPO forks. #[serde(with = "serde_utils::bytes_4_hex")] pub next_fork_version: [u8; 4], + /// `next_fork_epoch` is the epoch at which the next fork (whether a regular fork or a BPO fork) is planned pub next_fork_epoch: Epoch, } diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/core/eth_spec.rs similarity index 84% rename from consensus/types/src/eth_spec.rs rename to consensus/types/src/core/eth_spec.rs index 7cd7aeb521..408c1bf6de 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/core/eth_spec.rs @@ -1,16 +1,22 @@ -use crate::*; +use std::{ + fmt::{self, Debug}, + str::FromStr, +}; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; -use ssz_types::typenum::{ - bit::B0, UInt, U0, U1, U10, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, - U134217728, U16, U16777216, U17, U2, U2048, U256, U262144, U32, U33554432, U4, U4096, U512, - U625, U64, U65536, U8, U8192, +use typenum::{ + U0, U1, U2, U4, U8, U16, U17, U32, U64, U128, U256, U512, U625, U1024, U2048, U4096, U8192, + U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, U1073741824, + U1099511627776, UInt, Unsigned, bit::B0, }; -use std::fmt::{self, Debug}; -use std::str::FromStr; -pub type U5000 = UInt, B0>, B0>; // 625 * 8 = 5000 +use crate::{ + core::{ChainSpec, Epoch}, + state::BeaconStateError, +}; + +type U5000 = UInt, B0>, B0>; // 625 * 8 = 5000 const MAINNET: &str = "mainnet"; const MINIMAL: &str = "minimal"; @@ -49,9 +55,7 @@ impl fmt::Display for EthSpecId { } } -pub trait EthSpec: - 'static + Default + Sync + Send + Clone + Debug + PartialEq + Eq + for<'a> arbitrary::Arbitrary<'a> -{ +pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq + Eq { /* * Constants */ @@ -113,11 +117,14 @@ pub trait EthSpec: type BytesPerFieldElement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* - * New in PeerDAS + * New in Fulu */ type FieldElementsPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; type FieldElementsPerExtBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type CellsPerExtBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type NumberOfColumns: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type ProposerLookaheadSlots: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) */ @@ -165,7 +172,15 @@ pub trait EthSpec: type MaxPendingDepositsPerEpoch: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* - * FOCIL + * New in Gloas + */ + type PTCSize: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxPayloadAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type BuilderPendingPaymentsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type BuilderPendingWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; + + /* + * New in Eip7805 */ type InclusionListCommitteeSize: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxTransactionsPerInclusionList: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -187,7 +202,7 @@ pub trait EthSpec: fn get_committee_count_per_slot( active_validator_count: usize, spec: &ChainSpec, - ) -> Result { + ) -> Result { Self::get_committee_count_per_slot_with( active_validator_count, spec.max_committees_per_slot, @@ -199,7 +214,7 @@ pub trait EthSpec: active_validator_count: usize, max_committees_per_slot: usize, target_committee_size: usize, - ) -> Result { + ) -> Result { let slots_per_epoch = Self::SlotsPerEpoch::to_usize(); Ok(std::cmp::max( @@ -315,6 +330,11 @@ pub trait EthSpec: Self::BytesPerBlob::to_usize() } + /// Returns the `BYTES_PER_CELL` constant for this specification. + fn bytes_per_cell() -> usize { + Self::BytesPerCell::to_usize() + } + /// Returns the `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` preset for this specification. fn kzg_proof_inclusion_proof_depth() -> usize { Self::KzgCommitmentInclusionProofDepth::to_usize() @@ -351,6 +371,16 @@ pub trait EthSpec: Self::PendingConsolidationsLimit::to_usize() } + /// Returns the `BUILDER_PENDING_PAYMENTS_LIMIT` constant for this specification. + fn builder_pending_payments_limit() -> usize { + Self::BuilderPendingPaymentsLimit::to_usize() + } + + /// Returns the `BUILDER_PENDING_WITHDRAWALS_LIMIT` constant for this specification. + fn builder_pending_withdrawals_limit() -> usize { + Self::BuilderPendingWithdrawalsLimit::to_usize() + } + /// Returns the `MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD` constant for this specification. fn max_consolidation_requests_per_payload() -> usize { Self::MaxConsolidationRequestsPerPayload::to_usize() @@ -394,6 +424,28 @@ pub trait EthSpec: fn max_transactions_per_inclusion_list() -> usize { Self::MaxTransactionsPerInclusionList::to_usize() } + + fn cells_per_ext_blob() -> usize { + Self::CellsPerExtBlob::to_usize() + } + + fn number_of_columns() -> usize { + Self::NumberOfColumns::to_usize() + } + + fn proposer_lookahead_slots() -> usize { + Self::ProposerLookaheadSlots::to_usize() + } + + /// Returns the `PTCSize` constant for this specification. + fn ptc_size() -> usize { + Self::PTCSize::to_usize() + } + + /// Returns the `MaxPayloadAttestations` constant for this specification. + fn max_payload_attestations() -> usize { + Self::MaxPayloadAttestations::to_usize() + } } /// Macro to inherit some type values from another EthSpec. @@ -405,7 +457,8 @@ macro_rules! params_from_eth_spec { } /// Ethereum Foundation specifications. -#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] pub struct MainnetEthSpec; impl EthSpec for MainnetEthSpec { @@ -422,6 +475,8 @@ impl EthSpec for MainnetEthSpec { type EpochsPerSlashingsVector = U8192; type HistoricalRootsLimit = U16777216; type ValidatorRegistryLimit = U1099511627776; + type BuilderPendingPaymentsLimit = U64; // 2 * SLOTS_PER_EPOCH = 2 * 32 = 64 + type BuilderPendingWithdrawalsLimit = U1048576; type MaxProposerSlashings = U16; type MaxAttesterSlashings = U2; type MaxAttestations = U128; @@ -445,6 +500,9 @@ impl EthSpec for MainnetEthSpec { type MaxCellsPerBlock = U33554432; type KzgCommitmentInclusionProofDepth = U17; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments + type CellsPerExtBlob = U128; + type NumberOfColumns = U128; + type ProposerLookaheadSlots = U64; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch @@ -460,6 +518,8 @@ impl EthSpec for MainnetEthSpec { type MaxWithdrawalRequestsPerPayload = U16; type MaxTransactionsPerInclusionList = U16; type MaxPendingDepositsPerEpoch = U16; + type PTCSize = U512; + type MaxPayloadAttestations = U4; type InclusionListCommitteeSize = U16; fn default_spec() -> ChainSpec { @@ -472,7 +532,8 @@ impl EthSpec for MainnetEthSpec { } /// Ethereum Foundation minimal spec, as defined in the eth2.0-specs repo. -#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] pub struct MinimalEthSpec; impl EthSpec for MinimalEthSpec { @@ -490,17 +551,19 @@ impl EthSpec for MinimalEthSpec { type MaxWithdrawalsPerPayload = U4; type FieldElementsPerBlob = U4096; type BytesPerBlob = U131072; - type MaxBlobCommitmentsPerBlock = U32; - type KzgCommitmentInclusionProofDepth = U10; + type MaxBlobCommitmentsPerBlock = U4096; + type KzgCommitmentInclusionProofDepth = U17; type PendingPartialWithdrawalsLimit = U64; type PendingConsolidationsLimit = U64; - type MaxDepositRequestsPerPayload = U4; - type MaxWithdrawalRequestsPerPayload = U2; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; + type CellsPerExtBlob = U128; + type NumberOfColumns = U128; + type ProposerLookaheadSlots = U16; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH + type BuilderPendingPaymentsLimit = U16; // 2 * SLOTS_PER_EPOCH = 2 * 8 = 16 params_from_eth_spec!(MainnetEthSpec { JustificationBitsLength, @@ -510,6 +573,7 @@ impl EthSpec for MinimalEthSpec { GenesisEpoch, HistoricalRootsLimit, ValidatorRegistryLimit, + BuilderPendingWithdrawalsLimit, MaxProposerSlashings, MaxAttesterSlashings, MaxAttestations, @@ -528,6 +592,10 @@ impl EthSpec for MinimalEthSpec { MaxConsolidationRequestsPerPayload, MaxAttesterSlashingsElectra, MaxAttestationsElectra, + MaxDepositRequestsPerPayload, + MaxWithdrawalRequestsPerPayload, + PTCSize, + MaxPayloadAttestations, InclusionListCommitteeSize, MaxTransactionsPerInclusionList }); @@ -542,7 +610,8 @@ impl EthSpec for MinimalEthSpec { } /// Gnosis Beacon Chain specifications. -#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] pub struct GnosisEthSpec; impl EthSpec for GnosisEthSpec { @@ -559,6 +628,8 @@ impl EthSpec for GnosisEthSpec { type EpochsPerSlashingsVector = U8192; type HistoricalRootsLimit = U16777216; type ValidatorRegistryLimit = U1099511627776; + type BuilderPendingPaymentsLimit = U32; // 2 * SLOTS_PER_EPOCH = 2 * 16 = 32 + type BuilderPendingWithdrawalsLimit = U1048576; type MaxProposerSlashings = U16; type MaxAttesterSlashings = U2; type MaxAttestations = U128; @@ -596,6 +667,11 @@ impl EthSpec for GnosisEthSpec { type MaxCellsPerBlock = U33554432; type BytesPerCell = U2048; type KzgCommitmentsInclusionProofDepth = U4; + type CellsPerExtBlob = U128; + type NumberOfColumns = U128; + type ProposerLookaheadSlots = U32; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH + type PTCSize = U512; + type MaxPayloadAttestations = U2; type InclusionListCommitteeSize = U16; type MaxTransactionsPerInclusionList = U16; @@ -611,12 +687,17 @@ impl EthSpec for GnosisEthSpec { #[cfg(test)] mod test { use crate::{EthSpec, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; - use ssz_types::typenum::Unsigned; + use typenum::Unsigned; fn assert_valid_spec() { + let spec = E::default_spec(); E::kzg_commitments_tree_depth(); E::block_body_tree_depth(); assert!(E::MaxValidatorsPerSlot::to_i32() >= E::MaxValidatorsPerCommittee::to_i32()); + assert_eq!( + E::proposer_lookahead_slots(), + (spec.min_seed_lookahead.as_usize() + 1) * E::slots_per_epoch() as usize + ); } #[test] diff --git a/consensus/types/src/graffiti.rs b/consensus/types/src/core/graffiti.rs similarity index 95% rename from consensus/types/src/graffiti.rs rename to consensus/types/src/core/graffiti.rs index f781aacabd..d0e0e1b1a8 100644 --- a/consensus/types/src/graffiti.rs +++ b/consensus/types/src/core/graffiti.rs @@ -1,20 +1,19 @@ -use crate::{ - test_utils::{RngCore, TestRandom}, - Hash256, -}; +use std::{fmt, str::FromStr}; + +use rand::RngCore; use regex::bytes::Regex; -use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; use ssz::{Decode, DecodeError, Encode}; -use std::fmt; -use std::str::FromStr; use tree_hash::{PackedEncoding, TreeHash}; +use crate::{core::Hash256, test_utils::TestRandom}; + pub const GRAFFITI_BYTES_LEN: usize = 32; /// The 32-byte `graffiti` field on a beacon block. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Default, Debug, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)] #[serde(transparent)] -#[derive(arbitrary::Arbitrary)] pub struct Graffiti(#[serde(with = "serde_graffiti")] pub [u8; GRAFFITI_BYTES_LEN]); impl Graffiti { diff --git a/consensus/types/src/core/mod.rs b/consensus/types/src/core/mod.rs new file mode 100644 index 0000000000..b1bf2cc7a3 --- /dev/null +++ b/consensus/types/src/core/mod.rs @@ -0,0 +1,44 @@ +pub mod consts; + +mod application_domain; +mod chain_spec; +mod config_and_preset; +mod enr_fork_id; +mod eth_spec; +mod graffiti; +mod non_zero_usize; +mod preset; +mod relative_epoch; +mod signing_data; +mod slot_data; +#[macro_use] +mod slot_epoch_macros; +mod slot_epoch; +#[cfg(feature = "sqlite")] +mod sqlite; + +pub use application_domain::{APPLICATION_DOMAIN_BUILDER, ApplicationDomain}; +pub use chain_spec::{BlobParameters, BlobSchedule, ChainSpec, Config, Domain}; +pub use config_and_preset::{ + ConfigAndPreset, ConfigAndPresetDeneb, ConfigAndPresetElectra, ConfigAndPresetFulu, + ConfigAndPresetGloas, get_extra_fields, +}; +pub use enr_fork_id::EnrForkId; +pub use eth_spec::{EthSpec, EthSpecId, GNOSIS, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; +pub use graffiti::{GRAFFITI_BYTES_LEN, Graffiti, GraffitiString}; +pub use non_zero_usize::new_non_zero_usize; +pub use preset::{ + AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, Eip7805Preset, + ElectraPreset, FuluPreset, GloasPreset, +}; +pub use relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; +pub use signing_data::{SignedRoot, SigningData}; +pub use slot_data::SlotData; +pub use slot_epoch::{Epoch, Slot}; + +pub type Hash256 = alloy_primitives::B256; +pub type Uint256 = alloy_primitives::U256; +pub type Hash64 = alloy_primitives::B64; +pub type Address = alloy_primitives::Address; +pub type VersionedHash = Hash256; +pub type MerkleProof = Vec; diff --git a/consensus/types/src/non_zero_usize.rs b/consensus/types/src/core/non_zero_usize.rs similarity index 100% rename from consensus/types/src/non_zero_usize.rs rename to consensus/types/src/core/non_zero_usize.rs diff --git a/consensus/types/src/preset.rs b/consensus/types/src/core/preset.rs similarity index 93% rename from consensus/types/src/preset.rs rename to consensus/types/src/core/preset.rs index 1013b68579..92ddb9e547 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/core/preset.rs @@ -1,5 +1,7 @@ -use crate::{ChainSpec, Epoch, EthSpec, Unsigned}; use serde::{Deserialize, Serialize}; +use typenum::Unsigned; + +use crate::core::{ChainSpec, Epoch, EthSpec}; /// Value-level representation of an Ethereum consensus "preset". /// @@ -208,6 +210,8 @@ pub struct DenebPreset { #[serde(with = "serde_utils::quoted_u64")] pub max_blob_commitments_per_block: u64, #[serde(with = "serde_utils::quoted_u64")] + pub kzg_commitment_inclusion_proof_depth: u64, + #[serde(with = "serde_utils::quoted_u64")] pub field_elements_per_blob: u64, } @@ -215,6 +219,7 @@ impl DenebPreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { max_blob_commitments_per_block: E::max_blob_commitments_per_block() as u64, + kzg_commitment_inclusion_proof_depth: E::KzgCommitmentInclusionProofDepth::to_u64(), field_elements_per_blob: E::field_elements_per_blob() as u64, } } @@ -298,7 +303,7 @@ pub struct Eip7805Preset { } impl Eip7805Preset { - pub fn from_chain_spec(spec: &ChainSpec) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { Self { domain_inclusion_list_committee: spec.domain_inclusion_list_committee, inclusion_list_committee_size: spec.inclusion_list_committee_size, @@ -315,6 +320,10 @@ pub struct FuluPreset { pub field_elements_per_ext_blob: u64, #[serde(with = "serde_utils::quoted_u64")] pub kzg_commitments_inclusion_proof_depth: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub cells_per_ext_blob: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub number_of_columns: u64, } impl FuluPreset { @@ -324,10 +333,22 @@ impl FuluPreset { field_elements_per_ext_blob: E::field_elements_per_ext_blob() as u64, kzg_commitments_inclusion_proof_depth: E::kzg_commitments_inclusion_proof_depth() as u64, + cells_per_ext_blob: E::cells_per_ext_blob() as u64, + number_of_columns: E::number_of_columns() as u64, } } } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct GloasPreset {} + +impl GloasPreset { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + Self {} + } +} + #[cfg(test)] mod test { use super::*; @@ -374,10 +395,13 @@ mod test { assert_eq!(electra, ElectraPreset::from_chain_spec::(&spec)); let eip7805: Eip7805Preset = preset_from_file(&preset_name, "eip7805.yaml"); - assert_eq!(eip7805, Eip7805Preset::from_chain_spec::(&spec)); + assert_eq!(eip7805, Eip7805Preset::from_chain_spec(&spec)); let fulu: FuluPreset = preset_from_file(&preset_name, "fulu.yaml"); assert_eq!(fulu, FuluPreset::from_chain_spec::(&spec)); + + let gloas: GloasPreset = preset_from_file(&preset_name, "gloas.yaml"); + assert_eq!(gloas, GloasPreset::from_chain_spec::(&spec)); } #[test] diff --git a/consensus/types/src/relative_epoch.rs b/consensus/types/src/core/relative_epoch.rs similarity index 96% rename from consensus/types/src/relative_epoch.rs rename to consensus/types/src/core/relative_epoch.rs index 77a46b56e8..d1ee7ecc7c 100644 --- a/consensus/types/src/relative_epoch.rs +++ b/consensus/types/src/core/relative_epoch.rs @@ -1,6 +1,7 @@ -use crate::*; use safe_arith::{ArithError, SafeArith}; +use crate::core::{Epoch, Slot}; + #[derive(Debug, PartialEq, Clone, Copy)] pub enum Error { EpochTooLow { base: Epoch, other: Epoch }, @@ -18,7 +19,8 @@ impl From for Error { /// to and following some epoch. /// /// Spec v0.12.1 -#[derive(Debug, PartialEq, Clone, Copy, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum RelativeEpoch { /// The prior epoch. Previous, diff --git a/consensus/types/src/signing_data.rs b/consensus/types/src/core/signing_data.rs similarity index 64% rename from consensus/types/src/signing_data.rs rename to consensus/types/src/core/signing_data.rs index aa25ecffd9..907f03fac7 100644 --- a/consensus/types/src/signing_data.rs +++ b/consensus/types/src/core/signing_data.rs @@ -1,25 +1,14 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ForkName, Hash256}; - +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -#[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, -)] +use crate::{core::Hash256, fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[context_deserialize(ForkName)] pub struct SigningData { pub object_root: Hash256, diff --git a/consensus/types/src/slot_data.rs b/consensus/types/src/core/slot_data.rs similarity index 92% rename from consensus/types/src/slot_data.rs rename to consensus/types/src/core/slot_data.rs index 19775913b9..f0bd01814f 100644 --- a/consensus/types/src/slot_data.rs +++ b/consensus/types/src/core/slot_data.rs @@ -1,4 +1,4 @@ -use crate::Slot; +use crate::core::Slot; /// A trait providing a `Slot` getter for messages that are related to a single slot. Useful in /// making parts of attestation and sync committee processing generic. diff --git a/consensus/types/src/slot_epoch.rs b/consensus/types/src/core/slot_epoch.rs similarity index 89% rename from consensus/types/src/slot_epoch.rs rename to consensus/types/src/core/slot_epoch.rs index 0391756047..97457701b1 100644 --- a/consensus/types/src/slot_epoch.rs +++ b/consensus/types/src/core/slot_epoch.rs @@ -10,51 +10,38 @@ //! implement `Into`, however this would allow operations between `Slots` and `Epochs` which //! may lead to programming errors which are not detected by the compiler. -use crate::test_utils::TestRandom; -use crate::{ChainSpec, SignedRoot}; +use std::{fmt, hash::Hash}; use rand::RngCore; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; -use std::fmt; -use std::hash::Hash; + +use crate::{ + core::{ChainSpec, SignedRoot}, + test_utils::TestRandom, +}; #[cfg(feature = "legacy-arith")] use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, Sub, SubAssign}; -#[derive( - arbitrary::Arbitrary, - Clone, - Copy, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, -)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Slot(#[serde(with = "serde_utils::quoted_u64")] u64); -#[derive( - arbitrary::Arbitrary, - Clone, - Copy, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, -)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Epoch(#[serde(with = "serde_utils::quoted_u64")] u64); +impl Epoch { + /// Returns an iterator `(end..=start)` + pub fn range_inclusive_rev(start: Self, end: Self) -> impl Iterator { + (end.0..=start.0).rev().map(Epoch) + } +} + impl_common!(Slot); impl_common!(Epoch); @@ -118,7 +105,7 @@ impl Epoch { .as_u64()) } - pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter { + pub fn slot_iter(&self, slots_per_epoch: u64) -> SlotIter<'_> { SlotIter { current_iteration: 0, epoch: self, diff --git a/consensus/types/src/slot_epoch_macros.rs b/consensus/types/src/core/slot_epoch_macros.rs similarity index 100% rename from consensus/types/src/slot_epoch_macros.rs rename to consensus/types/src/core/slot_epoch_macros.rs diff --git a/consensus/types/src/sqlite.rs b/consensus/types/src/core/sqlite.rs similarity index 89% rename from consensus/types/src/sqlite.rs rename to consensus/types/src/core/sqlite.rs index aa20666ae1..de892b4e98 100644 --- a/consensus/types/src/sqlite.rs +++ b/consensus/types/src/core/sqlite.rs @@ -1,14 +1,15 @@ //! Implementations of SQLite compatibility traits. -use crate::{Epoch, Slot}; use rusqlite::{ - types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef}, Error, + types::{FromSql, FromSqlError, ToSql, ToSqlOutput, ValueRef}, }; +use crate::core::{Epoch, Slot}; + macro_rules! impl_to_from_sql { ($type:ty) => { impl ToSql for $type { - fn to_sql(&self) -> Result { + fn to_sql(&self) -> Result, Error> { let val_i64 = i64::try_from(self.as_u64()) .map_err(|e| Error::ToSqlConversionFailure(Box::new(e)))?; Ok(ToSqlOutput::from(val_i64)) diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/data/blob_sidecar.rs similarity index 89% rename from consensus/types/src/blob_sidecar.rs rename to consensus/types/src/data/blob_sidecar.rs index f7a5725c5a..709e556933 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/data/blob_sidecar.rs @@ -1,27 +1,33 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ - beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, AbstractExecPayload, BeaconBlockHeader, - BeaconStateError, Blob, ChainSpec, Epoch, EthSpec, FixedVector, ForkName, Hash256, KzgProofs, - RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, - VariableList, -}; +use std::{fmt::Debug, hash::Hash, sync::Arc}; + use bls::Signature; -use derivative::Derivative; -use kzg::{Blob as KzgBlob, Kzg, KzgCommitment, KzgProof, BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT}; -use merkle_proof::{merkle_root_from_branch, verify_merkle_proof, MerkleTreeError}; +use context_deserialize::context_deserialize; +use educe::Educe; +use kzg::{BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT, Blob as KzgBlob, Kzg, KzgCommitment, KzgProof}; +use merkle_proof::{MerkleTreeError, merkle_root_from_branch, verify_merkle_proof}; use rand::Rng; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; +use ssz_types::{FixedVector, RuntimeFixedVector, RuntimeVariableList, VariableList}; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + block::{ + BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlock, SignedBeaconBlockHeader, + }, + core::{ChainSpec, Epoch, EthSpec, Hash256, Slot}, + data::Blob, + execution::AbstractExecPayload, + fork::ForkName, + kzg_ext::KzgProofs, + state::BeaconStateError, + test_utils::TestRandom, +}; + /// Container of the data that identifies an individual blob. #[derive( Serialize, Deserialize, Encode, Decode, TreeHash, Copy, Clone, Debug, PartialEq, Eq, Hash, @@ -44,22 +50,15 @@ impl Ord for BlobIdentifier { } } -#[derive( - Debug, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - Derivative, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, Educe)] #[context_deserialize(ForkName)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] pub struct BlobSidecar { #[serde(with = "serde_utils::quoted_u64")] pub index: u64, @@ -89,6 +88,7 @@ pub enum BlobSidecarError { MissingKzgCommitment, BeaconState(BeaconStateError), MerkleTree(MerkleTreeError), + SszTypes(ssz_types::Error), ArithError(ArithError), } @@ -288,10 +288,11 @@ impl BlobSidecar { let blob_sidecar = BlobSidecar::new(i, blob, block, *kzg_proof)?; blob_sidecars.push(Arc::new(blob_sidecar)); } - Ok(RuntimeVariableList::from_vec( + RuntimeVariableList::new( blob_sidecars, spec.max_blobs_per_block(block.epoch()) as usize, - )) + ) + .map_err(BlobSidecarError::SszTypes) } } diff --git a/consensus/types/src/data_column_custody_group.rs b/consensus/types/src/data/data_column_custody_group.rs similarity index 61% rename from consensus/types/src/data_column_custody_group.rs rename to consensus/types/src/data/data_column_custody_group.rs index 9e9505da9f..d96d13cfff 100644 --- a/consensus/types/src/data_column_custody_group.rs +++ b/consensus/types/src/data/data_column_custody_group.rs @@ -1,9 +1,14 @@ -use crate::{ChainSpec, ColumnIndex, DataColumnSubnetId}; +use std::collections::HashSet; + use alloy_primitives::U256; use itertools::Itertools; -use maplit::hashset; use safe_arith::{ArithError, SafeArith}; -use std::collections::HashSet; + +use crate::{ + EthSpec, + core::ChainSpec, + data::{ColumnIndex, DataColumnSubnetId}, +}; pub type CustodyIndex = u64; @@ -25,13 +30,36 @@ pub fn get_custody_groups( custody_group_count: u64, spec: &ChainSpec, ) -> Result, DataColumnCustodyGroupError> { + if custody_group_count == spec.number_of_custody_groups { + Ok(HashSet::from_iter(0..spec.number_of_custody_groups)) + } else { + get_custody_groups_ordered(raw_node_id, custody_group_count, spec) + .map(|custody_groups| custody_groups.into_iter().collect()) + } +} + +/// Returns a deterministically ordered list of custody groups assigned to a node, +/// preserving the order in which they were computed during iteration. +/// +/// # Arguments +/// * `raw_node_id` - 32-byte node identifier +/// * `custody_group_count` - Number of custody groups to generate +/// * `spec` - Chain specification containing custody group parameters +/// +/// # Returns +/// Vector of custody group indices in computation order or error if parameters are invalid +fn get_custody_groups_ordered( + raw_node_id: [u8; 32], + custody_group_count: u64, + spec: &ChainSpec, +) -> Result, DataColumnCustodyGroupError> { if custody_group_count > spec.number_of_custody_groups { return Err(DataColumnCustodyGroupError::InvalidCustodyGroupCount( custody_group_count, )); } - let mut custody_groups: HashSet = hashset![]; + let mut custody_groups = vec![]; let mut current_id = U256::from_be_slice(&raw_node_id); while custody_groups.len() < custody_group_count as usize { let mut node_id_bytes = [0u8; 32]; @@ -44,7 +72,9 @@ pub fn get_custody_groups( let custody_group = hash_prefix_u64 .safe_rem(spec.number_of_custody_groups) .expect("spec.number_of_custody_groups must not be zero"); - custody_groups.insert(custody_group); + if !custody_groups.contains(&custody_group) { + custody_groups.push(custody_group); + } current_id = current_id.wrapping_add(U256::from(1u64)); } @@ -52,10 +82,31 @@ pub fn get_custody_groups( Ok(custody_groups) } +/// Returns a deterministically ordered list of custody columns assigned to a node, +/// preserving the order in which they were computed during iteration. +/// +/// # Arguments +/// * `raw_node_id` - 32-byte node identifier +/// * `spec` - Chain specification containing custody parameters +pub fn compute_ordered_custody_column_indices( + raw_node_id: [u8; 32], + spec: &ChainSpec, +) -> Result, DataColumnCustodyGroupError> { + let all_custody_groups_ordered = + get_custody_groups_ordered(raw_node_id, spec.number_of_custody_groups, spec)?; + + let mut ordered_custody_columns = vec![]; + for custody_index in all_custody_groups_ordered { + let columns = compute_columns_for_custody_group::(custody_index, spec)?; + ordered_custody_columns.extend(columns); + } + Ok(ordered_custody_columns) +} + /// Returns the columns that are associated with a given custody group. /// /// spec: https://github.com/ethereum/consensus-specs/blob/8e0d0d48e81d6c7c5a8253ab61340f5ea5bac66a/specs/fulu/das-core.md#compute_columns_for_custody_group -pub fn compute_columns_for_custody_group( +pub fn compute_columns_for_custody_group( custody_group: CustodyIndex, spec: &ChainSpec, ) -> Result, DataColumnCustodyGroupError> { @@ -67,7 +118,7 @@ pub fn compute_columns_for_custody_group( } let mut columns = Vec::new(); - for i in 0..spec.data_columns_per_group() { + for i in 0..spec.data_columns_per_group::() { let column = number_of_custody_groups .safe_mul(i) .and_then(|v| v.safe_add(custody_group)) @@ -78,7 +129,7 @@ pub fn compute_columns_for_custody_group( Ok(columns.into_iter()) } -pub fn compute_subnets_for_node( +pub fn compute_subnets_for_node( raw_node_id: [u8; 32], custody_group_count: u64, spec: &ChainSpec, @@ -87,7 +138,7 @@ pub fn compute_subnets_for_node( let mut subnets = HashSet::new(); for custody_group in custody_groups { - let custody_group_subnets = compute_subnets_from_custody_group(custody_group, spec)?; + let custody_group_subnets = compute_subnets_from_custody_group::(custody_group, spec)?; subnets.extend(custody_group_subnets); } @@ -95,11 +146,11 @@ pub fn compute_subnets_for_node( } /// Returns the subnets that are associated with a given custody group. -pub fn compute_subnets_from_custody_group( +pub fn compute_subnets_from_custody_group( custody_group: CustodyIndex, spec: &ChainSpec, ) -> Result + '_, DataColumnCustodyGroupError> { - let result = compute_columns_for_custody_group(custody_group, spec)? + let result = compute_columns_for_custody_group::(custody_group, spec)? .map(|column_index| DataColumnSubnetId::from_column_index(column_index, spec)) .unique(); Ok(result) @@ -108,19 +159,23 @@ pub fn compute_subnets_from_custody_group( #[cfg(test)] mod test { use super::*; + use crate::MainnetEthSpec; + + type E = MainnetEthSpec; #[test] fn test_compute_columns_for_custody_group() { let mut spec = ChainSpec::mainnet(); spec.number_of_custody_groups = 64; - spec.number_of_columns = 128; - let columns_per_custody_group = spec.number_of_columns / spec.number_of_custody_groups; + + let columns_per_custody_group = + E::number_of_columns() / (spec.number_of_custody_groups as usize); for custody_group in 0..spec.number_of_custody_groups { - let columns = compute_columns_for_custody_group(custody_group, &spec) + let columns = compute_columns_for_custody_group::(custody_group, &spec) .unwrap() .collect::>(); - assert_eq!(columns.len(), columns_per_custody_group as usize); + assert_eq!(columns.len(), columns_per_custody_group); } } @@ -128,14 +183,13 @@ mod test { fn test_compute_subnets_from_custody_group() { let mut spec = ChainSpec::mainnet(); spec.number_of_custody_groups = 64; - spec.number_of_columns = 256; spec.data_column_sidecar_subnet_count = 128; let subnets_per_custody_group = spec.data_column_sidecar_subnet_count / spec.number_of_custody_groups; for custody_group in 0..spec.number_of_custody_groups { - let subnets = compute_subnets_from_custody_group(custody_group, &spec) + let subnets = compute_subnets_from_custody_group::(custody_group, &spec) .unwrap() .collect::>(); assert_eq!(subnets.len(), subnets_per_custody_group as usize); diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs similarity index 59% rename from consensus/types/src/data_column_sidecar.rs rename to consensus/types/src/data/data_column_sidecar.rs index 5ec2b28b2b..71d821f83e 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -1,90 +1,51 @@ -use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ - BeaconBlockHeader, BeaconStateError, Epoch, EthSpec, ForkName, Hash256, RuntimeVariableList, - SignedBeaconBlockHeader, Slot, -}; +use std::sync::Arc; + use bls::Signature; -use derivative::Derivative; -use kzg::Error as KzgError; +use context_deserialize::context_deserialize; +use educe::Educe; use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; -use ssz::{DecodeError, Encode}; +use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::Error as SszError; use ssz_types::{FixedVector, VariableList}; -use std::sync::Arc; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + block::{BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlockHeader}, + core::{Epoch, EthSpec, Hash256, Slot}, + fork::ForkName, + kzg_ext::{KzgCommitments, KzgError}, + state::BeaconStateError, + test_utils::TestRandom, +}; + pub type ColumnIndex = u64; pub type Cell = FixedVector::BytesPerCell>; pub type DataColumn = VariableList, ::MaxBlobCommitmentsPerBlock>; /// Identifies a set of data columns associated with a specific beacon block. -#[derive(Encode, Clone, Debug, PartialEq)] -pub struct DataColumnsByRootIdentifier { +#[derive(Encode, Decode, Clone, Debug, PartialEq, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] +pub struct DataColumnsByRootIdentifier { pub block_root: Hash256, - pub columns: RuntimeVariableList, -} - -impl RuntimeVariableList { - pub fn from_ssz_bytes_with_nested( - bytes: &[u8], - max_len: usize, - num_columns: usize, - ) -> Result { - if bytes.is_empty() { - return Ok(RuntimeVariableList::empty(max_len)); - } - - let vec = ssz::decode_list_of_variable_length_items::, Vec>>( - bytes, - Some(max_len), - )? - .into_iter() - .map(|bytes| { - let mut builder = ssz::SszDecoderBuilder::new(&bytes); - builder.register_type::()?; - builder.register_anonymous_variable_length_item()?; - - let mut decoder = builder.build()?; - let block_root = decoder.decode_next()?; - let columns = decoder.decode_next_with(|bytes| { - RuntimeVariableList::from_ssz_bytes(bytes, num_columns) - })?; - Ok(DataColumnsByRootIdentifier { - block_root, - columns, - }) - }) - .collect::, _>>()?; - - Ok(RuntimeVariableList::from_vec(vec, max_len)) - } + pub columns: VariableList, } pub type DataColumnSidecarList = Vec>>; -#[derive( - Debug, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - Derivative, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, Educe)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] #[context_deserialize(ForkName)] pub struct DataColumnSidecar { #[serde(with = "serde_utils::quoted_u64")] @@ -183,6 +144,7 @@ pub enum DataColumnSidecarError { PreDeneb, SszError(SszError), BuildSidecarFailed(String), + InvalidCellProofLength { expected: usize, actual: usize }, } impl From for DataColumnSidecarError { @@ -208,45 +170,3 @@ impl From for DataColumnSidecarError { Self::SszError(e) } } - -#[cfg(test)] -mod test { - use super::*; - use bls::FixedBytesExtended; - - #[test] - fn round_trip_dcbroot_list() { - let max_outer = 5; - let max_inner = 10; - - let data = vec![ - DataColumnsByRootIdentifier { - block_root: Hash256::from_low_u64_be(10), - columns: RuntimeVariableList::::from_vec(vec![1u64, 2, 3], max_inner), - }, - DataColumnsByRootIdentifier { - block_root: Hash256::from_low_u64_be(20), - columns: RuntimeVariableList::::from_vec(vec![4u64, 5], max_inner), - }, - ]; - - let list = RuntimeVariableList::from_vec(data.clone(), max_outer); - - let ssz_bytes = list.as_ssz_bytes(); - - let decoded = - RuntimeVariableList::::from_ssz_bytes_with_nested( - &ssz_bytes, max_outer, max_inner, - ) - .expect("should decode list of DataColumnsByRootIdentifier"); - - assert_eq!(decoded.len(), data.len()); - for (original, decoded) in data.iter().zip(decoded.iter()) { - assert_eq!(decoded.block_root, original.block_root); - assert_eq!( - decoded.columns.iter().copied().collect::>(), - original.columns.iter().copied().collect::>() - ); - } - } -} diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data/data_column_subnet_id.rs similarity index 74% rename from consensus/types/src/data_column_subnet_id.rs rename to consensus/types/src/data/data_column_subnet_id.rs index 5b3eef24cc..c30ebbba20 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data/data_column_subnet_id.rs @@ -1,15 +1,25 @@ //! Identifies each data column subnet by an integer identifier. -use crate::data_column_sidecar::ColumnIndex; -use crate::ChainSpec; -use safe_arith::{ArithError, SafeArith}; -use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display}; -use std::ops::{Deref, DerefMut}; +use std::{ + fmt::{self, Display}, + ops::{Deref, DerefMut}, +}; -#[derive(arbitrary::Arbitrary, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +use safe_arith::SafeArith; +use serde::{Deserialize, Serialize}; + +use crate::{core::ChainSpec, data::ColumnIndex}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct DataColumnSubnetId(#[serde(with = "serde_utils::quoted_u64")] u64); +impl fmt::Debug for DataColumnSubnetId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } +} + impl DataColumnSubnetId { pub fn new(id: u64) -> Self { id.into() @@ -62,15 +72,3 @@ impl From<&DataColumnSubnetId> for u64 { val.0 } } - -#[derive(Debug)] -pub enum Error { - ArithError(ArithError), - InvalidCustodySubnetCount(u64), -} - -impl From for Error { - fn from(e: ArithError) -> Self { - Error::ArithError(e) - } -} diff --git a/consensus/types/src/data/mod.rs b/consensus/types/src/data/mod.rs new file mode 100644 index 0000000000..10d062bada --- /dev/null +++ b/consensus/types/src/data/mod.rs @@ -0,0 +1,23 @@ +mod blob_sidecar; +mod data_column_custody_group; +mod data_column_sidecar; +mod data_column_subnet_id; + +pub use blob_sidecar::{ + BlobIdentifier, BlobSidecar, BlobSidecarError, BlobSidecarList, BlobsList, FixedBlobSidecarList, +}; +pub use data_column_custody_group::{ + CustodyIndex, DataColumnCustodyGroupError, compute_columns_for_custody_group, + compute_ordered_custody_column_indices, compute_subnets_for_node, + compute_subnets_from_custody_group, get_custody_groups, +}; +pub use data_column_sidecar::{ + Cell, ColumnIndex, DataColumn, DataColumnSidecar, DataColumnSidecarError, + DataColumnSidecarList, DataColumnsByRootIdentifier, +}; +pub use data_column_subnet_id::DataColumnSubnetId; + +use crate::core::EthSpec; +use ssz_types::FixedVector; + +pub type Blob = FixedVector::BytesPerBlob>; diff --git a/consensus/types/src/deposit.rs b/consensus/types/src/deposit/deposit.rs similarity index 58% rename from consensus/types/src/deposit.rs rename to consensus/types/src/deposit/deposit.rs index 8b4b6af95d..0b08bd6509 100644 --- a/consensus/types/src/deposit.rs +++ b/consensus/types/src/deposit/deposit.rs @@ -1,29 +1,21 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::*; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::typenum::U33; +use ssz_types::FixedVector; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use typenum::U33; + +use crate::{core::Hash256, deposit::DepositData, fork::ForkName, test_utils::TestRandom}; pub const DEPOSIT_TREE_DEPTH: usize = 32; /// A deposit to potentially become a beacon chain validator. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct Deposit { diff --git a/consensus/types/src/deposit_data.rs b/consensus/types/src/deposit/deposit_data.rs similarity index 76% rename from consensus/types/src/deposit_data.rs rename to consensus/types/src/deposit/deposit_data.rs index d29e8c8d14..51697f5d1a 100644 --- a/consensus/types/src/deposit_data.rs +++ b/consensus/types/src/deposit/deposit_data.rs @@ -1,25 +1,23 @@ -use crate::test_utils::TestRandom; -use crate::*; +use bls::{PublicKeyBytes, SecretKey, SignatureBytes}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, Hash256, SignedRoot}, + deposit::DepositMessage, + fork::ForkName, + test_utils::TestRandom, +}; + /// The data supplied by the user to the deposit contract. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct DepositData { diff --git a/consensus/types/src/deposit_message.rs b/consensus/types/src/deposit/deposit_message.rs similarity index 62% rename from consensus/types/src/deposit_message.rs rename to consensus/types/src/deposit/deposit_message.rs index 5c2a0b7c2b..4495a5c023 100644 --- a/consensus/types/src/deposit_message.rs +++ b/consensus/types/src/deposit/deposit_message.rs @@ -1,26 +1,21 @@ -use crate::test_utils::TestRandom; -use crate::*; - +use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Hash256, SignedRoot}, + fork::ForkName, + test_utils::TestRandom, +}; + /// The data supplied by the user to the deposit contract. /// /// Spec v0.12.1 -#[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, -)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[context_deserialize(ForkName)] pub struct DepositMessage { pub pubkey: PublicKeyBytes, diff --git a/consensus/types/src/deposit_request.rs b/consensus/types/src/deposit/deposit_request.rs similarity index 73% rename from consensus/types/src/deposit_request.rs rename to consensus/types/src/deposit/deposit_request.rs index 141258b5ab..8d3c6e88ba 100644 --- a/consensus/types/src/deposit_request.rs +++ b/consensus/types/src/deposit/deposit_request.rs @@ -1,25 +1,16 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ForkName, Hash256, PublicKeyBytes}; -use bls::SignatureBytes; +use bls::{PublicKeyBytes, SignatureBytes}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Hash256, fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct DepositRequest { diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit/deposit_tree_snapshot.rs similarity index 93% rename from consensus/types/src/deposit_tree_snapshot.rs rename to consensus/types/src/deposit/deposit_tree_snapshot.rs index 2f9df8758b..24f41397a0 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit/deposit_tree_snapshot.rs @@ -1,10 +1,11 @@ -use crate::*; -use ethereum_hashing::{hash32_concat, ZERO_HASHES}; +use ethereum_hashing::{ZERO_HASHES, hash32_concat}; +use fixed_bytes::FixedBytesExtended; use int_to_bytes::int_to_bytes32; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; -use test_utils::TestRandom; + +use crate::{core::Hash256, deposit::DEPOSIT_TREE_DEPTH, test_utils::TestRandom}; #[derive(Encode, Decode, Deserialize, Serialize, Clone, Debug, PartialEq, TestRandom)] pub struct FinalizedExecutionBlock { diff --git a/consensus/types/src/deposit/mod.rs b/consensus/types/src/deposit/mod.rs new file mode 100644 index 0000000000..ff80f65cdb --- /dev/null +++ b/consensus/types/src/deposit/mod.rs @@ -0,0 +1,13 @@ +mod deposit; +mod deposit_data; +mod deposit_message; +mod deposit_request; +mod deposit_tree_snapshot; +mod pending_deposit; + +pub use deposit::{DEPOSIT_TREE_DEPTH, Deposit}; +pub use deposit_data::DepositData; +pub use deposit_message::DepositMessage; +pub use deposit_request::DepositRequest; +pub use deposit_tree_snapshot::{DepositTreeSnapshot, FinalizedExecutionBlock}; +pub use pending_deposit::PendingDeposit; diff --git a/consensus/types/src/pending_deposit.rs b/consensus/types/src/deposit/pending_deposit.rs similarity index 59% rename from consensus/types/src/pending_deposit.rs rename to consensus/types/src/deposit/pending_deposit.rs index 970c326467..4c039af39c 100644 --- a/consensus/types/src/pending_deposit.rs +++ b/consensus/types/src/deposit/pending_deposit.rs @@ -1,22 +1,19 @@ -use crate::test_utils::TestRandom; -use crate::*; +use bls::{PublicKeyBytes, SignatureBytes}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Hash256, Slot}, + fork::ForkName, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct PendingDeposit { diff --git a/consensus/types/src/bls_to_execution_change.rs b/consensus/types/src/execution/bls_to_execution_change.rs similarity index 72% rename from consensus/types/src/bls_to_execution_change.rs rename to consensus/types/src/execution/bls_to_execution_change.rs index b333862220..de14f1b4c5 100644 --- a/consensus/types/src/bls_to_execution_change.rs +++ b/consensus/types/src/execution/bls_to_execution_change.rs @@ -1,23 +1,20 @@ -use crate::test_utils::TestRandom; -use crate::*; +use bls::{PublicKeyBytes, SecretKey}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Address, ChainSpec, Domain, Hash256, SignedRoot}, + execution::SignedBlsToExecutionChange, + fork::ForkName, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct BlsToExecutionChange { diff --git a/consensus/types/src/execution/dumb_macros.rs b/consensus/types/src/execution/dumb_macros.rs new file mode 100644 index 0000000000..bee782294b --- /dev/null +++ b/consensus/types/src/execution/dumb_macros.rs @@ -0,0 +1,123 @@ +// These would usually be created by superstuct but now there's no longer a 1:1 mapping between +// the variants for ExecutionPayload and the variants for +// - ExecutionPayloadHeader +// - FullPayload +// - BlindedPayload +// TODO(EIP-7732): get rid of this whole file and panics once the engine_api is refactored for ePBS + +#[macro_export] +macro_rules! map_execution_payload_into_full_payload { + ($value:expr, $f:expr) => { + match $value { + ExecutionPayload::Bellatrix(inner) => { + let f: fn(ExecutionPayloadBellatrix<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Bellatrix) + } + ExecutionPayload::Capella(inner) => { + let f: fn(ExecutionPayloadCapella<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Capella) + } + ExecutionPayload::Deneb(inner) => { + let f: fn(ExecutionPayloadDeneb<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Deneb) + } + ExecutionPayload::Electra(inner) => { + let f: fn(ExecutionPayloadElectra<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Electra) + } + ExecutionPayload::Fulu(inner) => { + let f: fn(ExecutionPayloadFulu<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Fulu) + } + ExecutionPayload::Eip7805(inner) => { + let f: fn(ExecutionPayloadEip7805<_>, fn(_) -> _) -> _ = $f; + f(inner, FullPayload::Eip7805) + } + ExecutionPayload::Gloas(_) => panic!("FullPayload::Gloas does not exist!"), + } + }; +} + +#[macro_export] +macro_rules! map_execution_payload_into_blinded_payload { + ($value:expr, $f:expr) => { + match $value { + ExecutionPayload::Bellatrix(inner) => { + let f: fn(ExecutionPayloadBellatrix<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Bellatrix) + } + ExecutionPayload::Capella(inner) => { + let f: fn(ExecutionPayloadCapella<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Capella) + } + ExecutionPayload::Deneb(inner) => { + let f: fn(ExecutionPayloadDeneb<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Deneb) + } + ExecutionPayload::Electra(inner) => { + let f: fn(ExecutionPayloadElectra<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Electra) + } + ExecutionPayload::Fulu(inner) => { + let f: fn(ExecutionPayloadFulu<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Fulu) + } + ExecutionPayload::Eip7805(inner) => { + let f: fn(ExecutionPayloadEip7805<_>, fn(_) -> _) -> _ = $f; + f(inner, BlindedPayload::Eip7805) + } + ExecutionPayload::Gloas(_) => panic!("BlindedPayload::Gloas does not exist!"), + } + }; +} + +#[macro_export] +macro_rules! map_execution_payload_ref_into_execution_payload_header { + (&$lifetime:tt _, $value:expr, $f:expr) => { + match $value { + ExecutionPayloadRef::Bellatrix(inner) => { + let f: fn( + &$lifetime ExecutionPayloadBellatrix<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Bellatrix) + } + ExecutionPayloadRef::Capella(inner) => { + let f: fn( + &$lifetime ExecutionPayloadCapella<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Capella) + } + ExecutionPayloadRef::Deneb(inner) => { + let f: fn( + &$lifetime ExecutionPayloadDeneb<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Deneb) + } + ExecutionPayloadRef::Electra(inner) => { + let f: fn( + &$lifetime ExecutionPayloadElectra<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Electra) + } + ExecutionPayloadRef::Fulu(inner) => { + let f: fn( + &$lifetime ExecutionPayloadFulu<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Fulu) + } + ExecutionPayloadRef::Eip7805(inner) => { + let f: fn( + &$lifetime ExecutionPayloadEip7805<_>, + fn(_) -> _, + ) -> _ = $f; + f(inner, ExecutionPayloadHeader::Eip7805) + } + ExecutionPayloadRef::Gloas(_) => panic!("ExecutionPayloadHeader::Gloas does not exist!"), + } + } +} diff --git a/consensus/types/src/eth1_data.rs b/consensus/types/src/execution/eth1_data.rs similarity index 78% rename from consensus/types/src/eth1_data.rs rename to consensus/types/src/execution/eth1_data.rs index 7bd0d3228d..89a4e634a6 100644 --- a/consensus/types/src/eth1_data.rs +++ b/consensus/types/src/execution/eth1_data.rs @@ -1,17 +1,16 @@ -use super::Hash256; -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::ForkName; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Hash256, fork::ForkName, test_utils::TestRandom}; + /// Contains data obtained from the Eth1 chain. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, Debug, PartialEq, Clone, diff --git a/consensus/types/src/execution_block_hash.rs b/consensus/types/src/execution/execution_block_hash.rs similarity index 86% rename from consensus/types/src/execution_block_hash.rs rename to consensus/types/src/execution/execution_block_hash.rs index 6c031f6899..91c019ce04 100644 --- a/consensus/types/src/execution_block_hash.rs +++ b/consensus/types/src/execution/execution_block_hash.rs @@ -1,28 +1,23 @@ -use crate::test_utils::TestRandom; -use crate::FixedBytesExtended; -use crate::Hash256; -use derivative::Derivative; +use std::fmt; + +use fixed_bytes::FixedBytesExtended; use rand::RngCore; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; -use std::fmt; -#[derive( - arbitrary::Arbitrary, - Default, - Clone, - Copy, - Serialize, - Deserialize, - Eq, - PartialEq, - Hash, - Derivative, -)] -#[derivative(Debug = "transparent")] +use crate::{core::Hash256, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)] #[serde(transparent)] pub struct ExecutionBlockHash(#[serde(with = "serde_utils::b256_hex")] pub Hash256); +impl fmt::Debug for ExecutionBlockHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } +} + impl ExecutionBlockHash { pub fn zero() -> Self { Self(Hash256::zero()) diff --git a/consensus/types/src/execution_block_header.rs b/consensus/types/src/execution/execution_block_header.rs similarity index 98% rename from consensus/types/src/execution_block_header.rs rename to consensus/types/src/execution/execution_block_header.rs index 60f2960afb..e596ba1831 100644 --- a/consensus/types/src/execution_block_header.rs +++ b/consensus/types/src/execution/execution_block_header.rs @@ -17,10 +17,15 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -use crate::{Address, EthSpec, ExecutionPayloadRef, Hash256, Hash64, Uint256}; use alloy_rlp::RlpEncodable; +use fixed_bytes::Uint256; use metastruct::metastruct; +use crate::{ + core::{Address, EthSpec, Hash64, Hash256}, + execution::ExecutionPayloadRef, +}; + /// Execution block header as used for RLP encoding and Keccak hashing. /// /// Credit to Reth for the type definition. diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution/execution_payload.rs similarity index 77% rename from consensus/types/src/execution_payload.rs rename to consensus/types/src/execution/execution_payload.rs index fdcc500948..00f8e036cf 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution/execution_payload.rs @@ -1,21 +1,31 @@ -use crate::{test_utils::TestRandom, *}; -use derivative::Derivative; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; +use fixed_bytes::Uint256; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use ssz_types::{FixedVector, VariableList}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Address, EthSpec, Hash256}, + execution::ExecutionBlockHash, + fork::{ForkName, ForkVersionDecode}, + state::BeaconStateError, + test_utils::TestRandom, + withdrawal::Withdrawals, +}; + pub type Transaction = VariableList; pub type Transactions = VariableList< Transaction<::MaxBytesPerTransaction>, ::MaxTransactionsPerPayload, >; -pub type Withdrawals = VariableList::MaxWithdrawalsPerPayload>; - #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive( Default, @@ -27,25 +37,34 @@ pub type Withdrawals = VariableList::MaxWithdrawal Decode, TreeHash, TestRandom, - Derivative, - arbitrary::Arbitrary + Educe, ), context_deserialize(ForkName), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec") + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ), - cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - map_into(FullPayload, BlindedPayload), - map_ref_into(ExecutionPayloadHeader) + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) )] -#[derive( - Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec", untagged)] -#[arbitrary(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] pub struct ExecutionPayload { @@ -83,12 +102,12 @@ pub struct ExecutionPayload { pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, - #[superstruct(only(Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] pub withdrawals: Withdrawals, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub blob_gas_used: u64, - #[superstruct(only(Deneb, Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub excess_blob_gas: u64, } @@ -118,6 +137,7 @@ impl ForkVersionDecode for ExecutionPayload { ForkName::Electra => ExecutionPayloadElectra::from_ssz_bytes(bytes).map(Self::Electra), ForkName::Eip7805 => ExecutionPayloadEip7805::from_ssz_bytes(bytes).map(Self::Eip7805), ForkName::Fulu => ExecutionPayloadFulu::from_ssz_bytes(bytes).map(Self::Fulu), + ForkName::Gloas => ExecutionPayloadGloas::from_ssz_bytes(bytes).map(Self::Gloas), } } } @@ -125,6 +145,7 @@ impl ForkVersionDecode for ExecutionPayload { impl ExecutionPayload { #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. + /// TODO(EIP-7732): this seems to only exist for the Bellatrix fork, but Mark's branch has it for all the forks, i.e. max_execution_payload_eip7732_size pub fn max_execution_payload_bellatrix_size() -> usize { // Fixed part ExecutionPayloadBellatrix::::default().as_ssz_bytes().len() @@ -148,7 +169,7 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayload return Err(serde::de::Error::custom(format!( "ExecutionPayload failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Bellatrix => { Self::Bellatrix(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -168,6 +189,9 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayload ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } + ForkName::Gloas => { + Self::Gloas(Deserialize::deserialize(deserializer).map_err(convert_err)?) + } }) } } @@ -181,6 +205,7 @@ impl ExecutionPayload { ExecutionPayload::Electra(_) => ForkName::Electra, ExecutionPayload::Eip7805(_) => ForkName::Eip7805, ExecutionPayload::Fulu(_) => ForkName::Fulu, + ExecutionPayload::Gloas(_) => ForkName::Gloas, } } } diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs new file mode 100644 index 0000000000..20e461334d --- /dev/null +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -0,0 +1,40 @@ +use crate::test_utils::TestRandom; +use crate::{Address, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive( + Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe, TestRandom, +)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[educe(PartialEq, Hash)] +#[context_deserialize(ForkName)] +// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid +pub struct ExecutionPayloadBid { + pub parent_block_hash: ExecutionBlockHash, + pub parent_block_root: Hash256, + pub block_hash: ExecutionBlockHash, + #[serde(with = "serde_utils::address_hex")] + pub fee_recipient: Address, + #[serde(with = "serde_utils::quoted_u64")] + pub gas_limit: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub builder_index: u64, + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + pub value: u64, + pub blob_kzg_commitments_root: Hash256, +} + +impl SignedRoot for ExecutionPayloadBid {} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(ExecutionPayloadBid); +} diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs new file mode 100644 index 0000000000..64e03cec5a --- /dev/null +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -0,0 +1,36 @@ +use crate::test_utils::TestRandom; +use crate::{ + EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, + SignedRoot, Slot, +}; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(Debug, Clone, Serialize, Encode, Decode, Deserialize, TestRandom, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[context_deserialize(ForkName)] +#[serde(bound = "E: EthSpec")] +pub struct ExecutionPayloadEnvelope { + pub payload: ExecutionPayloadGloas, + pub execution_requests: ExecutionRequests, + #[serde(with = "serde_utils::quoted_u64")] + pub builder_index: u64, + pub beacon_block_root: Hash256, + pub slot: Slot, + pub blob_kzg_commitments: KzgCommitments, + pub state_root: Hash256, +} + +impl SignedRoot for ExecutionPayloadEnvelope {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(ExecutionPayloadEnvelope); +} diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution/execution_payload_header.rs similarity index 92% rename from consensus/types/src/execution_payload_header.rs rename to consensus/types/src/execution/execution_payload_header.rs index 872aab46b4..02f3afa709 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution/execution_payload_header.rs @@ -1,12 +1,28 @@ -use crate::{test_utils::TestRandom, *}; -use derivative::Derivative; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use ssz_types::{FixedVector, VariableList}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + core::{Address, EthSpec, Hash256, Uint256}, + execution::{ + ExecutionBlockHash, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, + ExecutionPayloadFulu, ExecutionPayloadRef, Transactions, + }, + fork::ForkName, + map_execution_payload_ref_into_execution_payload_header, + state::BeaconStateError, + test_utils::TestRandom, +}; + #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), variant_attributes( @@ -20,28 +36,39 @@ use tree_hash_derive::TreeHash; Decode, TreeHash, TestRandom, - Derivative, - arbitrary::Arbitrary + Educe, ), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ), ref_attributes( derive(PartialEq, TreeHash, Debug), tree_hash(enum_behaviour = "transparent") ), - cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), map_ref_into(ExecutionPayloadHeader) )] -#[derive( - Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec", untagged)] -#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] pub struct ExecutionPayloadHeader { @@ -113,13 +140,19 @@ impl ExecutionPayloadHeader { ExecutionPayloadHeaderEip7805::from_ssz_bytes(bytes).map(Self::Eip7805) } ForkName::Fulu => ExecutionPayloadHeaderFulu::from_ssz_bytes(bytes).map(Self::Fulu), + ForkName::Gloas => Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadHeader: {fork_name}", + ))), } } #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { // TODO(newfork): Add a new case here if there are new variable fields - if fork_name.bellatrix_enabled() { + if fork_name.gloas_enabled() { + // TODO(EIP7732): check this + 0 + } else if fork_name.bellatrix_enabled() { // Max size of variable length `extra_data` field E::max_extra_data_bytes() * ::ssz_fixed_len() } else { @@ -218,7 +251,7 @@ impl ExecutionPayloadHeaderDeneb { } } -impl ExecutionPayloadHeaderElectra { +impl ExecutionPayloadHeaderFulu { pub fn upgrade_to_eip7805(&self) -> ExecutionPayloadHeaderEip7805 { ExecutionPayloadHeaderEip7805 { parent_hash: self.parent_hash, @@ -242,7 +275,7 @@ impl ExecutionPayloadHeaderElectra { } } -impl ExecutionPayloadHeaderEip7805 { +impl ExecutionPayloadHeaderElectra { pub fn upgrade_to_fulu(&self) -> ExecutionPayloadHeaderFulu { ExecutionPayloadHeaderFulu { parent_hash: self.parent_hash, @@ -558,12 +591,6 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadHead )) }; Ok(match context { - ForkName::Base | ForkName::Altair => { - return Err(serde::de::Error::custom(format!( - "ExecutionPayloadHeader failed to deserialize: unsupported fork '{}'", - context - ))) - } ForkName::Bellatrix => { Self::Bellatrix(Deserialize::deserialize(deserializer).map_err(convert_err)?) } @@ -582,6 +609,13 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for ExecutionPayloadHead ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } + + ForkName::Base | ForkName::Altair | ForkName::Gloas => { + return Err(serde::de::Error::custom(format!( + "ExecutionPayloadHeader failed to deserialize: unsupported fork '{}'", + context + ))); + } }) } } diff --git a/consensus/types/src/execution_requests.rs b/consensus/types/src/execution/execution_requests.rs similarity index 86% rename from consensus/types/src/execution_requests.rs rename to consensus/types/src/execution/execution_requests.rs index 2fec3b5f66..92d717778e 100644 --- a/consensus/types/src/execution_requests.rs +++ b/consensus/types/src/execution/execution_requests.rs @@ -1,8 +1,6 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ConsolidationRequest, DepositRequest, EthSpec, ForkName, Hash256, WithdrawalRequest}; use alloy_primitives::Bytes; -use derivative::Derivative; +use context_deserialize::context_deserialize; +use educe::Educe; use ethereum_hashing::{DynamicContext, Sha256Context}; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -11,6 +9,15 @@ use ssz_types::VariableList; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + consolidation::ConsolidationRequest, + core::{EthSpec, Hash256}, + deposit::DepositRequest, + fork::ForkName, + test_utils::TestRandom, + withdrawal::WithdrawalRequest, +}; + pub type DepositRequests = VariableList::MaxDepositRequestsPerPayload>; pub type WithdrawalRequests = @@ -18,22 +25,16 @@ pub type WithdrawalRequests = pub type ConsolidationRequests = VariableList::MaxConsolidationRequestsPerPayload>; +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[derive( - arbitrary::Arbitrary, - Debug, - Derivative, - Default, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, Educe, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] #[context_deserialize(ForkName)] pub struct ExecutionRequests { pub deposits: DepositRequests, diff --git a/consensus/types/src/execution/inclusion_list.rs b/consensus/types/src/execution/inclusion_list.rs new file mode 100644 index 0000000000..0b61d89090 --- /dev/null +++ b/consensus/types/src/execution/inclusion_list.rs @@ -0,0 +1,54 @@ +use crate::test_utils::TestRandom; +use crate::{EthSpec, Hash256, SignedRoot, Slot, Transactions}; +use bls::{PublicKeyBytes, Signature}; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use ssz_types::FixedVector; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +pub type InclusionListCommittee = FixedVector::InclusionListCommitteeSize>; + +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Educe, Decode, TreeHash, TestRandom)] +#[serde(bound = "E: EthSpec")] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] +pub struct InclusionList { + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub inclusion_list_committee_root: Hash256, + #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] + pub transactions: Transactions, +} + +impl SignedRoot for InclusionList {} + +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Educe)] +#[serde(bound = "E: EthSpec")] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] +pub struct SignedInclusionList { + pub message: InclusionList, + pub signature: Signature, +} + +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +pub struct InclusionListDuty { + /// The slot during which the validator must produce an inclusion list. + pub slot: Slot, + #[serde(with = "serde_utils::quoted_u64")] + /// The index of the validator. + pub validator_index: u64, + /// The hash tree root of the inclusion list committee. + pub committee_root: Hash256, + /// The pubkey of the validator. + pub pubkey: PublicKeyBytes, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + + ssz_and_tree_hash_tests!(InclusionList); +} diff --git a/consensus/types/src/execution/mod.rs b/consensus/types/src/execution/mod.rs new file mode 100644 index 0000000000..eac4291e77 --- /dev/null +++ b/consensus/types/src/execution/mod.rs @@ -0,0 +1,49 @@ +mod eth1_data; +mod execution_block_hash; +mod execution_block_header; +#[macro_use] +mod execution_payload; +mod bls_to_execution_change; +mod dumb_macros; +mod execution_payload_bid; +mod execution_payload_envelope; +mod execution_payload_header; +mod execution_requests; +mod inclusion_list; +mod payload; +mod signed_bls_to_execution_change; +mod signed_execution_payload_bid; +mod signed_execution_payload_envelope; + +pub use bls_to_execution_change::BlsToExecutionChange; +pub use eth1_data::Eth1Data; +pub use execution_block_hash::ExecutionBlockHash; +pub use execution_block_header::{EncodableExecutionBlockHeader, ExecutionBlockHeader}; +pub use execution_payload::{ + ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, + ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, + ExecutionPayloadRef, Transaction, Transactions, +}; +pub use execution_payload_bid::ExecutionPayloadBid; +pub use execution_payload_envelope::ExecutionPayloadEnvelope; +pub use execution_payload_header::{ + ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, ExecutionPayloadHeaderElectra, + ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, +}; +pub use execution_requests::{ + ConsolidationRequests, DepositRequests, ExecutionRequests, RequestType, WithdrawalRequests, +}; +pub use inclusion_list::{ + InclusionList, InclusionListCommittee, InclusionListDuty, SignedInclusionList, +}; +pub use payload::{ + AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, + BlindedPayloadDeneb, BlindedPayloadEip7805, BlindedPayloadElectra, BlindedPayloadFulu, + BlindedPayloadRef, BlockProductionVersion, BlockType, ExecPayload, FullPayload, + FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, FullPayloadEip7805, + FullPayloadElectra, FullPayloadFulu, FullPayloadRef, OwnedExecPayload, +}; +pub use signed_bls_to_execution_change::SignedBlsToExecutionChange; +pub use signed_execution_payload_bid::SignedExecutionPayloadBid; +pub use signed_execution_payload_envelope::SignedExecutionPayloadEnvelope; diff --git a/consensus/types/src/payload.rs b/consensus/types/src/execution/payload.rs similarity index 83% rename from consensus/types/src/payload.rs rename to consensus/types/src/execution/payload.rs index 03b542cef5..784bc76277 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/execution/payload.rs @@ -1,16 +1,31 @@ -use crate::{test_utils::TestRandom, *}; -use derivative::Derivative; +use educe::Educe; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; -use std::borrow::Cow; -use std::fmt::Debug; -use std::hash::Hash; +use ssz_types::VariableList; +use std::{borrow::Cow, fmt::Debug, hash::Hash}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + core::{Address, EthSpec, Hash256}, + execution::{ + ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, + ExecutionPayloadFulu, ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, + ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadRef, + Transactions, + }, + fork::ForkName, + map_execution_payload_into_blinded_payload, map_execution_payload_into_full_payload, + state::BeaconStateError, + test_utils::TestRandom, +}; + #[derive(Debug, PartialEq)] pub enum BlockType { Blinded, @@ -38,8 +53,8 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + fn gas_limit(&self) -> u64; fn transactions(&self) -> Option<&Transactions>; /// fork-specific fields - fn withdrawals_root(&self) -> Result; - fn blob_gas_used(&self) -> Result; + fn withdrawals_root(&self) -> Result; + fn blob_gas_used(&self) -> Result; /// Is this a default payload with 0x0 roots for transactions and withdrawals? fn is_default_with_zero_roots(&self) -> bool; @@ -49,6 +64,7 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + } /// `ExecPayload` functionality the requires ownership. +#[cfg(feature = "arbitrary")] pub trait OwnedExecPayload: ExecPayload + Default @@ -61,7 +77,7 @@ pub trait OwnedExecPayload: + 'static { } - +#[cfg(feature = "arbitrary")] impl OwnedExecPayload for P where P: ExecPayload + Default @@ -75,6 +91,25 @@ impl OwnedExecPayload for P where { } +/// `ExecPayload` functionality the requires ownership. +#[cfg(not(feature = "arbitrary"))] +pub trait OwnedExecPayload: + ExecPayload + Default + Serialize + DeserializeOwned + Encode + Decode + TestRandom + 'static +{ +} +#[cfg(not(feature = "arbitrary"))] +impl OwnedExecPayload for P where + P: ExecPayload + + Default + + Serialize + + DeserializeOwned + + Encode + + Decode + + TestRandom + + 'static +{ +} + pub trait AbstractExecPayload: ExecPayload + Sized @@ -84,8 +119,8 @@ pub trait AbstractExecPayload: + TryInto + TryInto + TryInto - + TryInto + TryInto + + TryInto + Sync { type Ref<'a>: ExecPayload @@ -94,8 +129,8 @@ pub trait AbstractExecPayload: + From<&'a Self::Capella> + From<&'a Self::Deneb> + From<&'a Self::Electra> - + From<&'a Self::Eip7805> - + From<&'a Self::Fulu>; + + From<&'a Self::Fulu> + + From<&'a Self::Eip7805>; type Bellatrix: OwnedExecPayload + Into @@ -117,16 +152,16 @@ pub trait AbstractExecPayload: + for<'a> From>> + TryFrom> + Sync; - type Eip7805: OwnedExecPayload - + Into - + for<'a> From>> - + TryFrom> - + Sync; type Fulu: OwnedExecPayload + Into + for<'a> From>> + TryFrom> + Sync; + type Eip7805: OwnedExecPayload + + Into + + for<'a> From>> + + TryFrom> + + Sync; } #[superstruct( @@ -141,28 +176,41 @@ pub trait AbstractExecPayload: Decode, TestRandom, TreeHash, - Derivative, - arbitrary::Arbitrary, + Educe, ), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ssz(struct_behaviour = "transparent"), ), ref_attributes( - derive(Debug, Derivative, TreeHash), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + derive(Debug, Educe, TreeHash), + educe(PartialEq, Hash(bound(E: EthSpec))), tree_hash(enum_behaviour = "transparent"), ), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), - cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant") + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) )] -#[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] pub struct FullPayload { #[superstruct( @@ -284,36 +332,26 @@ impl ExecPayload for FullPayload { }) } - fn withdrawals_root(&self) -> Result { + fn withdrawals_root(&self) -> Result { match self { - FullPayload::Bellatrix(_) => Err(Error::IncorrectStateVariant), - FullPayload::Capella(ref inner) => { - Ok(inner.execution_payload.withdrawals.tree_hash_root()) - } - FullPayload::Deneb(ref inner) => { - Ok(inner.execution_payload.withdrawals.tree_hash_root()) - } - FullPayload::Electra(ref inner) => { - Ok(inner.execution_payload.withdrawals.tree_hash_root()) - } - FullPayload::Eip7805(ref inner) => { - Ok(inner.execution_payload.withdrawals.tree_hash_root()) - } - FullPayload::Fulu(ref inner) => { - Ok(inner.execution_payload.withdrawals.tree_hash_root()) - } + FullPayload::Bellatrix(_) => Err(BeaconStateError::IncorrectStateVariant), + FullPayload::Capella(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), + FullPayload::Deneb(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), + FullPayload::Electra(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), + FullPayload::Fulu(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), + FullPayload::Eip7805(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), } } - fn blob_gas_used(&self) -> Result { + fn blob_gas_used(&self) -> Result { match self { FullPayload::Bellatrix(_) | FullPayload::Capella(_) => { - Err(Error::IncorrectStateVariant) + Err(BeaconStateError::IncorrectStateVariant) } - FullPayload::Deneb(ref inner) => Ok(inner.execution_payload.blob_gas_used), - FullPayload::Electra(ref inner) => Ok(inner.execution_payload.blob_gas_used), - FullPayload::Eip7805(ref inner) => Ok(inner.execution_payload.blob_gas_used), - FullPayload::Fulu(ref inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Deneb(inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Electra(inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Fulu(inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Eip7805(inner) => Ok(inner.execution_payload.blob_gas_used), } } @@ -337,15 +375,16 @@ impl FullPayload { }) } - pub fn default_at_fork(fork_name: ForkName) -> Result { + pub fn default_at_fork(fork_name: ForkName) -> Result { match fork_name { - ForkName::Base | ForkName::Altair => Err(Error::IncorrectStateVariant), + ForkName::Base | ForkName::Altair => Err(BeaconStateError::IncorrectStateVariant), ForkName::Bellatrix => Ok(FullPayloadBellatrix::default().into()), ForkName::Capella => Ok(FullPayloadCapella::default().into()), ForkName::Deneb => Ok(FullPayloadDeneb::default().into()), ForkName::Electra => Ok(FullPayloadElectra::default().into()), ForkName::Eip7805 => Ok(FullPayloadEip7805::default().into()), ForkName::Fulu => Ok(FullPayloadFulu::default().into()), + ForkName::Gloas => Err(BeaconStateError::IncorrectStateVariant), } } } @@ -433,9 +472,9 @@ impl ExecPayload for FullPayloadRef<'_, E> { }) } - fn withdrawals_root(&self) -> Result { + fn withdrawals_root(&self) -> Result { match self { - FullPayloadRef::Bellatrix(_) => Err(Error::IncorrectStateVariant), + FullPayloadRef::Bellatrix(_) => Err(BeaconStateError::IncorrectStateVariant), FullPayloadRef::Capella(inner) => { Ok(inner.execution_payload.withdrawals.tree_hash_root()) } @@ -452,10 +491,10 @@ impl ExecPayload for FullPayloadRef<'_, E> { } } - fn blob_gas_used(&self) -> Result { + fn blob_gas_used(&self) -> Result { match self { FullPayloadRef::Bellatrix(_) | FullPayloadRef::Capella(_) => { - Err(Error::IncorrectStateVariant) + Err(BeaconStateError::IncorrectStateVariant) } FullPayloadRef::Deneb(inner) => Ok(inner.execution_payload.blob_gas_used), FullPayloadRef::Electra(inner) => Ok(inner.execution_payload.blob_gas_used), @@ -514,27 +553,40 @@ impl TryFrom> for FullPayload { Decode, TestRandom, TreeHash, - Derivative, - arbitrary::Arbitrary + Educe, ), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), ssz(struct_behaviour = "transparent"), ), ref_attributes( - derive(Debug, Derivative, TreeHash), - derivative(PartialEq, Hash(bound = "E: EthSpec")), + derive(Debug, Educe, TreeHash), + educe(PartialEq, Hash(bound(E: EthSpec))), tree_hash(enum_behaviour = "transparent"), ), map_into(ExecutionPayloadHeader), - cast_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "BeaconStateError::IncorrectStateVariant") + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ) )] -#[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary)] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, Deserialize, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[tree_hash(enum_behaviour = "transparent")] pub struct BlindedPayload { #[superstruct( @@ -634,32 +686,26 @@ impl ExecPayload for BlindedPayload { None } - fn withdrawals_root(&self) -> Result { + fn withdrawals_root(&self) -> Result { match self { - BlindedPayload::Bellatrix(_) => Err(Error::IncorrectStateVariant), - BlindedPayload::Capella(ref inner) => { - Ok(inner.execution_payload_header.withdrawals_root) - } - BlindedPayload::Deneb(ref inner) => Ok(inner.execution_payload_header.withdrawals_root), - BlindedPayload::Electra(ref inner) => { - Ok(inner.execution_payload_header.withdrawals_root) - } - BlindedPayload::Eip7805(ref inner) => { - Ok(inner.execution_payload_header.withdrawals_root) - } - BlindedPayload::Fulu(ref inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Bellatrix(_) => Err(BeaconStateError::IncorrectStateVariant), + BlindedPayload::Capella(inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Deneb(inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Electra(inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Fulu(inner) => Ok(inner.execution_payload_header.withdrawals_root), + BlindedPayload::Eip7805(inner) => Ok(inner.execution_payload_header.withdrawals_root), } } - fn blob_gas_used(&self) -> Result { + fn blob_gas_used(&self) -> Result { match self { BlindedPayload::Bellatrix(_) | BlindedPayload::Capella(_) => { - Err(Error::IncorrectStateVariant) + Err(BeaconStateError::IncorrectStateVariant) } - BlindedPayload::Deneb(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), - BlindedPayload::Electra(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), - BlindedPayload::Eip7805(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), - BlindedPayload::Fulu(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Deneb(inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Electra(inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Fulu(inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Eip7805(inner) => Ok(inner.execution_payload_header.blob_gas_used), } } @@ -748,9 +794,9 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { None } - fn withdrawals_root(&self) -> Result { + fn withdrawals_root(&self) -> Result { match self { - BlindedPayloadRef::Bellatrix(_) => Err(Error::IncorrectStateVariant), + BlindedPayloadRef::Bellatrix(_) => Err(BeaconStateError::IncorrectStateVariant), BlindedPayloadRef::Capella(inner) => { Ok(inner.execution_payload_header.withdrawals_root) } @@ -765,10 +811,10 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { } } - fn blob_gas_used(&self) -> Result { + fn blob_gas_used(&self) -> Result { match self { BlindedPayloadRef::Bellatrix(_) | BlindedPayloadRef::Capella(_) => { - Err(Error::IncorrectStateVariant) + Err(BeaconStateError::IncorrectStateVariant) } BlindedPayloadRef::Deneb(inner) => Ok(inner.execution_payload_header.blob_gas_used), BlindedPayloadRef::Electra(inner) => Ok(inner.execution_payload_header.blob_gas_used), @@ -861,12 +907,12 @@ macro_rules! impl_exec_payload_common { f(self) } - fn withdrawals_root(&self) -> Result { + fn withdrawals_root(&self) -> Result { let g = $g; g(self) } - fn blob_gas_used(&self) -> Result { + fn blob_gas_used(&self) -> Result { let h = $h; h(self) } @@ -901,15 +947,16 @@ macro_rules! impl_exec_payload_for_fork { }, { |_| { None } }, { - let c: for<'a> fn(&'a $wrapper_type_header) -> Result = - |payload: &$wrapper_type_header| { - let wrapper_ref_type = BlindedPayloadRef::$fork_variant(&payload); - wrapper_ref_type.withdrawals_root() - }; + let c: for<'a> fn( + &'a $wrapper_type_header, + ) -> Result = |payload: &$wrapper_type_header| { + let wrapper_ref_type = BlindedPayloadRef::$fork_variant(&payload); + wrapper_ref_type.withdrawals_root() + }; c }, { - let c: for<'a> fn(&'a $wrapper_type_header) -> Result = + let c: for<'a> fn(&'a $wrapper_type_header) -> Result = |payload: &$wrapper_type_header| { let wrapper_ref_type = BlindedPayloadRef::$fork_variant(&payload); wrapper_ref_type.blob_gas_used() @@ -919,12 +966,12 @@ macro_rules! impl_exec_payload_for_fork { ); impl TryInto<$wrapper_type_header> for BlindedPayload { - type Error = Error; + type Error = BeaconStateError; fn try_into(self) -> Result<$wrapper_type_header, Self::Error> { match self { BlindedPayload::$fork_variant(payload) => Ok(payload), - _ => Err(Error::IncorrectStateVariant), + _ => Err(BeaconStateError::IncorrectStateVariant), } } } @@ -947,13 +994,13 @@ macro_rules! impl_exec_payload_for_fork { } impl TryFrom> for $wrapper_type_header { - type Error = Error; + type Error = BeaconStateError; fn try_from(header: ExecutionPayloadHeader) -> Result { match header { ExecutionPayloadHeader::$fork_variant(execution_payload_header) => { Ok(execution_payload_header.into()) } - _ => Err(Error::PayloadConversionLogicFlaw), + _ => Err(BeaconStateError::PayloadConversionLogicFlaw), } } } @@ -988,7 +1035,7 @@ macro_rules! impl_exec_payload_for_fork { c }, { - let c: for<'a> fn(&'a $wrapper_type_full) -> Result = + let c: for<'a> fn(&'a $wrapper_type_full) -> Result = |payload: &$wrapper_type_full| { let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); wrapper_ref_type.withdrawals_root() @@ -996,7 +1043,7 @@ macro_rules! impl_exec_payload_for_fork { c }, { - let c: for<'a> fn(&'a $wrapper_type_full) -> Result = + let c: for<'a> fn(&'a $wrapper_type_full) -> Result = |payload: &$wrapper_type_full| { let wrapper_ref_type = FullPayloadRef::$fork_variant(&payload); wrapper_ref_type.blob_gas_used() @@ -1023,26 +1070,26 @@ macro_rules! impl_exec_payload_for_fork { } impl TryFrom> for $wrapper_type_full { - type Error = Error; + type Error = BeaconStateError; fn try_from(_: ExecutionPayloadHeader) -> Result { - Err(Error::PayloadConversionLogicFlaw) + Err(BeaconStateError::PayloadConversionLogicFlaw) } } impl TryFrom<$wrapped_type_header> for $wrapper_type_full { - type Error = Error; + type Error = BeaconStateError; fn try_from(_: $wrapped_type_header) -> Result { - Err(Error::PayloadConversionLogicFlaw) + Err(BeaconStateError::PayloadConversionLogicFlaw) } } impl TryInto<$wrapper_type_full> for FullPayload { - type Error = Error; + type Error = BeaconStateError; fn try_into(self) -> Result<$wrapper_type_full, Self::Error> { match self { FullPayload::$fork_variant(payload) => Ok(payload), - _ => Err(Error::PayloadConversionLogicFlaw), + _ => Err(BeaconStateError::PayloadConversionLogicFlaw), } } } diff --git a/consensus/types/src/signed_bls_to_execution_change.rs b/consensus/types/src/execution/signed_bls_to_execution_change.rs similarity index 55% rename from consensus/types/src/signed_bls_to_execution_change.rs rename to consensus/types/src/execution/signed_bls_to_execution_change.rs index 383663e36b..535960fb3f 100644 --- a/consensus/types/src/signed_bls_to_execution_change.rs +++ b/consensus/types/src/execution/signed_bls_to_execution_change.rs @@ -1,23 +1,15 @@ -use crate::test_utils::TestRandom; -use crate::*; +use bls::Signature; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{execution::BlsToExecutionChange, fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct SignedBlsToExecutionChange { diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs new file mode 100644 index 0000000000..29dfd03ba0 --- /dev/null +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -0,0 +1,35 @@ +use crate::test_utils::TestRandom; +use crate::{ExecutionPayloadBid, ForkName}; +use bls::Signature; +use context_deserialize::context_deserialize; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[educe(PartialEq, Hash)] +#[context_deserialize(ForkName)] +// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid +pub struct SignedExecutionPayloadBid { + pub message: ExecutionPayloadBid, + pub signature: Signature, +} + +impl SignedExecutionPayloadBid { + pub fn empty() -> Self { + Self { + message: ExecutionPayloadBid::default(), + signature: Signature::empty(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); +} diff --git a/consensus/types/src/execution/signed_execution_payload_envelope.rs b/consensus/types/src/execution/signed_execution_payload_envelope.rs new file mode 100644 index 0000000000..1641041615 --- /dev/null +++ b/consensus/types/src/execution/signed_execution_payload_envelope.rs @@ -0,0 +1,24 @@ +use crate::test_utils::TestRandom; +use crate::{EthSpec, ExecutionPayloadEnvelope}; +use bls::Signature; +use educe::Educe; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +#[derive(Debug, Clone, Serialize, Encode, Decode, Deserialize, TestRandom, TreeHash, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] +#[serde(bound = "E: EthSpec")] +pub struct SignedExecutionPayloadEnvelope { + pub message: ExecutionPayloadEnvelope, + pub signature: Signature, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MainnetEthSpec; + + ssz_and_tree_hash_tests!(SignedExecutionPayloadEnvelope); +} diff --git a/consensus/types/src/exit/mod.rs b/consensus/types/src/exit/mod.rs new file mode 100644 index 0000000000..cb066d1d7a --- /dev/null +++ b/consensus/types/src/exit/mod.rs @@ -0,0 +1,5 @@ +mod signed_voluntary_exit; +mod voluntary_exit; + +pub use signed_voluntary_exit::SignedVoluntaryExit; +pub use voluntary_exit::VoluntaryExit; diff --git a/consensus/types/src/signed_voluntary_exit.rs b/consensus/types/src/exit/signed_voluntary_exit.rs similarity index 63% rename from consensus/types/src/signed_voluntary_exit.rs rename to consensus/types/src/exit/signed_voluntary_exit.rs index b6451d3ab5..b49401a721 100644 --- a/consensus/types/src/signed_voluntary_exit.rs +++ b/consensus/types/src/exit/signed_voluntary_exit.rs @@ -1,27 +1,18 @@ -use crate::context_deserialize; -use crate::{test_utils::TestRandom, ForkName, VoluntaryExit}; use bls::Signature; - +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{exit::VoluntaryExit, fork::ForkName, test_utils::TestRandom}; + /// An exit voluntarily submitted a validator who wishes to withdraw. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct SignedVoluntaryExit { diff --git a/consensus/types/src/voluntary_exit.rs b/consensus/types/src/exit/voluntary_exit.rs similarity index 80% rename from consensus/types/src/voluntary_exit.rs rename to consensus/types/src/exit/voluntary_exit.rs index 75260add4b..30c6a97c4d 100644 --- a/consensus/types/src/voluntary_exit.rs +++ b/consensus/types/src/exit/voluntary_exit.rs @@ -1,29 +1,23 @@ -use crate::context_deserialize; -use crate::{ - test_utils::TestRandom, ChainSpec, Domain, Epoch, ForkName, Hash256, SecretKey, SignedRoot, - SignedVoluntaryExit, -}; - +use bls::SecretKey; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, Domain, Epoch, Hash256, SignedRoot}, + exit::SignedVoluntaryExit, + fork::ForkName, + test_utils::TestRandom, +}; + /// An exit voluntarily submitted a validator who wishes to withdraw. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct VoluntaryExit { diff --git a/consensus/types/src/fork.rs b/consensus/types/src/fork/fork.rs similarity index 89% rename from consensus/types/src/fork.rs rename to consensus/types/src/fork/fork.rs index 239ffe33c0..371b11e05c 100644 --- a/consensus/types/src/fork.rs +++ b/consensus/types/src/fork/fork.rs @@ -1,17 +1,16 @@ -use crate::test_utils::TestRandom; -use crate::{Epoch, ForkName}; -use context_deserialize_derive::context_deserialize; - +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Epoch, fork::ForkName, test_utils::TestRandom}; + /// Specifies a fork of the `BeaconChain`, to prevent replay attacks. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, Debug, Clone, Copy, diff --git a/consensus/types/src/fork/fork_context.rs b/consensus/types/src/fork/fork_context.rs new file mode 100644 index 0000000000..3407689e79 --- /dev/null +++ b/consensus/types/src/fork/fork_context.rs @@ -0,0 +1,285 @@ +use std::collections::BTreeMap; + +use parking_lot::RwLock; + +use crate::{ + core::{ChainSpec, Epoch, EthSpec, Hash256, Slot}, + fork::ForkName, +}; + +/// Represents a hard fork in the consensus protocol. +/// +/// A hard fork can be one of two types: +/// * A named fork (represented by `ForkName`) which introduces protocol changes. +/// * A blob-parameter-only (BPO) fork which only modifies blob parameters. +/// +/// For BPO forks, the `fork_name` remains unchanged from the previous fork, +/// but the `fork_epoch` and `fork_digest` will be different to reflect the +/// new blob parameter changes. +#[derive(Debug, Clone)] +pub struct HardFork { + fork_name: ForkName, + fork_epoch: Epoch, + fork_digest: [u8; 4], +} + +impl HardFork { + pub fn new(fork_name: ForkName, fork_digest: [u8; 4], fork_epoch: Epoch) -> HardFork { + HardFork { + fork_name, + fork_epoch, + fork_digest, + } + } +} + +/// Provides fork specific info like the current fork name and the fork digests corresponding to every valid fork. +#[derive(Debug)] +pub struct ForkContext { + current_fork: RwLock, + epoch_to_forks: BTreeMap, + pub spec: ChainSpec, +} + +impl ForkContext { + /// Creates a new `ForkContext` object by enumerating all enabled forks and computing their + /// fork digest. + /// + /// A fork is disabled in the `ChainSpec` if the activation slot corresponding to that fork is `None`. + pub fn new( + current_slot: Slot, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> Self { + let epoch_to_forks: BTreeMap<_, _> = spec + .all_digest_epochs() + .map(|epoch| { + let fork_name = spec.fork_name_at_epoch(epoch); + let fork_digest = spec.compute_fork_digest(genesis_validators_root, epoch); + (epoch, HardFork::new(fork_name, fork_digest, epoch)) + }) + .collect(); + + let current_epoch = current_slot.epoch(E::slots_per_epoch()); + let current_fork = epoch_to_forks + .values() + .rfind(|&fork| fork.fork_epoch <= current_epoch) + .cloned() + .expect("should match at least genesis epoch"); + + Self { + current_fork: RwLock::new(current_fork), + epoch_to_forks, + spec: spec.clone(), + } + } + + /// Returns `true` if the provided `fork_name` exists in the `ForkContext` object. + pub fn fork_exists(&self, fork_name: ForkName) -> bool { + self.spec.fork_epoch(fork_name).is_some() + } + + /// Returns the current fork name. + pub fn current_fork_name(&self) -> ForkName { + self.current_fork.read().fork_name + } + + /// Returns the current fork epoch. + pub fn current_fork_epoch(&self) -> Epoch { + self.current_fork.read().fork_epoch + } + + /// Returns the current fork digest. + pub fn current_fork_digest(&self) -> [u8; 4] { + self.current_fork.read().fork_digest + } + + /// Returns the next fork digest. If there's no future fork, returns the current fork digest. + pub fn next_fork_digest(&self) -> Option<[u8; 4]> { + let current_fork_epoch = self.current_fork_epoch(); + self.epoch_to_forks + .range(current_fork_epoch..) + .nth(1) + .map(|(_, fork)| fork.fork_digest) + } + + /// Updates the `digest_epoch` field to a new digest epoch. + pub fn update_current_fork( + &self, + new_fork_name: ForkName, + new_fork_digest: [u8; 4], + new_fork_epoch: Epoch, + ) { + debug_assert!(self.epoch_to_forks.contains_key(&new_fork_epoch)); + *self.current_fork.write() = HardFork::new(new_fork_name, new_fork_digest, new_fork_epoch); + } + + /// Returns the context bytes/fork_digest corresponding to the genesis fork version. + pub fn genesis_context_bytes(&self) -> [u8; 4] { + self.epoch_to_forks + .first_key_value() + .expect("must contain genesis epoch") + .1 + .fork_digest + } + + /// Returns the fork type given the context bytes/fork_digest. + /// Returns `None` if context bytes doesn't correspond to any valid `ForkName`. + pub fn get_fork_from_context_bytes(&self, context: [u8; 4]) -> Option<&ForkName> { + self.epoch_to_forks + .values() + .find(|fork| fork.fork_digest == context) + .map(|fork| &fork.fork_name) + } + + /// Returns the context bytes/fork_digest corresponding to an epoch. + /// See [`ChainSpec::compute_fork_digest`] + pub fn context_bytes(&self, epoch: Epoch) -> [u8; 4] { + self.epoch_to_forks + .range(..=epoch) + .next_back() + .expect("should match at least genesis epoch") + .1 + .fork_digest + } + + /// Returns all `fork_digest`s that are currently in the `ForkContext` object. + pub fn all_fork_digests(&self) -> Vec<[u8; 4]> { + self.epoch_to_forks + .values() + .map(|fork| fork.fork_digest) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::{BlobParameters, BlobSchedule, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_chain_spec() -> ChainSpec { + let blob_parameters = vec![ + BlobParameters { + epoch: Epoch::new(6), + max_blobs_per_block: 12, + }, + BlobParameters { + epoch: Epoch::new(50), + max_blobs_per_block: 24, + }, + BlobParameters { + epoch: Epoch::new(100), + max_blobs_per_block: 48, + }, + ]; + + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(1)); + spec.bellatrix_fork_epoch = Some(Epoch::new(2)); + spec.capella_fork_epoch = Some(Epoch::new(3)); + spec.deneb_fork_epoch = Some(Epoch::new(4)); + spec.electra_fork_epoch = Some(Epoch::new(5)); + spec.fulu_fork_epoch = Some(Epoch::new(6)); + spec.gloas_fork_epoch = Some(Epoch::new(7)); + spec.blob_schedule = BlobSchedule::new(blob_parameters); + spec + } + + #[test] + fn test_fork_exists() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + let current_slot = Slot::new(7); + + let context = ForkContext::new::(current_slot, genesis_root, &spec); + + assert!(context.fork_exists(ForkName::Electra)); + assert!(context.fork_exists(ForkName::Fulu)); + assert!(context.fork_exists(ForkName::Gloas)); + } + + #[test] + fn test_current_fork_name_and_epoch() { + let spec = make_chain_spec(); + let electra_epoch = spec.electra_fork_epoch.unwrap(); + let electra_slot = electra_epoch.end_slot(E::slots_per_epoch()); + let genesis_root = Hash256::ZERO; + + let context = ForkContext::new::(electra_slot, genesis_root, &spec); + + assert_eq!(context.current_fork_name(), ForkName::Electra); + assert_eq!(context.current_fork_epoch(), electra_epoch); + } + + #[test] + fn test_next_fork_digest() { + let spec = make_chain_spec(); + let electra_epoch = spec.electra_fork_epoch.unwrap(); + let electra_slot = electra_epoch.end_slot(E::slots_per_epoch()); + let genesis_root = Hash256::ZERO; + + let context = ForkContext::new::(electra_slot, genesis_root, &spec); + + let next_digest = context.next_fork_digest().unwrap(); + let expected_digest = spec.compute_fork_digest(genesis_root, spec.fulu_fork_epoch.unwrap()); + assert_eq!(next_digest, expected_digest); + } + + #[test] + fn test_get_fork_from_context_bytes() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + let current_slot = Slot::new(0); + + let context = ForkContext::new::(current_slot, genesis_root, &spec); + + let electra_digest = spec.compute_fork_digest(genesis_root, Epoch::new(5)); + assert_eq!( + context.get_fork_from_context_bytes(electra_digest), + Some(&ForkName::Electra) + ); + + let invalid_digest = [9, 9, 9, 9]; + assert!( + context + .get_fork_from_context_bytes(invalid_digest) + .is_none() + ); + } + + #[test] + fn test_context_bytes() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + let current_slot = Slot::new(0); + + let context = ForkContext::new::(current_slot, genesis_root, &spec); + + assert_eq!( + context.context_bytes(Epoch::new(0)), + spec.compute_fork_digest(genesis_root, Epoch::new(0)) + ); + + assert_eq!( + context.context_bytes(Epoch::new(12)), + spec.compute_fork_digest(genesis_root, Epoch::new(10)) + ); + } + + #[test] + fn test_all_fork_digests() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + let current_slot = Slot::new(20); + + let context = ForkContext::new::(current_slot, genesis_root, &spec); + + // Get all enabled fork digests + let fork_digests = context.all_fork_digests(); + let expected_digest_count = spec.all_digest_epochs().count(); + + assert_eq!(fork_digests.len(), expected_digest_count); + } +} diff --git a/consensus/types/src/fork_data.rs b/consensus/types/src/fork/fork_data.rs similarity index 63% rename from consensus/types/src/fork_data.rs rename to consensus/types/src/fork/fork_data.rs index 1ac91084d2..1b9c8bad9f 100644 --- a/consensus/types/src/fork_data.rs +++ b/consensus/types/src/fork/fork_data.rs @@ -1,27 +1,21 @@ -use crate::test_utils::TestRandom; -use crate::{ForkName, Hash256, SignedRoot}; -use context_deserialize_derive::context_deserialize; - +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{Hash256, SignedRoot}, + fork::ForkName, + test_utils::TestRandom, +}; + /// Specifies a fork of the `BeaconChain`, to prevent replay attacks. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Default, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, Clone, PartialEq, Default, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct ForkData { diff --git a/consensus/types/src/fork/fork_macros.rs b/consensus/types/src/fork/fork_macros.rs new file mode 100644 index 0000000000..2adc1fb88b --- /dev/null +++ b/consensus/types/src/fork/fork_macros.rs @@ -0,0 +1,64 @@ +/// Map a fork name into a fork-versioned superstruct type like `BeaconBlock`. +/// +/// The `$body` expression is where the magic happens. The macro allows us to achieve polymorphism +/// in the return type, which is not usually possible in Rust without trait objects. +/// +/// E.g. you could call `map_fork_name!(fork, BeaconBlock, serde_json::from_str(s))` to decode +/// different `BeaconBlock` variants depending on the value of `fork`. Note how the type of the body +/// will change between `BeaconBlockBase` and `BeaconBlockAltair` depending on which branch is +/// taken, the important thing is that they are re-unified by injecting them back into the +/// `BeaconBlock` parent enum. +/// +/// If you would also like to extract additional data alongside the superstruct type, use +/// the more flexible `map_fork_name_with` macro. +#[macro_export] +macro_rules! map_fork_name { + ($fork_name:expr, $t:tt, $body:expr) => { + $crate::map_fork_name_with!($fork_name, $t, { ($body, ()) }).0 + }; +} + +/// Map a fork name into a tuple of `(t, extra)` where `t` is a superstruct type. +#[macro_export] +macro_rules! map_fork_name_with { + ($fork_name:expr, $t:tt, $body:block) => { + match $fork_name { + $crate::fork::ForkName::Base => { + let (value, extra_data) = $body; + ($t::Base(value), extra_data) + } + $crate::fork::ForkName::Altair => { + let (value, extra_data) = $body; + ($t::Altair(value), extra_data) + } + $crate::fork::ForkName::Bellatrix => { + let (value, extra_data) = $body; + ($t::Bellatrix(value), extra_data) + } + $crate::fork::ForkName::Capella => { + let (value, extra_data) = $body; + ($t::Capella(value), extra_data) + } + $crate::fork::ForkName::Deneb => { + let (value, extra_data) = $body; + ($t::Deneb(value), extra_data) + } + $crate::fork::ForkName::Electra => { + let (value, extra_data) = $body; + ($t::Electra(value), extra_data) + } + $crate::fork::ForkName::Fulu => { + let (value, extra_data) = $body; + ($t::Fulu(value), extra_data) + } + $crate::fork::ForkName::Eip7805 => { + let (value, extra_data) = $body; + ($t::Eip7805(value), extra_data) + } + $crate::fork::ForkName::Gloas => { + let (value, extra_data) = $body; + ($t::Gloas(value), extra_data) + } + } + }; +} diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork/fork_name.rs similarity index 69% rename from consensus/types/src/fork_name.rs rename to consensus/types/src/fork/fork_name.rs index 13e95a46f4..ce78115811 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork/fork_name.rs @@ -1,8 +1,12 @@ -use crate::{ChainSpec, Epoch}; +use std::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; + use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use std::fmt::{self, Display, Formatter}; -use std::str::FromStr; + +use crate::core::{ChainSpec, Epoch}; #[derive( Debug, Clone, Copy, Decode, Encode, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, @@ -17,8 +21,9 @@ pub enum ForkName { Capella, Deneb, Electra, - Eip7805, Fulu, + Eip7805, + Gloas, } impl ForkName { @@ -30,16 +35,15 @@ impl ForkName { ForkName::Capella, ForkName::Deneb, ForkName::Electra, - ForkName::Eip7805, ForkName::Fulu, + ForkName::Eip7805, + ForkName::Gloas, ] } pub fn list_all_fork_epochs(spec: &ChainSpec) -> Vec<(ForkName, Option)> { ForkName::list_all() .into_iter() - // Skip Base - .skip(1) .map(|fork| (fork, spec.fork_epoch(fork))) .collect() } @@ -53,7 +57,7 @@ impl ForkName { /// This fork serves as the baseline for many tests, and the goal /// is to ensure features are passing on this fork. pub fn latest_stable() -> ForkName { - ForkName::Electra + ForkName::Fulu } /// Set the activation slots in the given `ChainSpec` so that the fork named by `self` @@ -67,8 +71,9 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; - spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; spec } ForkName::Altair => { @@ -77,8 +82,9 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; - spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; spec } ForkName::Bellatrix => { @@ -87,8 +93,9 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; - spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; spec } ForkName::Capella => { @@ -97,8 +104,9 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; - spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; spec } ForkName::Deneb => { @@ -107,8 +115,9 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = None; - spec.eip7805_fork_epoch = None; spec.fulu_fork_epoch = None; + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; spec } ForkName::Electra => { @@ -117,18 +126,8 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = None; spec.eip7805_fork_epoch = None; - spec.fulu_fork_epoch = None; - spec - } - ForkName::Eip7805 => { - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - spec.capella_fork_epoch = Some(Epoch::new(0)); - spec.deneb_fork_epoch = Some(Epoch::new(0)); - spec.electra_fork_epoch = Some(Epoch::new(0)); - spec.eip7805_fork_epoch = Some(Epoch::new(0)); - spec.fulu_fork_epoch = None; spec } ForkName::Fulu => { @@ -137,8 +136,30 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); - spec.eip7805_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.eip7805_fork_epoch = None; + spec.gloas_fork_epoch = None; + spec + } + ForkName::Eip7805 => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = None; + spec + } + ForkName::Gloas => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.eip7805_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); spec } } @@ -146,7 +167,7 @@ impl ForkName { /// Return the name of the fork immediately prior to the current one. /// - /// If `self` is `ForkName::Base` then `Base` is returned. + /// If `self` is `ForkName::Base` then `None` is returned. pub fn previous_fork(self) -> Option { match self { ForkName::Base => None, @@ -155,8 +176,9 @@ impl ForkName { ForkName::Capella => Some(ForkName::Bellatrix), ForkName::Deneb => Some(ForkName::Capella), ForkName::Electra => Some(ForkName::Deneb), - ForkName::Eip7805 => Some(ForkName::Electra), - ForkName::Fulu => Some(ForkName::Eip7805), + ForkName::Fulu => Some(ForkName::Electra), + ForkName::Eip7805 => Some(ForkName::Fulu), + ForkName::Gloas => Some(ForkName::Eip7805), } } @@ -170,9 +192,10 @@ impl ForkName { ForkName::Bellatrix => Some(ForkName::Capella), ForkName::Capella => Some(ForkName::Deneb), ForkName::Deneb => Some(ForkName::Electra), - ForkName::Electra => Some(ForkName::Eip7805), - ForkName::Eip7805 => Some(ForkName::Fulu), - ForkName::Fulu => None, + ForkName::Electra => Some(ForkName::Fulu), + ForkName::Fulu => Some(ForkName::Eip7805), + ForkName::Eip7805 => Some(ForkName::Gloas), + ForkName::Gloas => None, } } @@ -196,74 +219,57 @@ impl ForkName { self >= ForkName::Electra } + pub fn fulu_enabled(self) -> bool { + self >= ForkName::Fulu + } + pub fn eip7805_enabled(self) -> bool { self >= ForkName::Eip7805 } - pub fn fulu_enabled(self) -> bool { - self >= ForkName::Fulu + pub fn gloas_enabled(self) -> bool { + self >= ForkName::Gloas } -} -/// Map a fork name into a fork-versioned superstruct type like `BeaconBlock`. -/// -/// The `$body` expression is where the magic happens. The macro allows us to achieve polymorphism -/// in the return type, which is not usually possible in Rust without trait objects. -/// -/// E.g. you could call `map_fork_name!(fork, BeaconBlock, serde_json::from_str(s))` to decode -/// different `BeaconBlock` variants depending on the value of `fork`. Note how the type of the body -/// will change between `BeaconBlockBase` and `BeaconBlockAltair` depending on which branch is -/// taken, the important thing is that they are re-unified by injecting them back into the -/// `BeaconBlock` parent enum. -/// -/// If you would also like to extract additional data alongside the superstruct type, use -/// the more flexible `map_fork_name_with` macro. -#[macro_export] -macro_rules! map_fork_name { - ($fork_name:expr, $t:tt, $body:expr) => { - map_fork_name_with!($fork_name, $t, { ($body, ()) }).0 - }; -} - -/// Map a fork name into a tuple of `(t, extra)` where `t` is a superstruct type. -#[macro_export] -macro_rules! map_fork_name_with { - ($fork_name:expr, $t:tt, $body:block) => { - match $fork_name { - ForkName::Base => { - let (value, extra_data) = $body; - ($t::Base(value), extra_data) - } - ForkName::Altair => { - let (value, extra_data) = $body; - ($t::Altair(value), extra_data) - } - ForkName::Bellatrix => { - let (value, extra_data) = $body; - ($t::Bellatrix(value), extra_data) - } - ForkName::Capella => { - let (value, extra_data) = $body; - ($t::Capella(value), extra_data) - } - ForkName::Deneb => { - let (value, extra_data) = $body; - ($t::Deneb(value), extra_data) - } - ForkName::Electra => { - let (value, extra_data) = $body; - ($t::Electra(value), extra_data) - } - ForkName::Eip7805 => { - let (value, extra_data) = $body; - ($t::Eip7805(value), extra_data) - } - ForkName::Fulu => { - let (value, extra_data) = $body; - ($t::Fulu(value), extra_data) - } + pub fn fork_ascii(self) { + if self == ForkName::Fulu { + println!( + r#" + ╔═══════════════════════════════════════╗ + ║ ║ + ║ TO FULU, MOAR BLOBS TO ETHEREUM ║ + ║ ║ + ║ III DECEMBER MMXXV ║ + ║ ║ + ╚═══════════════════════════════════════╝ + + ============================================================================= + |||| |||| + |---------------------------------------------------------------------------| + |___-----___-----___-----___-----___-----___-----___-----___-----___-----___| + / _ \===/ _ \ / _ \===/ _ \ / _ \===/ _ \ / _ \===/ _ \ + ( (.\ oOo /.) ) ( (.\ oOo /.) ) ( (.\ oOo /.) ) ( (.\ oOo /.) ) + \__/=====\__/ \__/=====\__/ \__/=====\__/ \__/=====\__/ + ||||||| ||||||| ||||||| ||||||| + ||||||| ||||||| \\/), ||||||| ||||||| + ||||||| ||||||| ,'.' /, ||||||| ||||||| + ||||||| ||||||| (_)- / /, ||||||| ||||||| + ||||||| ||||||| /\_/ |__..--, * ||||||| ||||||| + ||||||| ||||||| (\___/\ \ \ / ).' ||||||| ||||||| + ||||||| ||||||| \____/ / (_ // ||||||| ||||||| + ||||||| ||||||| \\_ ,'--'\_( ||||||| ||||||| + (oOoOo) (oOoOo) )_)_/ )_/ )_) (oOoOo) (oOoOo) + J%%%%%L J%%%%%L (_(_.'(_.'(_.' J%%%%%L J%%%%%L + ZZZZZZZZZ ZZZZZZZZZ ZZZZZZZZZ ZZZZZZZZZ + =========================================================================== + |_________________________________________________________________________| + |___________________________________________________________________________| + |_____________________________________________________________________________| + |_______________________________________________________________________________| + "# + ); } - }; + } } impl FromStr for ForkName { @@ -279,6 +285,7 @@ impl FromStr for ForkName { "electra" => ForkName::Electra, "eip7805" => ForkName::Eip7805, "fulu" => ForkName::Fulu, + "gloas" => ForkName::Gloas, _ => return Err(format!("unknown fork name: {}", fork_name)), }) } @@ -295,6 +302,7 @@ impl Display for ForkName { ForkName::Electra => "electra".fmt(f), ForkName::Eip7805 => "eip7805".fmt(f), ForkName::Fulu => "fulu".fmt(f), + ForkName::Gloas => "gloas".fmt(f), } } } diff --git a/consensus/types/src/fork/fork_version_decode.rs b/consensus/types/src/fork/fork_version_decode.rs new file mode 100644 index 0000000000..4349efb21f --- /dev/null +++ b/consensus/types/src/fork/fork_version_decode.rs @@ -0,0 +1,6 @@ +use crate::fork::ForkName; + +pub trait ForkVersionDecode: Sized { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; +} diff --git a/consensus/types/src/fork/mod.rs b/consensus/types/src/fork/mod.rs new file mode 100644 index 0000000000..1ad1c7cb62 --- /dev/null +++ b/consensus/types/src/fork/mod.rs @@ -0,0 +1,15 @@ +mod fork; +mod fork_context; +mod fork_data; +mod fork_macros; +mod fork_name; +mod fork_version_decode; + +pub use crate::{map_fork_name, map_fork_name_with}; +pub use fork::Fork; +pub use fork_context::{ForkContext, HardFork}; +pub use fork_data::ForkData; +pub use fork_name::{ForkName, InconsistentFork}; +pub use fork_version_decode::ForkVersionDecode; + +pub type ForkVersion = [u8; 4]; diff --git a/consensus/types/src/fork_context.rs b/consensus/types/src/fork_context.rs deleted file mode 100644 index a6360705ba..0000000000 --- a/consensus/types/src/fork_context.rs +++ /dev/null @@ -1,95 +0,0 @@ -use parking_lot::RwLock; - -use crate::{ChainSpec, EthSpec, ForkName, Hash256, Slot}; -use std::collections::HashMap; - -/// Provides fork specific info like the current fork name and the fork digests corresponding to every valid fork. -#[derive(Debug)] -pub struct ForkContext { - current_fork: RwLock, - fork_to_digest: HashMap, - digest_to_fork: HashMap<[u8; 4], ForkName>, - pub spec: ChainSpec, -} - -impl ForkContext { - /// Creates a new `ForkContext` object by enumerating all enabled forks and computing their - /// fork digest. - /// - /// A fork is disabled in the `ChainSpec` if the activation slot corresponding to that fork is `None`. - pub fn new( - current_slot: Slot, - genesis_validators_root: Hash256, - spec: &ChainSpec, - ) -> Self { - let fork_to_digest: HashMap = ForkName::list_all() - .into_iter() - .filter_map(|fork| { - if spec.fork_epoch(fork).is_some() { - Some(( - fork, - ChainSpec::compute_fork_digest( - spec.fork_version_for_name(fork), - genesis_validators_root, - ), - )) - } else { - None - } - }) - .collect(); - - let digest_to_fork = fork_to_digest - .clone() - .into_iter() - .map(|(k, v)| (v, k)) - .collect(); - - Self { - current_fork: RwLock::new(spec.fork_name_at_slot::(current_slot)), - fork_to_digest, - digest_to_fork, - spec: spec.clone(), - } - } - - /// Returns `true` if the provided `fork_name` exists in the `ForkContext` object. - pub fn fork_exists(&self, fork_name: ForkName) -> bool { - self.fork_to_digest.contains_key(&fork_name) - } - - /// Returns the `current_fork`. - pub fn current_fork(&self) -> ForkName { - *self.current_fork.read() - } - - /// Updates the `current_fork` field to a new fork. - pub fn update_current_fork(&self, new_fork: ForkName) { - *self.current_fork.write() = new_fork; - } - - /// Returns the context bytes/fork_digest corresponding to the genesis fork version. - pub fn genesis_context_bytes(&self) -> [u8; 4] { - *self - .fork_to_digest - .get(&ForkName::Base) - .expect("ForkContext must contain genesis context bytes") - } - - /// Returns the fork type given the context bytes/fork_digest. - /// Returns `None` if context bytes doesn't correspond to any valid `ForkName`. - pub fn from_context_bytes(&self, context: [u8; 4]) -> Option<&ForkName> { - self.digest_to_fork.get(&context) - } - - /// Returns the context bytes/fork_digest corresponding to a fork name. - /// Returns `None` if the `ForkName` has not been initialized. - pub fn to_context_bytes(&self, fork_name: ForkName) -> Option<[u8; 4]> { - self.fork_to_digest.get(&fork_name).cloned() - } - - /// Returns all `fork_digest`s that are currently in the `ForkContext` object. - pub fn all_fork_digests(&self) -> Vec<[u8; 4]> { - self.digest_to_fork.keys().cloned().collect() - } -} diff --git a/consensus/types/src/inclusion_list.rs b/consensus/types/src/inclusion_list.rs deleted file mode 100644 index f9fd9cca89..0000000000 --- a/consensus/types/src/inclusion_list.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::test_utils::TestRandom; -use crate::{EthSpec, Hash256, Signature, SignedRoot, Slot, Transactions}; - -use derivative::Derivative; -use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use test_random_derive::TestRandom; -use tree_hash_derive::TreeHash; - -#[derive( - Debug, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - Derivative, - arbitrary::Arbitrary, -)] -#[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] -pub struct InclusionList { - pub slot: Slot, - #[serde(with = "serde_utils::quoted_u64")] - pub validator_index: u64, - pub inclusion_list_committee_root: Hash256, - #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] - pub transactions: Transactions, -} - -impl SignedRoot for InclusionList {} - -#[derive( - Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Derivative, arbitrary::Arbitrary, -)] -#[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] -pub struct SignedInclusionList { - pub message: InclusionList, - pub signature: Signature, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::*; - - ssz_and_tree_hash_tests!(InclusionList); -} diff --git a/consensus/types/src/inclusion_list_committee.rs b/consensus/types/src/inclusion_list_committee.rs deleted file mode 100644 index 2aa928e02e..0000000000 --- a/consensus/types/src/inclusion_list_committee.rs +++ /dev/null @@ -1,3 +0,0 @@ -use crate::*; - -pub type InclusionListCommittee = FixedVector::InclusionListCommitteeSize>; diff --git a/consensus/types/src/inclusion_list_duty.rs b/consensus/types/src/inclusion_list_duty.rs deleted file mode 100644 index 9f7969f917..0000000000 --- a/consensus/types/src/inclusion_list_duty.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::*; -use serde::{Deserialize, Serialize}; - -#[derive(arbitrary::Arbitrary, Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] -pub struct InclusionListDuty { - /// The slot during which the validator must produce an inclusion list. - pub slot: Slot, - #[serde(with = "serde_utils::quoted_u64")] - /// The index of the validator. - pub validator_index: u64, - /// The hash tree root of the inclusion list committee. - pub committee_root: Hash256, - /// The pubkey of the validator. - pub pubkey: PublicKeyBytes, -} diff --git a/consensus/types/src/kzg_ext/consts.rs b/consensus/types/src/kzg_ext/consts.rs new file mode 100644 index 0000000000..06c9f9c749 --- /dev/null +++ b/consensus/types/src/kzg_ext/consts.rs @@ -0,0 +1,3 @@ +pub use kzg::{ + BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_FIELD_ELEMENT, VERSIONED_HASH_VERSION_KZG, +}; diff --git a/consensus/types/src/kzg_ext/mod.rs b/consensus/types/src/kzg_ext/mod.rs new file mode 100644 index 0000000000..63533ec71f --- /dev/null +++ b/consensus/types/src/kzg_ext/mod.rs @@ -0,0 +1,27 @@ +pub mod consts; + +pub use kzg::{Blob as KzgBlob, Error as KzgError, Kzg, KzgCommitment, KzgProof}; + +use ssz_types::VariableList; + +use crate::core::EthSpec; + +// Note on List limit: +// - Deneb to Electra: `MaxBlobCommitmentsPerBlock` +// - Fulu: `MaxCellsPerBlock` +// We choose to use a single type (with the larger value from Fulu as `N`) instead of having to +// introduce a new type for Fulu. This is to avoid messy conversions and having to add extra types +// with no gains - as `N` does not impact serialisation at all, and only affects merkleization, +// which we don't current do on `KzgProofs` anyway. +pub type KzgProofs = VariableList::MaxCellsPerBlock>; + +pub type KzgCommitments = + VariableList::MaxBlobCommitmentsPerBlock>; + +/// Util method helpful for logging. +pub fn format_kzg_commitments(commitments: &[KzgCommitment]) -> String { + let commitment_strings: Vec = commitments.iter().map(|x| x.to_string()).collect(); + let commitments_joined = commitment_strings.join(", "); + let surrounded_commitments = format!("[{}]", commitments_joined); + surrounded_commitments +} diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 33586da28a..5a89fcb1d4 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -1,4 +1,4 @@ -//! Ethereum 2.0 types +//! Ethereum Consensus types // Clippy lint set up #![cfg_attr( not(test), @@ -12,292 +12,166 @@ #[macro_use] pub mod test_utils; -pub mod aggregate_and_proof; -pub mod application_domain; pub mod attestation; -pub mod attestation_data; -pub mod attestation_duty; -pub mod attester_slashing; -pub mod beacon_block; -pub mod beacon_block_body; -pub mod beacon_block_header; -pub mod beacon_committee; -pub mod beacon_response; -pub mod beacon_state; -pub mod bls_to_execution_change; -pub mod builder_bid; -pub mod chain_spec; -pub mod checkpoint; -pub mod consolidation_request; -pub mod consts; -pub mod contribution_and_proof; +pub mod block; +pub mod builder; +pub mod consolidation; +pub mod core; +pub mod data; pub mod deposit; -pub mod deposit_data; -pub mod deposit_message; -pub mod deposit_request; -pub mod deposit_tree_snapshot; -pub mod enr_fork_id; -pub mod eth1_data; -pub mod eth_spec; -pub mod execution_block_hash; -pub mod execution_payload; -pub mod execution_payload_header; +pub mod execution; +pub mod exit; pub mod fork; -pub mod fork_data; -pub mod fork_name; -pub mod graffiti; -pub mod historical_batch; -pub mod historical_summary; -pub mod inclusion_list; -pub mod inclusion_list_committee; -pub mod inclusion_list_duty; -pub mod indexed_attestation; -pub mod light_client_bootstrap; -pub mod light_client_finality_update; -pub mod light_client_optimistic_update; -pub mod light_client_update; -pub mod pending_attestation; -pub mod pending_consolidation; -pub mod pending_deposit; -pub mod pending_partial_withdrawal; -pub mod proposer_preparation_data; -pub mod proposer_slashing; -pub mod relative_epoch; -pub mod selection_proof; -pub mod shuffling_id; -pub mod signed_aggregate_and_proof; -pub mod signed_beacon_block; -pub mod signed_beacon_block_header; -pub mod signed_bls_to_execution_change; -pub mod signed_contribution_and_proof; -pub mod signed_voluntary_exit; -pub mod signing_data; -pub mod sync_committee_subscription; -pub mod sync_duty; -pub mod validator; -pub mod validator_subscription; -pub mod voluntary_exit; -pub mod withdrawal_credentials; -pub mod withdrawal_request; -#[macro_use] -pub mod slot_epoch_macros; -pub mod activation_queue; -pub mod config_and_preset; -pub mod execution_block_header; -pub mod execution_requests; -pub mod fork_context; -pub mod participation_flags; -pub mod payload; -pub mod preset; -pub mod slot_epoch; -pub mod subnet_id; -pub mod sync_aggregate; -pub mod sync_aggregator_selection_data; +pub mod kzg_ext; +pub mod light_client; +pub mod slashing; +pub mod state; pub mod sync_committee; -pub mod sync_committee_contribution; -pub mod sync_committee_message; -pub mod sync_selection_proof; -pub mod sync_subnet_id; -pub mod validator_registration_data; +pub mod validator; pub mod withdrawal; -pub mod epoch_cache; -pub mod slot_data; -#[cfg(feature = "sqlite")] -pub mod sqlite; +// Temporary root level exports to maintain backwards compatibility for Lighthouse. +pub use attestation::*; +pub use block::*; +pub use builder::*; +pub use consolidation::*; +pub use core::{consts, *}; +pub use data::*; +pub use deposit::*; +pub use execution::*; +pub use exit::*; +pub use fork::*; +pub use kzg_ext::*; +pub use light_client::*; +pub use slashing::*; +pub use state::*; +pub use sync_committee::*; +pub use validator::*; +pub use withdrawal::*; -pub mod blob_sidecar; -pub mod data_column_custody_group; -pub mod data_column_sidecar; -pub mod data_column_subnet_id; -pub mod light_client_header; -pub mod non_zero_usize; -pub mod runtime_fixed_vector; -pub mod runtime_var_list; +// Temporary facade modules to maintain backwards compatibility for Lighthouse. +pub mod eth_spec { + pub use crate::core::EthSpec; +} -pub use crate::activation_queue::ActivationQueue; -pub use crate::aggregate_and_proof::{ - AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, -}; -pub use crate::attestation::{ - Attestation, AttestationBase, AttestationElectra, AttestationRef, AttestationRefMut, - Error as AttestationError, SingleAttestation, -}; -pub use crate::attestation_data::AttestationData; -pub use crate::attestation_duty::AttestationDuty; -pub use crate::attester_slashing::{ - AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingOnDisk, - AttesterSlashingRef, AttesterSlashingRefOnDisk, -}; -pub use crate::beacon_block::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockCapella, - BeaconBlockDeneb, BeaconBlockEip7805, BeaconBlockElectra, BeaconBlockFulu, BeaconBlockRef, - BeaconBlockRefMut, BlindedBeaconBlock, BlockImportSource, EmptyBlock, -}; -pub use crate::beacon_block_body::{ - BeaconBlockBody, BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, - BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyEip7805, BeaconBlockBodyElectra, - BeaconBlockBodyFulu, BeaconBlockBodyRef, BeaconBlockBodyRefMut, -}; -pub use crate::beacon_block_header::BeaconBlockHeader; -pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; -pub use crate::beacon_response::{ - BeaconResponse, ForkVersionDecode, ForkVersionedResponse, UnversionedResponse, -}; -pub use crate::beacon_state::{Error as BeaconStateError, *}; -pub use crate::blob_sidecar::{BlobIdentifier, BlobSidecar, BlobSidecarList, BlobsList}; -pub use crate::bls_to_execution_change::BlsToExecutionChange; -pub use crate::chain_spec::{ChainSpec, Config, Domain}; -pub use crate::checkpoint::Checkpoint; -pub use crate::config_and_preset::{ - ConfigAndPreset, ConfigAndPresetDeneb, ConfigAndPresetElectra, ConfigAndPresetFulu, -}; -pub use crate::consolidation_request::ConsolidationRequest; -pub use crate::contribution_and_proof::ContributionAndProof; -pub use crate::data_column_sidecar::{ - ColumnIndex, DataColumnSidecar, DataColumnSidecarList, DataColumnsByRootIdentifier, -}; -pub use crate::data_column_subnet_id::DataColumnSubnetId; -pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; -pub use crate::deposit_data::DepositData; -pub use crate::deposit_message::DepositMessage; -pub use crate::deposit_request::DepositRequest; -pub use crate::deposit_tree_snapshot::{DepositTreeSnapshot, FinalizedExecutionBlock}; -pub use crate::enr_fork_id::EnrForkId; -pub use crate::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; -pub use crate::eth1_data::Eth1Data; -pub use crate::eth_spec::EthSpecId; -pub use crate::execution_block_hash::ExecutionBlockHash; -pub use crate::execution_block_header::{EncodableExecutionBlockHeader, ExecutionBlockHeader}; -pub use crate::execution_payload::{ - ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadRef, - Transaction, Transactions, Withdrawals, -}; -pub use crate::execution_payload_header::{ - ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, ExecutionPayloadHeaderElectra, - ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, -}; -pub use crate::execution_requests::{ExecutionRequests, RequestType}; -pub use crate::fork::Fork; -pub use crate::fork_context::ForkContext; -pub use crate::fork_data::ForkData; -pub use crate::fork_name::{ForkName, InconsistentFork}; -pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; -pub use crate::historical_batch::HistoricalBatch; -pub use crate::inclusion_list::{InclusionList, SignedInclusionList}; -pub use crate::inclusion_list_committee::InclusionListCommittee; -pub use crate::inclusion_list_duty::InclusionListDuty; -pub use crate::indexed_attestation::{ - IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, -}; -pub use crate::light_client_bootstrap::{ - LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, - LightClientBootstrapDeneb, LightClientBootstrapElectra, LightClientBootstrapFulu, -}; -pub use crate::light_client_finality_update::{ - LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, - LightClientFinalityUpdateDeneb, LightClientFinalityUpdateElectra, - LightClientFinalityUpdateFulu, -}; -pub use crate::light_client_header::{ - LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - LightClientHeaderElectra, LightClientHeaderFulu, -}; -pub use crate::light_client_optimistic_update::{ - LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, - LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, - LightClientOptimisticUpdateElectra, LightClientOptimisticUpdateFulu, -}; -pub use crate::light_client_update::{ - Error as LightClientUpdateError, LightClientUpdate, LightClientUpdateAltair, - LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, - LightClientUpdateFulu, MerkleProof, -}; -pub use crate::participation_flags::ParticipationFlags; -pub use crate::payload::{ - AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, - BlindedPayloadDeneb, BlindedPayloadEip7805, BlindedPayloadElectra, BlindedPayloadFulu, - BlindedPayloadRef, BlockType, ExecPayload, FullPayload, FullPayloadBellatrix, - FullPayloadCapella, FullPayloadDeneb, FullPayloadEip7805, FullPayloadElectra, FullPayloadFulu, - FullPayloadRef, OwnedExecPayload, -}; -pub use crate::pending_attestation::PendingAttestation; -pub use crate::pending_consolidation::PendingConsolidation; -pub use crate::pending_deposit::PendingDeposit; -pub use crate::pending_partial_withdrawal::PendingPartialWithdrawal; -pub use crate::preset::{ - AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, Eip7805Preset, - ElectraPreset, FuluPreset, -}; -pub use crate::proposer_preparation_data::ProposerPreparationData; -pub use crate::proposer_slashing::ProposerSlashing; -pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; -pub use crate::runtime_fixed_vector::RuntimeFixedVector; -pub use crate::runtime_var_list::RuntimeVariableList; -pub use crate::selection_proof::SelectionProof; -pub use crate::shuffling_id::AttestationShufflingId; -pub use crate::signed_aggregate_and_proof::{ - SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, -}; -pub use crate::signed_beacon_block::{ - ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc, SignedBeaconBlock, - SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, - SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockEip7805, - SignedBeaconBlockElectra, SignedBeaconBlockFulu, SignedBeaconBlockHash, - SignedBlindedBeaconBlock, -}; -pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader; -pub use crate::signed_bls_to_execution_change::SignedBlsToExecutionChange; -pub use crate::signed_contribution_and_proof::SignedContributionAndProof; -pub use crate::signed_voluntary_exit::SignedVoluntaryExit; -pub use crate::signing_data::{SignedRoot, SigningData}; -pub use crate::slot_epoch::{Epoch, Slot}; -pub use crate::subnet_id::SubnetId; -pub use crate::sync_aggregate::SyncAggregate; -pub use crate::sync_aggregator_selection_data::SyncAggregatorSelectionData; -pub use crate::sync_committee::SyncCommittee; -pub use crate::sync_committee_contribution::{SyncCommitteeContribution, SyncContributionData}; -pub use crate::sync_committee_message::SyncCommitteeMessage; -pub use crate::sync_committee_subscription::SyncCommitteeSubscription; -pub use crate::sync_duty::SyncDuty; -pub use crate::sync_selection_proof::SyncSelectionProof; -pub use crate::sync_subnet_id::SyncSubnetId; -pub use crate::validator::Validator; -pub use crate::validator_registration_data::*; -pub use crate::validator_subscription::ValidatorSubscription; -pub use crate::voluntary_exit::VoluntaryExit; -pub use crate::withdrawal::Withdrawal; -pub use crate::withdrawal_credentials::WithdrawalCredentials; -pub use crate::withdrawal_request::WithdrawalRequest; -pub use fixed_bytes::FixedBytesExtended; +pub mod chain_spec { + pub use crate::core::ChainSpec; +} -pub type CommitteeIndex = u64; -pub type Hash256 = fixed_bytes::Hash256; -pub type Uint256 = fixed_bytes::Uint256; -pub type Address = fixed_bytes::Address; -pub type ForkVersion = [u8; 4]; -pub type BLSFieldElement = Uint256; -pub type Blob = FixedVector::BytesPerBlob>; -// Note on List limit: -// - Deneb to Electra: `MaxBlobCommitmentsPerBlock` -// - Fulu: `MaxCellsPerBlock` -// We choose to use a single type (with the larger value from Fulu as `N`) instead of having to -// introduce a new type for Fulu. This is to avoid messy conversions and having to add extra types -// with no gains - as `N` does not impact serialisation at all, and only affects merkleization, -// which we don't current do on `KzgProofs` anyway. -pub type KzgProofs = VariableList::MaxCellsPerBlock>; -pub type VersionedHash = Hash256; -pub type Hash64 = alloy_primitives::B64; +pub mod beacon_block { + pub use crate::block::{BlindedBeaconBlock, BlockImportSource}; +} -pub use bls::{ - AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, - Signature, SignatureBytes, -}; -pub use context_deserialize::ContextDeserialize; -pub use context_deserialize_derive::context_deserialize; -pub use kzg::{KzgCommitment, KzgProof, VERSIONED_HASH_VERSION_KZG}; -pub use milhouse::{self, List, Vector}; -pub use ssz_types::{typenum, typenum::Unsigned, BitList, BitVector, FixedVector, VariableList}; -pub use superstruct::superstruct; +pub mod beacon_block_body { + pub use crate::kzg_ext::{KzgCommitments, format_kzg_commitments}; +} + +pub mod beacon_state { + pub use crate::state::{ + BeaconState, BeaconStateBase, CommitteeCache, compute_committee_index_in_epoch, + compute_committee_range_in_epoch, epoch_committee_count, + }; +} + +pub mod graffiti { + pub use crate::core::GraffitiString; +} + +pub mod indexed_attestation { + pub use crate::attestation::{IndexedAttestationBase, IndexedAttestationElectra}; +} + +pub mod historical_summary { + pub use crate::state::HistoricalSummary; +} + +pub mod participation_flags { + pub use crate::attestation::ParticipationFlags; +} + +pub mod epoch_cache { + pub use crate::state::{EpochCache, EpochCacheError, EpochCacheKey}; +} + +pub mod non_zero_usize { + pub use crate::core::new_non_zero_usize; +} + +pub mod data_column_sidecar { + pub use crate::data::{ + Cell, ColumnIndex, DataColumn, DataColumnSidecar, DataColumnSidecarError, + DataColumnSidecarList, + }; +} + +pub mod builder_bid { + pub use crate::builder::*; +} + +pub mod blob_sidecar { + pub use crate::data::{ + BlobIdentifier, BlobSidecar, BlobSidecarError, BlobsList, FixedBlobSidecarList, + }; +} + +pub mod payload { + pub use crate::execution::BlockProductionVersion; +} + +pub mod execution_requests { + pub use crate::execution::{ + ConsolidationRequests, DepositRequests, ExecutionRequests, RequestType, WithdrawalRequests, + }; +} + +pub mod execution_payload_envelope { + pub use crate::execution::{ExecutionPayloadEnvelope, SignedExecutionPayloadEnvelope}; +} + +pub mod data_column_custody_group { + pub use crate::data::{ + CustodyIndex, compute_columns_for_custody_group, compute_ordered_custody_column_indices, + compute_subnets_for_node, compute_subnets_from_custody_group, get_custody_groups, + }; +} + +pub mod sync_aggregate { + pub use crate::sync_committee::SyncAggregateError as Error; +} + +pub mod light_client_update { + pub use crate::light_client::consts::{ + CURRENT_SYNC_COMMITTEE_INDEX, CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA, FINALIZED_ROOT_INDEX, + FINALIZED_ROOT_INDEX_ELECTRA, MAX_REQUEST_LIGHT_CLIENT_UPDATES, NEXT_SYNC_COMMITTEE_INDEX, + NEXT_SYNC_COMMITTEE_INDEX_ELECTRA, + }; +} + +pub mod sync_committee_contribution { + pub use crate::sync_committee::{ + SyncCommitteeContributionError as Error, SyncContributionData, + }; +} + +pub mod slot_data { + pub use crate::core::SlotData; +} + +pub mod signed_aggregate_and_proof { + pub use crate::attestation::SignedAggregateAndProofRefMut; +} + +pub mod payload_attestation { + pub use crate::attestation::{ + PayloadAttestation, PayloadAttestationData, PayloadAttestationMessage, + }; +} + +pub mod application_domain { + pub use crate::core::ApplicationDomain; +} + +// Temporary re-exports to maintain backwards compatibility for Lighthouse. +pub use crate::kzg_ext::consts::VERSIONED_HASH_VERSION_KZG; +pub use crate::light_client::LightClientError as LightClientUpdateError; +pub use crate::state::BeaconStateError as Error; diff --git a/consensus/types/src/light_client/consts.rs b/consensus/types/src/light_client/consts.rs new file mode 100644 index 0000000000..0092e75e87 --- /dev/null +++ b/consensus/types/src/light_client/consts.rs @@ -0,0 +1,21 @@ +pub const FINALIZED_ROOT_PROOF_LEN: usize = 6; +pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; +pub const NEXT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; +pub const EXECUTION_PAYLOAD_PROOF_LEN: usize = 4; + +pub const FINALIZED_ROOT_PROOF_LEN_ELECTRA: usize = 7; +pub const NEXT_SYNC_COMMITTEE_PROOF_LEN_ELECTRA: usize = 6; +pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN_ELECTRA: usize = 6; + +pub const FINALIZED_ROOT_INDEX: usize = 105; +pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54; +pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55; +pub const EXECUTION_PAYLOAD_INDEX: usize = 25; + +pub const FINALIZED_ROOT_INDEX_ELECTRA: usize = 169; +pub const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA: usize = 86; +pub const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA: usize = 87; + +// Max light client updates by range request limits +// spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#configuration +pub const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u64 = 128; diff --git a/consensus/types/src/light_client/error.rs b/consensus/types/src/light_client/error.rs new file mode 100644 index 0000000000..4c7a30db5e --- /dev/null +++ b/consensus/types/src/light_client/error.rs @@ -0,0 +1,42 @@ +use safe_arith::ArithError; + +use crate::state::BeaconStateError; + +#[derive(Debug, PartialEq, Clone)] +pub enum LightClientError { + SszTypesError(ssz_types::Error), + MilhouseError(milhouse::Error), + BeaconStateError(BeaconStateError), + ArithError(ArithError), + AltairForkNotActive, + NotEnoughSyncCommitteeParticipants, + MismatchingPeriods, + InvalidFinalizedBlock, + BeaconBlockBodyError, + InconsistentFork, + GloasNotImplemented, +} + +impl From for LightClientError { + fn from(e: ssz_types::Error) -> LightClientError { + LightClientError::SszTypesError(e) + } +} + +impl From for LightClientError { + fn from(e: BeaconStateError) -> LightClientError { + LightClientError::BeaconStateError(e) + } +} + +impl From for LightClientError { + fn from(e: ArithError) -> LightClientError { + LightClientError::ArithError(e) + } +} + +impl From for LightClientError { + fn from(e: milhouse::Error) -> LightClientError { + LightClientError::MilhouseError(e) + } +} diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client/light_client_bootstrap.rs similarity index 76% rename from consensus/types/src/light_client_bootstrap.rs rename to consensus/types/src/light_client/light_client_bootstrap.rs index 0f62b3b32f..43cdc6a6bd 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client/light_client_bootstrap.rs @@ -1,19 +1,29 @@ -use crate::context_deserialize; -use crate::{ - light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, ContextDeserialize, - EthSpec, FixedVector, ForkName, Hash256, LightClientHeader, LightClientHeaderAltair, - LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, - LightClientHeaderFulu, SignedBlindedBeaconBlock, Slot, SyncCommittee, -}; -use derivative::Derivative; +use std::sync::Arc; + +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; -use std::sync::Arc; +use ssz_types::FixedVector; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + block::SignedBlindedBeaconBlock, + core::{ChainSpec, EthSpec, Hash256, Slot}, + fork::ForkName, + light_client::{ + CurrentSyncCommitteeProofLen, CurrentSyncCommitteeProofLenElectra, LightClientError, + LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, + }, + state::BeaconState, + sync_committee::SyncCommittee, + test_utils::TestRandom, +}; + /// A LightClientBootstrap is the initializer we send over to light_client nodes /// that are trying to generate their basic storage when booting up. #[superstruct( @@ -22,29 +32,34 @@ use tree_hash_derive::TreeHash; derive( Debug, Clone, - PartialEq, Serialize, Deserialize, - Derivative, + Educe, Decode, Encode, TestRandom, - arbitrary::Arbitrary, TreeHash, ), + educe(PartialEq), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ) )] -#[derive( - Debug, Clone, Serialize, TreeHash, Encode, Deserialize, arbitrary::Arbitrary, PartialEq, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, Serialize, TreeHash, Encode, Deserialize, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct LightClientBootstrap { /// The requested beacon block header. #[superstruct(only(Altair), partial_getter(rename = "header_altair"))] @@ -104,10 +119,11 @@ impl LightClientBootstrap { Self::Electra(LightClientBootstrapElectra::from_ssz_bytes(bytes)?) } ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu::from_ssz_bytes(bytes)?), - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientBootstrap decoding for {fork_name} not implemented" - ))) + ))); } }; @@ -127,6 +143,8 @@ impl LightClientBootstrap { as Encode>::ssz_fixed_len() } ForkName::Fulu => as Encode>::ssz_fixed_len(), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => as Encode>::ssz_fixed_len(), }; fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -136,37 +154,49 @@ impl LightClientBootstrap { current_sync_committee: Arc>, current_sync_committee_branch: Vec, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { let light_client_bootstrap = match block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), ForkName::Altair | ForkName::Bellatrix => Self::Altair(LightClientBootstrapAltair { header: LightClientHeaderAltair::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Capella => Self::Capella(LightClientBootstrapCapella { header: LightClientHeaderCapella::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb { header: LightClientHeaderDeneb::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Electra | ForkName::Eip7805 => Self::Electra(LightClientBootstrapElectra { header: LightClientHeaderElectra::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu { header: LightClientHeaderFulu::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), }; Ok(light_client_bootstrap) @@ -176,42 +206,52 @@ impl LightClientBootstrap { beacon_state: &mut BeaconState, block: &SignedBlindedBeaconBlock, chain_spec: &ChainSpec, - ) -> Result { - let mut header = beacon_state.latest_block_header().clone(); - header.state_root = beacon_state.update_tree_hash_cache()?; + ) -> Result { let current_sync_committee_branch = beacon_state.compute_current_sync_committee_proof()?; let current_sync_committee = beacon_state.current_sync_committee()?.clone(); let light_client_bootstrap = match block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), ForkName::Altair | ForkName::Bellatrix => Self::Altair(LightClientBootstrapAltair { header: LightClientHeaderAltair::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Capella => Self::Capella(LightClientBootstrapCapella { header: LightClientHeaderCapella::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb { header: LightClientHeaderDeneb::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Electra | ForkName::Eip7805 => Self::Electra(LightClientBootstrapElectra { header: LightClientHeaderElectra::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu { header: LightClientHeaderFulu::block_to_light_client_header(block)?, current_sync_committee, - current_sync_committee_branch: current_sync_committee_branch.into(), + current_sync_committee_branch: current_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, }), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), }; Ok(light_client_bootstrap) @@ -234,7 +274,7 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientBootstrap return Err(serde::de::Error::custom(format!( "LightClientBootstrap failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Altair | ForkName::Bellatrix => { Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -251,6 +291,13 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientBootstrap ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } + ForkName::Gloas => { + // TODO(EIP-7732): check if this is correct + return Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + context + ))); + } }) } } diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client/light_client_finality_update.rs similarity index 75% rename from consensus/types/src/light_client_finality_update.rs rename to consensus/types/src/light_client/light_client_finality_update.rs index 6ab5d3dafc..7b6bd6bc98 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client/light_client_finality_update.rs @@ -1,47 +1,61 @@ -use super::{EthSpec, FixedVector, Hash256, LightClientHeader, Slot, SyncAggregate}; -use crate::context_deserialize; -use crate::ChainSpec; -use crate::{ - light_client_update::*, test_utils::TestRandom, ContextDeserialize, ForkName, - LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - LightClientHeaderElectra, LightClientHeaderFulu, SignedBlindedBeaconBlock, -}; -use derivative::Derivative; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::Decode; use ssz_derive::Encode; +use ssz_types::FixedVector; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + block::SignedBlindedBeaconBlock, + core::{ChainSpec, EthSpec, Hash256, Slot}, + fork::ForkName, + light_client::{ + FinalizedRootProofLen, FinalizedRootProofLenElectra, LightClientError, LightClientHeader, + LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, LightClientHeaderFulu, + }, + sync_committee::SyncAggregate, + test_utils::TestRandom, +}; + #[superstruct( variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, Clone, - PartialEq, Serialize, Deserialize, - Derivative, + Educe, Decode, Encode, TestRandom, - arbitrary::Arbitrary, TreeHash, ), + educe(PartialEq), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ) )] -#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct LightClientFinalityUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] @@ -79,6 +93,7 @@ pub struct LightClientFinalityUpdate { /// current sync aggregate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated signature + #[superstruct(getter(copy))] pub signature_slot: Slot, } @@ -90,10 +105,10 @@ impl LightClientFinalityUpdate { sync_aggregate: SyncAggregate, signature_slot: Slot, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { let finality_update = match attested_block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { ForkName::Altair | ForkName::Bellatrix => { Self::Altair(LightClientFinalityUpdateAltair { @@ -103,7 +118,9 @@ impl LightClientFinalityUpdate { finalized_header: LightClientHeaderAltair::block_to_light_client_header( finalized_block, )?, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate, signature_slot, }) @@ -115,7 +132,9 @@ impl LightClientFinalityUpdate { finalized_header: LightClientHeaderCapella::block_to_light_client_header( finalized_block, )?, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate, signature_slot, }), @@ -126,23 +145,25 @@ impl LightClientFinalityUpdate { finalized_header: LightClientHeaderDeneb::block_to_light_client_header( finalized_block, )?, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, + sync_aggregate, + signature_slot, + }), + ForkName::Electra => Self::Electra(LightClientFinalityUpdateElectra { + attested_header: LightClientHeaderElectra::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderElectra::block_to_light_client_header( + finalized_block, + )?, + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate, signature_slot, }), - ForkName::Electra | ForkName::Eip7805 => { - Self::Electra(LightClientFinalityUpdateElectra { - attested_header: LightClientHeaderElectra::block_to_light_client_header( - attested_block, - )?, - finalized_header: LightClientHeaderElectra::block_to_light_client_header( - finalized_block, - )?, - finality_branch: finality_branch.into(), - sync_aggregate, - signature_slot, - }) - } ForkName::Fulu => Self::Fulu(LightClientFinalityUpdateFulu { attested_header: LightClientHeaderFulu::block_to_light_client_header( attested_block, @@ -150,12 +171,15 @@ impl LightClientFinalityUpdate { finalized_header: LightClientHeaderFulu::block_to_light_client_header( finalized_block, )?, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate, signature_slot, }), - - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Eip7805 => return Err(LightClientError::GloasNotImplemented), + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), }; Ok(finality_update) @@ -181,6 +205,20 @@ impl LightClientFinalityUpdate { }) } + pub fn get_attested_header_root<'a>(&'a self) -> Hash256 { + map_light_client_finality_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.attested_header.beacon.canonical_root() + }) + } + + pub fn get_finalized_header_root<'a>(&'a self) -> Hash256 { + map_light_client_finality_update_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(inner); + inner.finalized_header.beacon.canonical_root() + }) + } + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { let finality_update = match fork_name { ForkName::Altair | ForkName::Bellatrix => { @@ -194,10 +232,11 @@ impl LightClientFinalityUpdate { Self::Electra(LightClientFinalityUpdateElectra::from_ssz_bytes(bytes)?) } ForkName::Fulu => Self::Fulu(LightClientFinalityUpdateFulu::from_ssz_bytes(bytes)?), - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientFinalityUpdate decoding for {fork_name} not implemented" - ))) + ))); } }; @@ -217,6 +256,8 @@ impl LightClientFinalityUpdate { as Encode>::ssz_fixed_len() } ForkName::Fulu => as Encode>::ssz_fixed_len(), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => 0, }; // `2 *` because there are two headers in the update fixed_size + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) @@ -231,7 +272,7 @@ impl LightClientFinalityUpdate { if attested_slot > prev_slot { true } else { - attested_slot == prev_slot && signature_slot > *self.signature_slot() + attested_slot == prev_slot && signature_slot > self.signature_slot() } } } @@ -252,7 +293,7 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientFinalityU return Err(serde::de::Error::custom(format!( "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Altair | ForkName::Bellatrix => { Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -269,6 +310,13 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientFinalityU ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } + ForkName::Gloas => { + // TODO(EIP-7732): check if this is correct + return Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + context + ))); + } }) } } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client/light_client_header.rs similarity index 82% rename from consensus/types/src/light_client_header.rs rename to consensus/types/src/light_client/light_client_header.rs index 7dbc8b8bdf..8db1e01be4 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client/light_client_header.rs @@ -1,49 +1,61 @@ -use crate::context_deserialize; -use crate::ChainSpec; -use crate::{light_client_update::*, BeaconBlockBody}; -use crate::{ - test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, FixedVector, Hash256, - SignedBlindedBeaconBlock, -}; -use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; -use crate::{ContextDeserialize, ForkName}; -use derivative::Derivative; +use std::marker::PhantomData; + +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use serde::{Deserialize, Deserializer, Serialize}; use ssz::Decode; use ssz_derive::{Decode, Encode}; -use std::marker::PhantomData; +use ssz_types::FixedVector; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + block::{BeaconBlockBody, BeaconBlockHeader, SignedBlindedBeaconBlock}, + core::{ChainSpec, EthSpec, Hash256}, + execution::{ + ExecutionPayloadHeader, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, + }, + fork::ForkName, + light_client::{ExecutionPayloadProofLen, LightClientError, consts::EXECUTION_PAYLOAD_INDEX}, + test_utils::TestRandom, +}; + #[superstruct( - variants(Altair, Capella, Deneb, Electra, Fulu), + variants(Altair, Capella, Deneb, Electra, Fulu,), variant_attributes( derive( Debug, Clone, - PartialEq, Serialize, Deserialize, - Derivative, + Educe, Decode, Encode, TestRandom, - arbitrary::Arbitrary, TreeHash, ), + educe(PartialEq), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ) )] -#[derive(Debug, Clone, Serialize, TreeHash, Encode, arbitrary::Arbitrary, PartialEq)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, TreeHash, Encode, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct LightClientHeader { pub beacon: BeaconBlockHeader, @@ -68,7 +80,7 @@ pub struct LightClientHeader { #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[serde(skip)] - #[arbitrary(default)] + #[cfg_attr(feature = "arbitrary", arbitrary(default))] pub _phantom_data: PhantomData, } @@ -76,12 +88,12 @@ impl LightClientHeader { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { let header = match block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), ForkName::Altair | ForkName::Bellatrix => LightClientHeader::Altair( LightClientHeaderAltair::block_to_light_client_header(block)?, ), @@ -97,6 +109,8 @@ impl LightClientHeader { ForkName::Fulu => { LightClientHeader::Fulu(LightClientHeaderFulu::block_to_light_client_header(block)?) } + // TODO(gloas): implement Gloas light client + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), }; Ok(header) } @@ -118,10 +132,11 @@ impl LightClientHeader { ForkName::Fulu => { LightClientHeader::Fulu(LightClientHeaderFulu::from_ssz_bytes(bytes)?) } - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientHeader decoding for {fork_name} not implemented" - ))) + ))); } }; @@ -137,7 +152,10 @@ impl LightClientHeader { } pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { - if fork_name.capella_enabled() { + if fork_name.gloas_enabled() { + // TODO(EIP7732): check this + 0 + } else if fork_name.capella_enabled() { ExecutionPayloadHeader::::ssz_max_var_len_for_fork(fork_name) } else { 0 @@ -148,7 +166,7 @@ impl LightClientHeader { impl LightClientHeaderAltair { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { Ok(LightClientHeaderAltair { beacon: block.message().block_header(), _phantom_data: PhantomData, @@ -168,7 +186,7 @@ impl Default for LightClientHeaderAltair { impl LightClientHeaderCapella { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { let payload = block .message() .execution_payload()? @@ -179,7 +197,7 @@ impl LightClientHeaderCapella { block .message() .body_capella() - .map_err(|_| Error::BeaconBlockBodyError)? + .map_err(|_| LightClientError::BeaconBlockBodyError)? .to_owned(), ); @@ -210,7 +228,7 @@ impl Default for LightClientHeaderCapella { impl LightClientHeaderDeneb { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { let header = block .message() .execution_payload()? @@ -221,7 +239,7 @@ impl LightClientHeaderDeneb { block .message() .body_deneb() - .map_err(|_| Error::BeaconBlockBodyError)? + .map_err(|_| LightClientError::BeaconBlockBodyError)? .to_owned(), ); @@ -252,7 +270,7 @@ impl Default for LightClientHeaderDeneb { impl LightClientHeaderElectra { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { let payload = block .message() .execution_payload()? @@ -263,7 +281,7 @@ impl LightClientHeaderElectra { block .message() .body_electra() - .map_err(|_| Error::BeaconBlockBodyError)? + .map_err(|_| LightClientError::BeaconBlockBodyError)? .to_owned(), ); @@ -294,7 +312,7 @@ impl Default for LightClientHeaderElectra { impl LightClientHeaderFulu { pub fn block_to_light_client_header( block: &SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { let payload = block .message() .execution_payload()? @@ -305,7 +323,7 @@ impl LightClientHeaderFulu { block .message() .body_fulu() - .map_err(|_| Error::BeaconBlockBodyError)? + .map_err(|_| LightClientError::BeaconBlockBodyError)? .to_owned(), ); @@ -345,11 +363,12 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientHeader )) }; Ok(match context { - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(serde::de::Error::custom(format!( "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Altair | ForkName::Bellatrix => { Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -396,4 +415,10 @@ mod tests { use crate::{LightClientHeaderElectra, MainnetEthSpec}; ssz_tests!(LightClientHeaderElectra); } + + #[cfg(test)] + mod fulu { + use crate::{LightClientHeaderFulu, MainnetEthSpec}; + ssz_tests!(LightClientHeaderFulu); + } } diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client/light_client_optimistic_update.rs similarity index 84% rename from consensus/types/src/light_client_optimistic_update.rs rename to consensus/types/src/light_client/light_client_optimistic_update.rs index 1b8af6e1da..9d6da2b1d2 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client/light_client_optimistic_update.rs @@ -1,21 +1,25 @@ -use super::{ContextDeserialize, EthSpec, ForkName, LightClientHeader, Slot, SyncAggregate}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ - light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, - LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, - SignedBlindedBeaconBlock, -}; -use derivative::Derivative; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; -use ssz_derive::Decode; -use ssz_derive::Encode; +use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash::Hash256; use tree_hash_derive::TreeHash; +use crate::{ + block::SignedBlindedBeaconBlock, + core::{ChainSpec, EthSpec, Slot}, + fork::ForkName, + light_client::{ + LightClientError, LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, + }, + sync_committee::SyncAggregate, + test_utils::TestRandom, +}; + /// A LightClientOptimisticUpdate is the update we send on each slot, /// it is based off the current unfinalized epoch is verified only against BLS signature. #[superstruct( @@ -24,27 +28,34 @@ use tree_hash_derive::TreeHash; derive( Debug, Clone, - PartialEq, Serialize, Deserialize, - Derivative, + Educe, Decode, Encode, TestRandom, - arbitrary::Arbitrary, TreeHash, ), + educe(PartialEq), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ) )] -#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct LightClientOptimisticUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] @@ -60,6 +71,7 @@ pub struct LightClientOptimisticUpdate { /// current sync aggregate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated signature + #[superstruct(getter(copy))] pub signature_slot: Slot, } @@ -69,10 +81,10 @@ impl LightClientOptimisticUpdate { sync_aggregate: SyncAggregate, signature_slot: Slot, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { let optimistic_update = match attested_block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { ForkName::Altair | ForkName::Bellatrix => { Self::Altair(LightClientOptimisticUpdateAltair { @@ -113,7 +125,8 @@ impl LightClientOptimisticUpdate { sync_aggregate, signature_slot, }), - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), }; Ok(optimistic_update) @@ -168,10 +181,11 @@ impl LightClientOptimisticUpdate { Self::Electra(LightClientOptimisticUpdateElectra::from_ssz_bytes(bytes)?) } ForkName::Fulu => Self::Fulu(LightClientOptimisticUpdateFulu::from_ssz_bytes(bytes)?), - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientOptimisticUpdate decoding for {fork_name} not implemented" - ))) + ))); } }; @@ -191,6 +205,8 @@ impl LightClientOptimisticUpdate { as Encode>::ssz_fixed_len() } ForkName::Fulu => as Encode>::ssz_fixed_len(), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => 0, }; fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -204,7 +220,7 @@ impl LightClientOptimisticUpdate { if attested_slot > prev_slot { true } else { - attested_slot == prev_slot && signature_slot > *self.signature_slot() + attested_slot == prev_slot && signature_slot > self.signature_slot() } } } @@ -225,7 +241,7 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientOptimisti return Err(serde::de::Error::custom(format!( "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Altair | ForkName::Bellatrix => { Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -242,6 +258,13 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientOptimisti ForkName::Fulu => { Self::Fulu(Deserialize::deserialize(deserializer).map_err(convert_err)?) } + ForkName::Gloas => { + // TODO(EIP-7732): check if this is correct + return Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + context + ))); + } }) } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client/light_client_update.rs similarity index 85% rename from consensus/types/src/light_client_update.rs rename to consensus/types/src/light_client/light_client_update.rs index 38ff25d8e8..da103c7976 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client/light_client_update.rs @@ -1,33 +1,30 @@ -use super::{EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; -use crate::context_deserialize; -use crate::light_client_header::LightClientHeaderElectra; -use crate::LightClientHeader; -use crate::{ - beacon_state, test_utils::TestRandom, ChainSpec, ContextDeserialize, Epoch, ForkName, - LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - LightClientHeaderFulu, SignedBlindedBeaconBlock, -}; -use derivative::Derivative; +use std::sync::Arc; + +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use safe_arith::ArithError; use safe_arith::SafeArith; use serde::{Deserialize, Deserializer, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::Decode; use ssz_derive::Encode; -use ssz_types::typenum::{U4, U5, U6, U7}; -use std::sync::Arc; +use ssz_types::FixedVector; use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use typenum::{U4, U5, U6, U7}; -pub const FINALIZED_ROOT_INDEX: usize = 105; -pub const CURRENT_SYNC_COMMITTEE_INDEX: usize = 54; -pub const NEXT_SYNC_COMMITTEE_INDEX: usize = 55; -pub const EXECUTION_PAYLOAD_INDEX: usize = 25; - -pub const FINALIZED_ROOT_INDEX_ELECTRA: usize = 169; -pub const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA: usize = 86; -pub const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA: usize = 87; +use crate::{ + block::SignedBlindedBeaconBlock, + core::{ChainSpec, Epoch, EthSpec, Hash256, Slot}, + fork::ForkName, + light_client::{ + LightClientError, LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, + LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, + }, + sync_committee::{SyncAggregate, SyncCommittee}, + test_utils::TestRandom, +}; pub type FinalizedRootProofLen = U6; pub type CurrentSyncCommitteeProofLen = U5; @@ -38,64 +35,12 @@ pub type FinalizedRootProofLenElectra = U7; pub type CurrentSyncCommitteeProofLenElectra = U6; pub type NextSyncCommitteeProofLenElectra = U6; -pub const FINALIZED_ROOT_PROOF_LEN: usize = 6; -pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; -pub const NEXT_SYNC_COMMITTEE_PROOF_LEN: usize = 5; -pub const EXECUTION_PAYLOAD_PROOF_LEN: usize = 4; - -pub const FINALIZED_ROOT_PROOF_LEN_ELECTRA: usize = 7; -pub const NEXT_SYNC_COMMITTEE_PROOF_LEN_ELECTRA: usize = 6; -pub const CURRENT_SYNC_COMMITTEE_PROOF_LEN_ELECTRA: usize = 6; - -pub type MerkleProof = Vec; -// Max light client updates by range request limits -// spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#configuration -pub const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u64 = 128; - type FinalityBranch = FixedVector; type FinalityBranchElectra = FixedVector; type NextSyncCommitteeBranch = FixedVector; type NextSyncCommitteeBranchElectra = FixedVector; -#[derive(Debug, PartialEq, Clone)] -pub enum Error { - SszTypesError(ssz_types::Error), - MilhouseError(milhouse::Error), - BeaconStateError(beacon_state::Error), - ArithError(ArithError), - AltairForkNotActive, - NotEnoughSyncCommitteeParticipants, - MismatchingPeriods, - InvalidFinalizedBlock, - BeaconBlockBodyError, - InconsistentFork, -} - -impl From for Error { - fn from(e: ssz_types::Error) -> Error { - Error::SszTypesError(e) - } -} - -impl From for Error { - fn from(e: beacon_state::Error) -> Error { - Error::BeaconStateError(e) - } -} - -impl From for Error { - fn from(e: ArithError) -> Error { - Error::ArithError(e) - } -} - -impl From for Error { - fn from(e: milhouse::Error) -> Error { - Error::MilhouseError(e) - } -} - /// A LightClientUpdate is the update we request solely to either complete the bootstrapping process, /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. @@ -105,27 +50,34 @@ impl From for Error { derive( Debug, Clone, - PartialEq, Serialize, Deserialize, - Derivative, + Educe, Decode, Encode, TestRandom, - arbitrary::Arbitrary, TreeHash, ), + educe(PartialEq), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec"), + ), context_deserialize(ForkName), ) )] -#[derive(Debug, Clone, Serialize, Encode, TreeHash, arbitrary::Arbitrary, PartialEq)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, Clone, Serialize, Encode, TreeHash, PartialEq)] #[serde(untagged)] #[tree_hash(enum_behaviour = "transparent")] #[ssz(enum_behaviour = "transparent")] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -#[arbitrary(bound = "E: EthSpec")] pub struct LightClientUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. #[superstruct(only(Altair), partial_getter(rename = "attested_header_altair"))] @@ -188,11 +140,12 @@ impl<'de, E: EthSpec> ContextDeserialize<'de, ForkName> for LightClientUpdate serde::de::Error::custom(format!("LightClientUpdate failed to deserialize: {:?}", e)) }; Ok(match context { - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(serde::de::Error::custom(format!( "LightClientUpdate failed to deserialize: unsupported fork '{}'", context - ))) + ))); } ForkName::Altair | ForkName::Bellatrix => { Self::Altair(Deserialize::deserialize(deserializer).map_err(convert_err)?) @@ -224,12 +177,12 @@ impl LightClientUpdate { attested_block: &SignedBlindedBeaconBlock, finalized_block: Option<&SignedBlindedBeaconBlock>, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { let light_client_update = match attested_block .fork_name(chain_spec) - .map_err(|_| Error::InconsistentFork)? + .map_err(|_| LightClientError::InconsistentFork)? { - ForkName::Base => return Err(Error::AltairForkNotActive), + ForkName::Base => return Err(LightClientError::AltairForkNotActive), fork_name @ ForkName::Altair | fork_name @ ForkName::Bellatrix => { let attested_header = LightClientHeaderAltair::block_to_light_client_header(attested_block)?; @@ -247,9 +200,13 @@ impl LightClientUpdate { Self::Altair(LightClientUpdateAltair { attested_header, next_sync_committee, - next_sync_committee_branch: next_sync_committee_branch.into(), + next_sync_committee_branch: next_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, finalized_header, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) @@ -271,9 +228,13 @@ impl LightClientUpdate { Self::Capella(LightClientUpdateCapella { attested_header, next_sync_committee, - next_sync_committee_branch: next_sync_committee_branch.into(), + next_sync_committee_branch: next_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, finalized_header, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) @@ -295,9 +256,13 @@ impl LightClientUpdate { Self::Deneb(LightClientUpdateDeneb { attested_header, next_sync_committee, - next_sync_committee_branch: next_sync_committee_branch.into(), + next_sync_committee_branch: next_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, finalized_header, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) @@ -319,9 +284,13 @@ impl LightClientUpdate { Self::Electra(LightClientUpdateElectra { attested_header, next_sync_committee, - next_sync_committee_branch: next_sync_committee_branch.into(), + next_sync_committee_branch: next_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, finalized_header, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) @@ -343,15 +312,22 @@ impl LightClientUpdate { Self::Fulu(LightClientUpdateFulu { attested_header, next_sync_committee, - next_sync_committee_branch: next_sync_committee_branch.into(), + next_sync_committee_branch: next_sync_committee_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, finalized_header, - finality_branch: finality_branch.into(), + finality_branch: finality_branch + .try_into() + .map_err(LightClientError::SszTypesError)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) - } // To add a new fork, just append the new fork variant on the latest fork. Forks that - // have a distinct execution header will need a new LightClientUpdate variant only - // if you need to test or support lightclient usages + } + // To add a new fork, just append the new fork variant on the latest fork. Forks that + // have a distinct execution header will need a new LightClientUpdate variant only + // if you need to test or support lightclient usages + // TODO(gloas): implement Gloas light client + ForkName::Gloas => return Err(LightClientError::GloasNotImplemented), }; Ok(light_client_update) @@ -368,10 +344,11 @@ impl LightClientUpdate { Self::Electra(LightClientUpdateElectra::from_ssz_bytes(bytes)?) } ForkName::Fulu => Self::Fulu(LightClientUpdateFulu::from_ssz_bytes(bytes)?), - ForkName::Base => { + // TODO(gloas): implement Gloas light client + ForkName::Base | ForkName::Gloas => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientUpdate decoding for {fork_name} not implemented" - ))) + ))); } }; @@ -401,23 +378,32 @@ impl LightClientUpdate { fn attested_header_sync_committee_period( &self, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { compute_sync_committee_period_at_slot::(self.attested_header_slot(), chain_spec) - .map_err(Error::ArithError) + .map_err(LightClientError::ArithError) } - fn signature_slot_sync_committee_period(&self, chain_spec: &ChainSpec) -> Result { + fn signature_slot_sync_committee_period( + &self, + chain_spec: &ChainSpec, + ) -> Result { compute_sync_committee_period_at_slot::(*self.signature_slot(), chain_spec) - .map_err(Error::ArithError) + .map_err(LightClientError::ArithError) } - pub fn is_sync_committee_update(&self, chain_spec: &ChainSpec) -> Result { + pub fn is_sync_committee_update( + &self, + chain_spec: &ChainSpec, + ) -> Result { Ok(!self.is_next_sync_committee_branch_empty() && (self.attested_header_sync_committee_period(chain_spec)? == self.signature_slot_sync_committee_period(chain_spec)?)) } - pub fn has_sync_committee_finality(&self, chain_spec: &ChainSpec) -> Result { + pub fn has_sync_committee_finality( + &self, + chain_spec: &ChainSpec, + ) -> Result { Ok( compute_sync_committee_period_at_slot::(self.finalized_header_slot(), chain_spec)? == self.attested_header_sync_committee_period(chain_spec)?, @@ -431,7 +417,7 @@ impl LightClientUpdate { &self, new: &Self, chain_spec: &ChainSpec, - ) -> Result { + ) -> Result { // Compare super majority (> 2/3) sync committee participation let max_active_participants = new.sync_aggregate().sync_committee_bits.len(); @@ -517,6 +503,8 @@ impl LightClientUpdate { as Encode>::ssz_fixed_len() } ForkName::Fulu => as Encode>::ssz_fixed_len(), + // TODO(gloas): implement Gloas light client + ForkName::Gloas => 0, }; fixed_len + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -555,7 +543,8 @@ fn compute_sync_committee_period_at_slot( #[cfg(test)] mod tests { use super::*; - use ssz_types::typenum::Unsigned; + use crate::light_client::consts::*; + use typenum::Unsigned; // `ssz_tests!` can only be defined once per namespace #[cfg(test)] diff --git a/consensus/types/src/light_client/mod.rs b/consensus/types/src/light_client/mod.rs new file mode 100644 index 0000000000..24f3fdbb55 --- /dev/null +++ b/consensus/types/src/light_client/mod.rs @@ -0,0 +1,35 @@ +mod error; +mod light_client_bootstrap; +mod light_client_finality_update; +mod light_client_header; +mod light_client_optimistic_update; +mod light_client_update; + +pub mod consts; + +pub use error::LightClientError; +pub use light_client_bootstrap::{ + LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, + LightClientBootstrapDeneb, LightClientBootstrapElectra, LightClientBootstrapFulu, +}; +pub use light_client_finality_update::{ + LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, + LightClientFinalityUpdateDeneb, LightClientFinalityUpdateElectra, + LightClientFinalityUpdateFulu, +}; +pub use light_client_header::{ + LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, LightClientHeaderFulu, +}; +pub use light_client_optimistic_update::{ + LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, + LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, + LightClientOptimisticUpdateElectra, LightClientOptimisticUpdateFulu, +}; +pub use light_client_update::{ + CurrentSyncCommitteeProofLen, CurrentSyncCommitteeProofLenElectra, ExecutionPayloadProofLen, + FinalizedRootProofLen, FinalizedRootProofLenElectra, LightClientUpdate, + LightClientUpdateAltair, LightClientUpdateCapella, LightClientUpdateDeneb, + LightClientUpdateElectra, LightClientUpdateFulu, NextSyncCommitteeProofLen, + NextSyncCommitteeProofLenElectra, +}; diff --git a/consensus/types/src/runtime_fixed_vector.rs b/consensus/types/src/runtime_fixed_vector.rs deleted file mode 100644 index 2b08b7bf70..0000000000 --- a/consensus/types/src/runtime_fixed_vector.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Emulates a fixed size array but with the length set at runtime. -//! -//! The length of the list cannot be changed once it is set. - -#[derive(Clone, Debug)] -pub struct RuntimeFixedVector { - vec: Vec, - len: usize, -} - -impl RuntimeFixedVector { - pub fn new(vec: Vec) -> Self { - let len = vec.len(); - Self { vec, len } - } - - pub fn to_vec(&self) -> Vec { - self.vec.clone() - } - - pub fn as_slice(&self) -> &[T] { - self.vec.as_slice() - } - - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.len - } - - pub fn into_vec(self) -> Vec { - self.vec - } - - pub fn default(max_len: usize) -> Self { - Self { - vec: vec![T::default(); max_len], - len: max_len, - } - } - - pub fn take(&mut self) -> Self { - let new = std::mem::take(&mut self.vec); - *self = Self::new(vec![T::default(); self.len]); - Self { - vec: new, - len: self.len, - } - } -} - -impl std::ops::Deref for RuntimeFixedVector { - type Target = [T]; - - fn deref(&self) -> &[T] { - &self.vec[..] - } -} - -impl std::ops::DerefMut for RuntimeFixedVector { - fn deref_mut(&mut self) -> &mut [T] { - &mut self.vec[..] - } -} - -impl IntoIterator for RuntimeFixedVector { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.vec.into_iter() - } -} - -impl<'a, T> IntoIterator for &'a RuntimeFixedVector { - type Item = &'a T; - type IntoIter = std::slice::Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.vec.iter() - } -} diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs deleted file mode 100644 index 454c8b9e18..0000000000 --- a/consensus/types/src/runtime_var_list.rs +++ /dev/null @@ -1,327 +0,0 @@ -use crate::ContextDeserialize; -use derivative::Derivative; -use serde::de::Error as DeError; -use serde::{Deserialize, Deserializer, Serialize}; -use ssz::Decode; -use ssz_types::Error; -use std::ops::{Deref, Index, IndexMut}; -use std::slice::SliceIndex; - -/// Emulates a SSZ `List`. -/// -/// An ordered, heap-allocated, variable-length, homogeneous collection of `T`, with no more than -/// `max_len` values. -/// -/// To ensure there are no inconsistent states, we do not allow any mutating operation if `max_len` is not set. -/// -/// ## Example -/// -/// ``` -/// use types::{RuntimeVariableList}; -/// -/// let base: Vec = vec![1, 2, 3, 4]; -/// -/// // Create a `RuntimeVariableList` from a `Vec` that has the expected length. -/// let exact: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base.clone(), 4); -/// assert_eq!(&exact[..], &[1, 2, 3, 4]); -/// -/// // Create a `RuntimeVariableList` from a `Vec` that is too long and the `Vec` is truncated. -/// let short: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base.clone(), 3); -/// assert_eq!(&short[..], &[1, 2, 3]); -/// -/// // Create a `RuntimeVariableList` from a `Vec` that is shorter than the maximum. -/// let mut long: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base, 5); -/// assert_eq!(&long[..], &[1, 2, 3, 4]); -/// -/// // Push a value to if it does not exceed the maximum -/// long.push(5).unwrap(); -/// assert_eq!(&long[..], &[1, 2, 3, 4, 5]); -/// -/// // Push a value to if it _does_ exceed the maximum. -/// assert!(long.push(6).is_err()); -/// -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] -#[derivative(PartialEq, Eq, Hash(bound = "T: std::hash::Hash"))] -#[serde(transparent)] -pub struct RuntimeVariableList { - vec: Vec, - #[serde(skip)] - max_len: usize, -} - -impl RuntimeVariableList { - /// Returns `Ok` if the given `vec` equals the fixed length of `Self`. Otherwise returns - /// `Err(OutOfBounds { .. })`. - pub fn new(vec: Vec, max_len: usize) -> Result { - if vec.len() <= max_len { - Ok(Self { vec, max_len }) - } else { - Err(Error::OutOfBounds { - i: vec.len(), - len: max_len, - }) - } - } - - pub fn from_vec(mut vec: Vec, max_len: usize) -> Self { - vec.truncate(max_len); - - Self { vec, max_len } - } - - /// Create an empty list with the given `max_len`. - pub fn empty(max_len: usize) -> Self { - Self { - vec: vec![], - max_len, - } - } - - pub fn as_slice(&self) -> &[T] { - self.vec.as_slice() - } - - pub fn as_mut_slice(&mut self) -> &mut [T] { - self.vec.as_mut_slice() - } - - /// Returns the number of values presently in `self`. - pub fn len(&self) -> usize { - self.vec.len() - } - - /// True if `self` does not contain any values. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the type-level maximum length. - /// - /// Returns `None` if self is uninitialized with a max_len. - pub fn max_len(&self) -> usize { - self.max_len - } - - /// Appends `value` to the back of `self`. - /// - /// Returns `Err(())` when appending `value` would exceed the maximum length. - pub fn push(&mut self, value: T) -> Result<(), Error> { - if self.vec.len() < self.max_len { - self.vec.push(value); - Ok(()) - } else { - Err(Error::OutOfBounds { - i: self.vec.len().saturating_add(1), - len: self.max_len, - }) - } - } -} - -impl RuntimeVariableList { - pub fn from_ssz_bytes(bytes: &[u8], max_len: usize) -> Result { - let vec = if bytes.is_empty() { - vec![] - } else if ::is_ssz_fixed_len() { - let num_items = bytes - .len() - .checked_div(::ssz_fixed_len()) - .ok_or(ssz::DecodeError::ZeroLengthItem)?; - - if num_items > max_len { - return Err(ssz::DecodeError::BytesInvalid(format!( - "RuntimeVariableList of {} items exceeds maximum of {}", - num_items, max_len - ))); - } - - bytes.chunks(::ssz_fixed_len()).try_fold( - Vec::with_capacity(num_items), - |mut vec, chunk| { - vec.push(::from_ssz_bytes(chunk)?); - Ok(vec) - }, - )? - } else { - ssz::decode_list_of_variable_length_items(bytes, Some(max_len))? - }; - Ok(Self { vec, max_len }) - } -} - -impl From> for Vec { - fn from(list: RuntimeVariableList) -> Vec { - list.vec - } -} - -impl> Index for RuntimeVariableList { - type Output = I::Output; - - #[inline] - fn index(&self, index: I) -> &Self::Output { - Index::index(&self.vec, index) - } -} - -impl> IndexMut for RuntimeVariableList { - #[inline] - fn index_mut(&mut self, index: I) -> &mut Self::Output { - IndexMut::index_mut(&mut self.vec, index) - } -} - -impl Deref for RuntimeVariableList { - type Target = [T]; - - fn deref(&self) -> &[T] { - &self.vec[..] - } -} - -impl<'a, T> IntoIterator for &'a RuntimeVariableList { - type Item = &'a T; - type IntoIter = std::slice::Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl IntoIterator for RuntimeVariableList { - type Item = T; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.vec.into_iter() - } -} - -impl ssz::Encode for RuntimeVariableList -where - T: ssz::Encode, -{ - fn is_ssz_fixed_len() -> bool { - >::is_ssz_fixed_len() - } - - fn ssz_append(&self, buf: &mut Vec) { - self.vec.ssz_append(buf) - } - - fn ssz_fixed_len() -> usize { - >::ssz_fixed_len() - } - - fn ssz_bytes_len(&self) -> usize { - self.vec.ssz_bytes_len() - } -} - -impl<'de, C, T> ContextDeserialize<'de, (C, usize)> for RuntimeVariableList -where - T: ContextDeserialize<'de, C>, - C: Clone, -{ - fn context_deserialize(deserializer: D, context: (C, usize)) -> Result - where - D: Deserializer<'de>, - { - // first parse out a Vec using the Vec impl you already have - let vec: Vec = Vec::context_deserialize(deserializer, context.0)?; - if vec.len() > context.1 { - return Err(DeError::custom(format!( - "RuntimeVariableList lengh {} exceeds max_len {}", - vec.len(), - context.1 - ))); - } - Ok(RuntimeVariableList::from_vec(vec, context.1)) - } -} - -#[cfg(test)] -mod test { - use super::*; - use ssz::*; - use std::fmt::Debug; - - #[test] - fn new() { - let vec = vec![42; 5]; - let fixed: Result, _> = RuntimeVariableList::new(vec, 4); - assert!(fixed.is_err()); - - let vec = vec![42; 3]; - let fixed: Result, _> = RuntimeVariableList::new(vec, 4); - assert!(fixed.is_ok()); - - let vec = vec![42; 4]; - let fixed: Result, _> = RuntimeVariableList::new(vec, 4); - assert!(fixed.is_ok()); - } - - #[test] - fn indexing() { - let vec = vec![1, 2]; - - let mut fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 8192); - - assert_eq!(fixed[0], 1); - assert_eq!(&fixed[0..1], &vec[0..1]); - assert_eq!(fixed[..].len(), 2); - - fixed[1] = 3; - assert_eq!(fixed[1], 3); - } - - #[test] - fn length() { - let vec = vec![42; 5]; - let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 4); - assert_eq!(&fixed[..], &vec[0..4]); - - let vec = vec![42; 3]; - let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 4); - assert_eq!(&fixed[0..3], &vec[..]); - assert_eq!(&fixed[..], &vec![42, 42, 42][..]); - - let vec = vec![]; - let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec, 4); - assert_eq!(&fixed[..], &[] as &[u64]); - } - - #[test] - fn deref() { - let vec = vec![0, 2, 4, 6]; - let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec, 4); - - assert_eq!(fixed.first(), Some(&0)); - assert_eq!(fixed.get(3), Some(&6)); - assert_eq!(fixed.get(4), None); - } - - #[test] - fn encode() { - let vec: RuntimeVariableList = RuntimeVariableList::from_vec(vec![0; 2], 2); - assert_eq!(vec.as_ssz_bytes(), vec![0, 0, 0, 0]); - assert_eq!( as Encode>::ssz_fixed_len(), 4); - } - - fn round_trip(item: RuntimeVariableList) { - let max_len = item.max_len(); - let encoded = &item.as_ssz_bytes(); - assert_eq!(item.ssz_bytes_len(), encoded.len()); - assert_eq!( - RuntimeVariableList::from_ssz_bytes(encoded, max_len), - Ok(item) - ); - } - - #[test] - fn u16_len_8() { - round_trip::(RuntimeVariableList::from_vec(vec![42; 8], 8)); - round_trip::(RuntimeVariableList::from_vec(vec![0; 8], 8)); - } -} diff --git a/consensus/types/src/attester_slashing.rs b/consensus/types/src/slashing/attester_slashing.rs similarity index 87% rename from consensus/types/src/attester_slashing.rs rename to consensus/types/src/slashing/attester_slashing.rs index 8fb5862f21..5c214b35f7 100644 --- a/consensus/types/src/attester_slashing.rs +++ b/consensus/types/src/slashing/attester_slashing.rs @@ -1,10 +1,5 @@ -use crate::context_deserialize; -use crate::indexed_attestation::{ - IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, -}; -use crate::{test_utils::TestRandom, EthSpec}; -use crate::{ContextDeserialize, ForkName}; -use derivative::Derivative; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use educe::Educe; use rand::{Rng, RngCore}; use serde::{Deserialize, Deserializer, Serialize}; use ssz_derive::{Decode, Encode}; @@ -12,11 +7,18 @@ use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + attestation::{IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef}, + core::EthSpec, + fork::ForkName, + test_utils::TestRandom, +}; + #[superstruct( variants(Base, Electra), variant_attributes( derive( - Derivative, + Educe, Debug, Clone, Serialize, @@ -25,21 +27,26 @@ use tree_hash_derive::TreeHash; Decode, TreeHash, TestRandom, - arbitrary::Arbitrary ), context_deserialize(ForkName), - derivative(PartialEq, Eq, Hash(bound = "E: EthSpec")), + educe(PartialEq, Eq, Hash(bound(E: EthSpec))), serde(bound = "E: EthSpec"), - arbitrary(bound = "E: EthSpec") + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") + ), ), ref_attributes(derive(Debug)) )] -#[derive( - Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Educe)] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec", untagged)] -#[arbitrary(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] pub struct AttesterSlashing { @@ -52,8 +59,8 @@ pub struct AttesterSlashing { /// This is a copy of the `AttesterSlashing` enum but with `Encode` and `Decode` derived /// using the `union` behavior for the purposes of persistence on disk. We use a separate /// type so that we don't accidentally use this non-spec encoding in consensus objects. -#[derive(Debug, Clone, Encode, Decode, Derivative)] -#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Encode, Decode, Educe)] +#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))] #[ssz(enum_behaviour = "union")] pub enum AttesterSlashingOnDisk { Base(AttesterSlashingBase), @@ -141,7 +148,7 @@ impl<'a, E: EthSpec> AttesterSlashingRef<'a, E> { } impl AttesterSlashing { - pub fn attestation_1(&self) -> IndexedAttestationRef { + pub fn attestation_1(&self) -> IndexedAttestationRef<'_, E> { match self { AttesterSlashing::Base(attester_slashing) => { IndexedAttestationRef::Base(&attester_slashing.attestation_1) @@ -152,7 +159,7 @@ impl AttesterSlashing { } } - pub fn attestation_2(&self) -> IndexedAttestationRef { + pub fn attestation_2(&self) -> IndexedAttestationRef<'_, E> { match self { AttesterSlashing::Base(attester_slashing) => { IndexedAttestationRef::Base(&attester_slashing.attestation_2) @@ -166,7 +173,7 @@ impl AttesterSlashing { impl TestRandom for AttesterSlashing { fn random_for_test(rng: &mut impl RngCore) -> Self { - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { AttesterSlashing::Base(AttesterSlashingBase::random_for_test(rng)) } else { AttesterSlashing::Electra(AttesterSlashingElectra::random_for_test(rng)) diff --git a/consensus/types/src/slashing/mod.rs b/consensus/types/src/slashing/mod.rs new file mode 100644 index 0000000000..551b8e3137 --- /dev/null +++ b/consensus/types/src/slashing/mod.rs @@ -0,0 +1,8 @@ +mod attester_slashing; +mod proposer_slashing; + +pub use attester_slashing::{ + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingOnDisk, + AttesterSlashingRef, AttesterSlashingRefOnDisk, +}; +pub use proposer_slashing::ProposerSlashing; diff --git a/consensus/types/src/proposer_slashing.rs b/consensus/types/src/slashing/proposer_slashing.rs similarity index 70% rename from consensus/types/src/proposer_slashing.rs rename to consensus/types/src/slashing/proposer_slashing.rs index 7b03dbb83e..697bd1a9aa 100644 --- a/consensus/types/src/proposer_slashing.rs +++ b/consensus/types/src/slashing/proposer_slashing.rs @@ -1,28 +1,17 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ForkName, SignedBeaconBlockHeader}; - +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{block::SignedBeaconBlockHeader, fork::ForkName, test_utils::TestRandom}; + /// Two conflicting proposals from the same proposer (validator). /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct ProposerSlashing { diff --git a/consensus/types/src/activation_queue.rs b/consensus/types/src/state/activation_queue.rs similarity index 88% rename from consensus/types/src/activation_queue.rs rename to consensus/types/src/state/activation_queue.rs index 09ffa5b85e..0d920a20cf 100644 --- a/consensus/types/src/activation_queue.rs +++ b/consensus/types/src/state/activation_queue.rs @@ -1,8 +1,13 @@ -use crate::{ChainSpec, Epoch, Validator}; use std::collections::BTreeSet; +use crate::{ + core::{ChainSpec, Epoch}, + validator::Validator, +}; + /// Activation queue computed during epoch processing for use in the *next* epoch. -#[derive(Debug, PartialEq, Eq, Default, Clone, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct ActivationQueue { /// Validators represented by `(activation_eligibility_epoch, index)` in sorted order. /// diff --git a/consensus/types/src/beacon_state/balance.rs b/consensus/types/src/state/balance.rs similarity index 88% rename from consensus/types/src/beacon_state/balance.rs rename to consensus/types/src/state/balance.rs index e537a5b984..cd449bdb82 100644 --- a/consensus/types/src/beacon_state/balance.rs +++ b/consensus/types/src/state/balance.rs @@ -1,10 +1,12 @@ +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use safe_arith::{ArithError, SafeArith}; /// A balance which will never be below the specified `minimum`. /// /// This is an effort to ensure the `EFFECTIVE_BALANCE_INCREMENT` minimum is always respected. -#[derive(PartialEq, Debug, Clone, Copy, Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(PartialEq, Debug, Clone, Copy)] pub struct Balance { raw: u64, minimum: u64, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/state/beacon_state.rs similarity index 77% rename from consensus/types/src/beacon_state.rs rename to consensus/types/src/state/beacon_state.rs index 2112946fd4..4c1c2085b9 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -1,51 +1,58 @@ -use self::committee_cache::get_active_validator_indices; -use crate::historical_summary::HistoricalSummary; -use crate::test_utils::TestRandom; -use crate::ContextDeserialize; -use crate::FixedBytesExtended; -use crate::*; +use std::{fmt, hash::Hash, mem, sync::Arc}; + +use bls::{AggregatePublicKey, PublicKeyBytes, Signature}; use compare_fields::CompareFields; -use compare_fields_derive::CompareFields; -use derivative::Derivative; +use context_deserialize::ContextDeserialize; +use educe::Educe; use ethereum_hashing::hash; +use fixed_bytes::FixedBytesExtended; use int_to_bytes::{int_to_bytes4, int_to_bytes8}; -use metastruct::{metastruct, NumFields}; -pub use pubkey_cache::PubkeyCache; +use metastruct::{NumFields, metastruct}; +use milhouse::{List, Vector}; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Deserializer, Serialize}; -use ssz::{ssz_encode, Decode, DecodeError, Encode}; +use ssz::{Decode, DecodeError, Encode, ssz_encode}; use ssz_derive::{Decode, Encode}; -use std::hash::Hash; -use std::{fmt, mem, sync::Arc}; +use ssz_types::{BitVector, FixedVector}; use superstruct::superstruct; use swap_or_not_shuffle::compute_shuffled_index; use test_random_derive::TestRandom; +use tracing::instrument; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use typenum::Unsigned; -pub use self::committee_cache::{ - compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count, - CommitteeCache, +use crate::{ + BuilderPendingPayment, BuilderPendingWithdrawal, ExecutionBlockHash, ExecutionPayloadBid, + attestation::{ + AttestationDuty, BeaconCommittee, Checkpoint, CommitteeIndex, ParticipationFlags, + PendingAttestation, + }, + block::{BeaconBlock, BeaconBlockHeader, SignedBeaconBlockHash}, + consolidation::PendingConsolidation, + core::{ChainSpec, Domain, Epoch, EthSpec, Hash256, RelativeEpoch, RelativeEpochError, Slot}, + deposit::PendingDeposit, + execution::{ + Eth1Data, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderEip7805, ExecutionPayloadHeaderElectra, + ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, + InclusionListCommittee, InclusionListDuty, + }, + fork::{Fork, ForkName, ForkVersionDecode, InconsistentFork, map_fork_name}, + light_client::consts::{ + CURRENT_SYNC_COMMITTEE_INDEX, CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA, FINALIZED_ROOT_INDEX, + FINALIZED_ROOT_INDEX_ELECTRA, NEXT_SYNC_COMMITTEE_INDEX, NEXT_SYNC_COMMITTEE_INDEX_ELECTRA, + }, + state::{ + BlockRootsIter, CommitteeCache, EpochCache, EpochCacheError, ExitCache, HistoricalBatch, + HistoricalSummary, ProgressiveBalancesCache, PubkeyCache, SlashingsCache, + get_active_validator_indices, + }, + sync_committee::{SyncCommittee, SyncDuty}, + test_utils::TestRandom, + validator::Validator, + withdrawal::PendingPartialWithdrawal, }; -pub use crate::beacon_state::balance::Balance; -pub use crate::beacon_state::exit_cache::ExitCache; -pub use crate::beacon_state::inclusion_list_cache::InclusionListCache; -pub use crate::beacon_state::progressive_balances_cache::*; -pub use crate::beacon_state::slashings_cache::SlashingsCache; -pub use eth_spec::*; -pub use iter::BlockRootsIter; -pub use milhouse::{interface::Interface, List, Vector}; - -#[macro_use] -mod committee_cache; -mod balance; -mod exit_cache; -mod inclusion_list_cache; -mod iter; -mod progressive_balances_cache; -mod pubkey_cache; -mod slashings_cache; -mod tests; pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; @@ -55,7 +62,7 @@ pub type Validators = List::ValidatorRegistryLimit> pub type Balances = List::ValidatorRegistryLimit>; #[derive(Debug, PartialEq, Clone)] -pub enum Error { +pub enum BeaconStateError { /// A state for a different hard-fork was required -- a severe logic error. IncorrectStateVariant, EpochOutOfBounds, @@ -174,6 +181,21 @@ pub enum Error { AggregatorNotInCommittee { aggregator_index: u64, }, + ComputeProposerIndicesPastEpoch { + current_epoch: Epoch, + request_epoch: Epoch, + }, + ComputeProposerIndicesInsufficientLookahead { + current_epoch: Epoch, + request_epoch: Epoch, + }, + ComputeProposerIndicesExcessiveLookahead { + current_epoch: Epoch, + request_epoch: Epoch, + }, + ProposerLookaheadOutOfBounds { + i: usize, + }, } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. @@ -184,7 +206,7 @@ enum AllowNextEpoch { } impl AllowNextEpoch { - fn upper_bound_of(self, current_epoch: Epoch) -> Result { + fn upper_bound_of(self, current_epoch: Epoch) -> Result { match self { AllowNextEpoch::True => Ok(current_epoch.safe_add(1)?), AllowNextEpoch::False => Ok(current_epoch), @@ -192,7 +214,8 @@ impl AllowNextEpoch { } } -#[derive(PartialEq, Eq, Hash, Clone, Copy, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Eq, Hash, Clone, Copy)] pub struct BeaconStateHash(Hash256); impl fmt::Debug for BeaconStateHash { @@ -228,10 +251,10 @@ impl From for Hash256 { /// /// https://github.com/sigp/milhouse/issues/43 #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas), variant_attributes( derive( - Derivative, + Educe, Debug, PartialEq, Serialize, @@ -241,11 +264,14 @@ impl From for Hash256 { TreeHash, TestRandom, CompareFields, - arbitrary::Arbitrary, ), serde(bound = "E: EthSpec", deny_unknown_fields), - arbitrary(bound = "E: EthSpec"), - derivative(Clone), + cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") + ), + educe(Clone), ), specific_variant_attributes( Base(metastruct( @@ -332,6 +358,20 @@ impl From for Hash256 { )), num_fields(all()), )), + Fulu(metastruct( + mappings( + map_beacon_state_fulu_fields(), + map_beacon_state_fulu_tree_list_fields(mutable, fallible, groups(tree_lists)), + map_beacon_state_fulu_tree_list_fields_immutable(groups(tree_lists)), + ), + bimappings(bimap_beacon_state_fulu_tree_list_fields( + other_type = "BeaconStateFulu", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), + )), Eip7805(metastruct( mappings( map_beacon_state_eip7805_fields(), @@ -346,14 +386,14 @@ impl From for Hash256 { )), num_fields(all()), )), - Fulu(metastruct( + Gloas(metastruct( mappings( - map_beacon_state_fulu_fields(), - map_beacon_state_fulu_tree_list_fields(mutable, fallible, groups(tree_lists)), - map_beacon_state_fulu_tree_list_fields_immutable(groups(tree_lists)), + map_beacon_state_gloas_fields(), + map_beacon_state_gloas_tree_list_fields(mutable, fallible, groups(tree_lists)), + map_beacon_state_gloas_tree_list_fields_immutable(groups(tree_lists)), ), - bimappings(bimap_beacon_state_fulu_tree_list_fields( - other_type = "BeaconStateFulu", + bimappings(bimap_beacon_state_gloas_tree_list_fields( + other_type = "BeaconStateGloas", self_mutable, fallible, groups(tree_lists) @@ -361,14 +401,24 @@ impl From for Hash256 { num_fields(all()), )) ), - cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), - partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant"), + cast_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), + partial_getter_error( + ty = "BeaconStateError", + expr = "BeaconStateError::IncorrectStateVariant" + ), map_ref_mut_into(BeaconStateRef) )] -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, arbitrary::Arbitrary)] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode)] #[serde(untagged)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[ssz(enum_behaviour = "transparent")] pub struct BeaconState where @@ -441,11 +491,11 @@ where // Participation (Altair and later) #[compare_fields(as_iter)] - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[test_random(default)] #[compare_fields(as_iter)] pub previous_epoch_participation: List, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[test_random(default)] pub current_epoch_participation: List, @@ -465,15 +515,15 @@ where // Inactivity #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[test_random(default)] pub inactivity_scores: List, // Light-client sync committees - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[metastruct(exclude_from(tree_lists))] pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[metastruct(exclude_from(tree_lists))] pub next_sync_committee: Arc>, @@ -502,70 +552,111 @@ where )] #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, - #[superstruct( - only(Eip7805), - partial_getter(rename = "latest_execution_payload_header_eip7805") - )] - #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_header: ExecutionPayloadHeaderEip7805, #[superstruct( only(Fulu), partial_getter(rename = "latest_execution_payload_header_fulu") )] #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, - - // Capella - #[superstruct(only(Capella, Deneb, Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct( + only(Eip7805), + partial_getter(rename = "latest_execution_payload_header_eip7805") + )] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_header: ExecutionPayloadHeaderEip7805, + #[superstruct(only(Gloas))] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_bid: ExecutionPayloadBid, + #[superstruct( + only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas), + partial_getter(copy) + )] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb, Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct( + only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas), + partial_getter(copy) + )] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_validator_index: u64, // Deep history valid from Capella onwards. - #[superstruct(only(Capella, Deneb, Electra, Eip7805, Fulu))] + #[superstruct(only(Capella, Deneb, Electra, Fulu, Eip7805, Gloas))] #[test_random(default)] pub historical_summaries: List, // Electra - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub deposit_requests_start_index: u64, - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub deposit_balance_to_consume: u64, - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub exit_balance_to_consume: u64, - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] pub earliest_exit_epoch: Epoch, - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub consolidation_balance_to_consume: u64, - #[superstruct(only(Electra, Eip7805, Fulu), partial_getter(copy))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] pub earliest_consolidation_epoch: Epoch, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub pending_deposits: List, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub pending_partial_withdrawals: List, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra, Eip7805, Fulu))] + #[superstruct(only(Electra, Fulu, Eip7805, Gloas))] pub pending_consolidations: List, + // Fulu + #[compare_fields(as_iter)] + #[test_random(default)] + #[superstruct(only(Fulu, Eip7805, Gloas))] + #[serde(with = "ssz_types::serde_utils::quoted_u64_fixed_vec")] + pub proposer_lookahead: Vector, + + // Gloas + #[test_random(default)] + #[superstruct(only(Gloas))] + #[metastruct(exclude_from(tree_lists))] + pub execution_payload_availability: BitVector, + + #[compare_fields(as_iter)] + #[test_random(default)] + #[superstruct(only(Gloas))] + pub builder_pending_payments: Vector, + + #[compare_fields(as_iter)] + #[test_random(default)] + #[superstruct(only(Gloas))] + pub builder_pending_withdrawals: + List, + + #[test_random(default)] + #[superstruct(only(Gloas))] + #[metastruct(exclude_from(tree_lists))] + pub latest_block_hash: ExecutionBlockHash, + + #[test_random(default)] + #[superstruct(only(Gloas))] + #[metastruct(exclude_from(tree_lists))] + pub latest_withdrawals_root: Hash256, + // Caching (not in the spec) #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] @@ -704,17 +795,18 @@ impl BeaconState { BeaconState::Capella { .. } => ForkName::Capella, BeaconState::Deneb { .. } => ForkName::Deneb, BeaconState::Electra { .. } => ForkName::Electra, - BeaconState::Eip7805 { .. } => ForkName::Eip7805, BeaconState::Fulu { .. } => ForkName::Fulu, + BeaconState::Eip7805 { .. } => ForkName::Eip7805, + BeaconState::Gloas { .. } => ForkName::Gloas, } } /// Returns the `tree_hash_root` of the state. - pub fn canonical_root(&mut self) -> Result { + pub fn canonical_root(&mut self) -> Result { self.update_tree_hash_cache() } - pub fn historical_batch(&mut self) -> Result, Error> { + pub fn historical_batch(&mut self) -> Result, BeaconStateError> { // Updating before cloning makes the clone cheap and saves repeated hashing. self.block_roots_mut().apply_updates()?; self.state_roots_mut().apply_updates()?; @@ -728,7 +820,10 @@ impl BeaconState { /// This method ensures the state's pubkey cache is fully up-to-date before checking if the validator /// exists in the registry. If a validator pubkey exists in the validator registry, returns `Some(i)`, /// otherwise returns `None`. - pub fn get_validator_index(&mut self, pubkey: &PublicKeyBytes) -> Result, Error> { + pub fn get_validator_index( + &mut self, + pubkey: &PublicKeyBytes, + ) -> Result, BeaconStateError> { self.update_pubkey_cache()?; Ok(self.pubkey_cache().get(pubkey)) } @@ -753,7 +848,7 @@ impl BeaconState { /// The epoch following `self.current_epoch()`. /// /// Spec v0.12.1 - pub fn next_epoch(&self) -> Result { + pub fn next_epoch(&self) -> Result { Ok(self.current_epoch().safe_add(1)?) } @@ -762,7 +857,7 @@ impl BeaconState { /// Makes use of the committee cache and will fail if no cache exists for the slot's epoch. /// /// Spec v0.12.1 - pub fn get_committee_count_at_slot(&self, slot: Slot) -> Result { + pub fn get_committee_count_at_slot(&self, slot: Slot) -> Result { let cache = self.committee_cache_at_slot(slot)?; Ok(cache.committees_per_slot()) } @@ -770,7 +865,10 @@ impl BeaconState { /// Compute the number of committees in an entire epoch. /// /// Spec v0.12.1 - pub fn get_epoch_committee_count(&self, relative_epoch: RelativeEpoch) -> Result { + pub fn get_epoch_committee_count( + &self, + relative_epoch: RelativeEpoch, + ) -> Result { let cache = self.committee_cache(relative_epoch)?; Ok(cache.epoch_committee_count() as u64) } @@ -783,7 +881,7 @@ impl BeaconState { pub fn get_cached_active_validator_indices( &self, relative_epoch: RelativeEpoch, - ) -> Result<&[usize], Error> { + ) -> Result<&[usize], BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; Ok(cache.active_validator_indices()) @@ -796,7 +894,7 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { if epoch >= self.compute_activation_exit_epoch(self.current_epoch(), spec)? { Err(BeaconStateError::EpochOutOfBounds) } else { @@ -809,7 +907,10 @@ impl BeaconState { /// Note: the indices are shuffled (i.e., not in ascending order). /// /// Returns an error if that epoch is not cached, or the cache is not initialized. - pub fn get_shuffling(&self, relative_epoch: RelativeEpoch) -> Result<&[usize], Error> { + pub fn get_shuffling( + &self, + relative_epoch: RelativeEpoch, + ) -> Result<&[usize], BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; Ok(cache.shuffling()) @@ -824,14 +925,14 @@ impl BeaconState { &self, slot: Slot, index: CommitteeIndex, - ) -> Result { + ) -> Result, BeaconStateError> { let epoch = slot.epoch(E::slots_per_epoch()); let relative_epoch = RelativeEpoch::from_epoch(self.current_epoch(), epoch)?; let cache = self.committee_cache(relative_epoch)?; cache .get_beacon_committee(slot, index) - .ok_or(Error::NoCommittee { slot, index }) + .ok_or(BeaconStateError::NoCommittee { slot, index }) } /// Get all of the Beacon committees at a given slot. @@ -839,7 +940,10 @@ impl BeaconState { /// Utilises the committee cache. /// /// Spec v0.12.1 - pub fn get_beacon_committees_at_slot(&self, slot: Slot) -> Result, Error> { + pub fn get_beacon_committees_at_slot( + &self, + slot: Slot, + ) -> Result>, BeaconStateError> { let cache = self.committee_cache_at_slot(slot)?; cache.get_beacon_committees_at_slot(slot) } @@ -852,7 +956,7 @@ impl BeaconState { pub fn get_beacon_committees_at_epoch( &self, relative_epoch: RelativeEpoch, - ) -> Result, Error> { + ) -> Result>, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; cache.get_all_beacon_committees() } @@ -864,14 +968,14 @@ impl BeaconState { &self, slot: Slot, spec: &ChainSpec, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { let epoch = slot.epoch(E::slots_per_epoch()); let current_epoch = self.current_epoch(); let next_epoch = current_epoch.safe_add(1)?; // TODO(focil) review this logic if epoch != current_epoch && epoch != next_epoch { - return Err(Error::SlotOutOfBounds); + return Err(BeaconStateError::SlotOutOfBounds); } let seed = self.get_seed(epoch, Domain::InclusionListCommittee, spec)?; @@ -893,15 +997,17 @@ impl BeaconState { seed.as_slice(), spec.shuffle_round_count, ) - .ok_or(Error::UnableToShuffle)?; + .ok_or(BeaconStateError::UnableToShuffle)?; let validator_index = *active_validator_indices .get(shuffled_index) - .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; il_committee_indices.push(validator_index as u64); i.safe_add_assign(1)?; } - Ok(InclusionListCommittee::::from(il_committee_indices)) + Ok(InclusionListCommittee::::from( + il_committee_indices.try_into()?, + )) } /// Returns the block root which decided the proposer shuffling for the epoch passed in parameter. This root @@ -914,8 +1020,9 @@ impl BeaconState { &self, epoch: Epoch, block_root: Hash256, - ) -> Result { - let decision_slot = self.proposer_shuffling_decision_slot(epoch); + spec: &ChainSpec, + ) -> Result { + let decision_slot = spec.proposer_shuffling_decision_slot::(epoch); if self.slot() <= decision_slot { Ok(block_root) } else { @@ -923,6 +1030,22 @@ impl BeaconState { } } + /// Returns the block root at the last slot of `epoch - 1`. + /// + /// This can be deleted after Glamsterdam and the removal of the v1 proposer duties endpoint. + pub fn legacy_proposer_shuffling_decision_root_at_epoch( + &self, + epoch: Epoch, + head_block_root: Hash256, + ) -> Result { + let decision_slot = epoch.saturating_sub(1u64).end_slot(E::slots_per_epoch()); + if self.slot() <= decision_slot { + Ok(head_block_root) + } else { + self.get_block_root(decision_slot).copied() + } + } + /// Returns the block root which decided the proposer shuffling for the current epoch. This root /// can be used to key this proposer shuffling. /// @@ -930,19 +1053,21 @@ impl BeaconState { /// /// The `block_root` covers the one-off scenario where the genesis block decides its own /// shuffling. It should be set to the latest block applied to `self` or the genesis block root. - pub fn proposer_shuffling_decision_root(&self, block_root: Hash256) -> Result { - let decision_slot = self.proposer_shuffling_decision_slot(self.current_epoch()); - if self.slot() == decision_slot { - Ok(block_root) - } else { - self.get_block_root(decision_slot).copied() - } + pub fn proposer_shuffling_decision_root( + &self, + block_root: Hash256, + spec: &ChainSpec, + ) -> Result { + self.proposer_shuffling_decision_root_at_epoch(self.current_epoch(), block_root, spec) } - /// Returns the slot at which the proposer shuffling was decided. The block root at this slot - /// can be used to key the proposer shuffling for the given epoch. - fn proposer_shuffling_decision_slot(&self, epoch: Epoch) -> Slot { - epoch.start_slot(E::slots_per_epoch()).saturating_sub(1_u64) + pub fn epoch_cache_decision_root( + &self, + block_root: Hash256, + ) -> Result { + // Epoch cache decision root for the current epoch (N) is the block root at the end of epoch + // N - 1. This is the same as the root that determines the next epoch attester shuffling. + self.attester_shuffling_decision_root(block_root, RelativeEpoch::Next) } /// Returns the block root which decided the attester shuffling for the given `relative_epoch`. @@ -956,7 +1081,7 @@ impl BeaconState { &self, block_root: Hash256, relative_epoch: RelativeEpoch, - ) -> Result { + ) -> Result { let decision_slot = self.attester_shuffling_decision_slot(relative_epoch); if self.slot() == decision_slot { Ok(block_root) @@ -983,9 +1108,9 @@ impl BeaconState { indices: &[usize], seed: &[u8], spec: &ChainSpec, - ) -> Result { + ) -> Result { if indices.is_empty() { - return Err(Error::InsufficientValidators); + return Err(BeaconStateError::InsufficientValidators); } let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); @@ -1003,10 +1128,10 @@ impl BeaconState { seed, spec.shuffle_round_count, ) - .ok_or(Error::UnableToShuffle)?; + .ok_or(BeaconStateError::UnableToShuffle)?; let candidate_index = *indices .get(shuffled_index) - .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; let random_value = self.shuffling_random_value(i, seed)?; let effective_balance = self.get_effective_balance(candidate_index)?; if effective_balance.safe_mul(max_random_value)? @@ -1018,12 +1143,72 @@ impl BeaconState { } } + // Vec is just much easier to work with here + fn compute_proposer_indices( + &self, + epoch: Epoch, + seed: &[u8], + indices: &[usize], + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + // Regardless of fork, we never support computing proposer indices for past epochs. + let current_epoch = self.current_epoch(); + if epoch < current_epoch { + return Err(BeaconStateError::ComputeProposerIndicesPastEpoch { + current_epoch, + request_epoch: epoch, + }); + } + + if spec.fork_name_at_epoch(epoch).fulu_enabled() { + // Post-Fulu we must never compute proposer indices using insufficient lookahead. This + // would be very dangerous as it would lead to conflicts between the *true* proposer as + // defined by `self.proposer_lookahead` and the output of this function. + // With MIN_SEED_LOOKAHEAD=1 (common config), this is equivalent to checking that the + // requested epoch is not the current epoch. + // + // We do not run this check if this function is called from `upgrade_to_fulu`, + // which runs *after* the slot is incremented, and needs to compute the proposer + // shuffling for the epoch that was just transitioned into. + if self.fork_name_unchecked().fulu_enabled() + && epoch < current_epoch.safe_add(spec.min_seed_lookahead)? + { + return Err( + BeaconStateError::ComputeProposerIndicesInsufficientLookahead { + current_epoch, + request_epoch: epoch, + }, + ); + } + } else { + // Pre-Fulu the situation is reversed, we *should not* compute proposer indices using + // too much lookahead. To do so would make us vulnerable to changes in the proposer + // indices caused by effective balance changes. + if epoch >= current_epoch.safe_add(spec.min_seed_lookahead)? { + return Err(BeaconStateError::ComputeProposerIndicesExcessiveLookahead { + current_epoch, + request_epoch: epoch, + }); + } + } + + epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| { + let mut preimage = seed.to_vec(); + preimage.append(&mut int_to_bytes8(slot.as_u64())); + let seed = hash(&preimage); + self.compute_proposer_index(indices, &seed, spec) + }) + .collect() + } + /// Fork-aware abstraction for the shuffling. /// /// In Electra and later, the random value is a 16-bit integer stored in a `u64`. /// /// Prior to Electra, the random value is an 8-bit integer stored in a `u64`. - fn shuffling_random_value(&self, i: usize, seed: &[u8]) -> Result { + fn shuffling_random_value(&self, i: usize, seed: &[u8]) -> Result { if self.fork_name_unchecked().electra_enabled() { Self::shuffling_random_u16_electra(i, seed).map(u64::from) } else { @@ -1034,35 +1219,39 @@ impl BeaconState { /// Get a random byte from the given `seed`. /// /// Used by the proposer & sync committee selection functions. - fn shuffling_random_byte(i: usize, seed: &[u8]) -> Result { + fn shuffling_random_byte(i: usize, seed: &[u8]) -> Result { let mut preimage = seed.to_vec(); preimage.append(&mut int_to_bytes8(i.safe_div(32)? as u64)); let index = i.safe_rem(32)?; hash(&preimage) .get(index) .copied() - .ok_or(Error::ShuffleIndexOutOfBounds(index)) + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(index)) } /// Get two random bytes from the given `seed`. /// /// This is used in place of `shuffling_random_byte` from Electra onwards. - fn shuffling_random_u16_electra(i: usize, seed: &[u8]) -> Result { + fn shuffling_random_u16_electra(i: usize, seed: &[u8]) -> Result { let mut preimage = seed.to_vec(); preimage.append(&mut int_to_bytes8(i.safe_div(16)? as u64)); let offset = i.safe_rem(16)?.safe_mul(2)?; hash(&preimage) .get(offset..offset.safe_add(2)?) - .ok_or(Error::ShuffleIndexOutOfBounds(offset))? + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(offset))? .try_into() .map(u16::from_le_bytes) - .map_err(|_| Error::ShuffleIndexOutOfBounds(offset)) + .map_err(|_| BeaconStateError::ShuffleIndexOutOfBounds(offset)) } /// Convenience accessor for the `execution_payload_header` as an `ExecutionPayloadHeaderRef`. - pub fn latest_execution_payload_header(&self) -> Result, Error> { + pub fn latest_execution_payload_header( + &self, + ) -> Result, BeaconStateError> { match self { - BeaconState::Base(_) | BeaconState::Altair(_) => Err(Error::IncorrectStateVariant), + BeaconState::Base(_) | BeaconState::Altair(_) => { + Err(BeaconStateError::IncorrectStateVariant) + } BeaconState::Bellatrix(state) => Ok(ExecutionPayloadHeaderRef::Bellatrix( &state.latest_execution_payload_header, )), @@ -1075,20 +1264,24 @@ impl BeaconState { BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRef::Electra( &state.latest_execution_payload_header, )), - BeaconState::Eip7805(state) => Ok(ExecutionPayloadHeaderRef::Eip7805( - &state.latest_execution_payload_header, - )), BeaconState::Fulu(state) => Ok(ExecutionPayloadHeaderRef::Fulu( &state.latest_execution_payload_header, )), + BeaconState::Eip7805(state) => Ok(ExecutionPayloadHeaderRef::Eip7805( + &state.latest_execution_payload_header, + )), + // TODO(EIP-7732): investigate calling functions + BeaconState::Gloas(_) => Err(BeaconStateError::IncorrectStateVariant), } } pub fn latest_execution_payload_header_mut( &mut self, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { match self { - BeaconState::Base(_) | BeaconState::Altair(_) => Err(Error::IncorrectStateVariant), + BeaconState::Base(_) | BeaconState::Altair(_) => { + Err(BeaconStateError::IncorrectStateVariant) + } BeaconState::Bellatrix(state) => Ok(ExecutionPayloadHeaderRefMut::Bellatrix( &mut state.latest_execution_payload_header, )), @@ -1101,12 +1294,14 @@ impl BeaconState { BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRefMut::Electra( &mut state.latest_execution_payload_header, )), - BeaconState::Eip7805(state) => Ok(ExecutionPayloadHeaderRefMut::Eip7805( - &mut state.latest_execution_payload_header, - )), BeaconState::Fulu(state) => Ok(ExecutionPayloadHeaderRefMut::Fulu( &mut state.latest_execution_payload_header, )), + BeaconState::Eip7805(state) => Ok(ExecutionPayloadHeaderRefMut::Eip7805( + &mut state.latest_execution_payload_header, + )), + // TODO(EIP-7732): investigate calling functions + BeaconState::Gloas(_) => Err(BeaconStateError::IncorrectStateVariant), } } @@ -1119,7 +1314,7 @@ impl BeaconState { index: CommitteeIndex, slot_signature: &Signature, spec: &ChainSpec, - ) -> Result { + ) -> Result { let committee = self.get_beacon_committee(slot, index)?; let modulo = std::cmp::max( 1, @@ -1130,7 +1325,7 @@ impl BeaconState { signature_hash .get(0..8) .and_then(|bytes| bytes.try_into().ok()) - .ok_or(Error::IsAggregatorOutOfBounds)?, + .ok_or(BeaconStateError::IsAggregatorOutOfBounds)?, ); Ok(signature_hash_int.safe_rem(modulo)? == 0) @@ -1138,43 +1333,78 @@ impl BeaconState { /// Returns the beacon proposer index for the `slot` in `self.current_epoch()`. /// - /// Spec v0.12.1 - pub fn get_beacon_proposer_index(&self, slot: Slot, spec: &ChainSpec) -> Result { + /// Spec v1.6.0-alpha.1 + pub fn get_beacon_proposer_index( + &self, + slot: Slot, + spec: &ChainSpec, + ) -> Result { // Proposer indices are only known for the current epoch, due to the dependence on the // effective balances of validators, which change at every epoch transition. let epoch = slot.epoch(E::slots_per_epoch()); + // TODO(EIP-7917): Explore allowing this function to be called with a slot one epoch in the future. if epoch != self.current_epoch() { - return Err(Error::SlotOutOfBounds); + return Err(BeaconStateError::SlotOutOfBounds); } - let seed = self.get_beacon_proposer_seed(slot, spec)?; - let indices = self.get_active_validator_indices(epoch, spec)?; + if let Ok(proposer_lookahead) = self.proposer_lookahead() { + // Post-Fulu + let index = slot.as_usize().safe_rem(E::slots_per_epoch() as usize)?; + proposer_lookahead + .get(index) + .ok_or(BeaconStateError::ProposerLookaheadOutOfBounds { i: index }) + .map(|index| *index as usize) + } else { + // Pre-Fulu + let seed = self.get_beacon_proposer_seed(slot, spec)?; + let indices = self.get_active_validator_indices(epoch, spec)?; - self.compute_proposer_index(&indices, &seed, spec) + self.compute_proposer_index(&indices, &seed, spec) + } } - /// Returns the beacon proposer index for each `slot` in `self.current_epoch()`. + /// Returns the beacon proposer index for each `slot` in `epoch`. /// - /// The returned `Vec` contains one proposer index for each slot. For example, if - /// `state.current_epoch() == 1`, then `vec[0]` refers to slot `32` and `vec[1]` refers to slot - /// `33`. It will always be the case that `vec.len() == SLOTS_PER_EPOCH`. - pub fn get_beacon_proposer_indices(&self, spec: &ChainSpec) -> Result, Error> { - // Not using the cached validator indices since they are shuffled. - let indices = self.get_active_validator_indices(self.current_epoch(), spec)?; + /// The returned `Vec` contains one proposer index for each slot in the epoch. + pub fn get_beacon_proposer_indices( + &self, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { + // This isn't in the spec, but we remove the footgun that is requesting the current epoch + // for a Fulu state. + if let Ok(proposer_lookahead) = self.proposer_lookahead() + && epoch >= self.current_epoch() + && epoch <= self.next_epoch()? + { + let slots_per_epoch = E::slots_per_epoch() as usize; + let start_offset = if epoch == self.current_epoch() { + 0 + } else { + slots_per_epoch + }; + return Ok(proposer_lookahead + .iter_from(start_offset)? + .take(slots_per_epoch) + .map(|x| *x as usize) + .collect()); + } - self.current_epoch() - .slot_iter(E::slots_per_epoch()) - .map(|slot| { - let seed = self.get_beacon_proposer_seed(slot, spec)?; - self.compute_proposer_index(&indices, &seed, spec) - }) - .collect() + // Not using the cached validator indices since they are shuffled. + let indices = self.get_active_validator_indices(epoch, spec)?; + + let preimage = self.get_seed(epoch, Domain::BeaconProposer, spec)?; + self.compute_proposer_indices(epoch, preimage.as_slice(), &indices, spec) } /// Compute the seed to use for the beacon proposer selection at the given `slot`. /// /// Spec v0.12.1 - pub fn get_beacon_proposer_seed(&self, slot: Slot, spec: &ChainSpec) -> Result, Error> { + pub fn get_beacon_proposer_seed( + &self, + slot: Slot, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { let epoch = slot.epoch(E::slots_per_epoch()); let mut preimage = self .get_seed(epoch, Domain::BeaconProposer, spec)? @@ -1189,7 +1419,7 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result<&Arc>, Error> { + ) -> Result<&Arc>, BeaconStateError> { let sync_committee_period = epoch.sync_committee_period(spec)?; let current_sync_committee_period = self.current_epoch().sync_committee_period(spec)?; let next_sync_committee_period = current_sync_committee_period.safe_add(1)?; @@ -1199,7 +1429,7 @@ impl BeaconState { } else if sync_committee_period == next_sync_committee_period { self.next_sync_committee() } else { - Err(Error::SyncCommitteeNotKnown { + Err(BeaconStateError::SyncCommitteeNotKnown { current_epoch: self.current_epoch(), epoch, }) @@ -1210,7 +1440,7 @@ impl BeaconState { pub fn get_sync_committee_indices( &mut self, sync_committee: &SyncCommittee, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { self.update_pubkey_cache()?; sync_committee .pubkeys @@ -1218,13 +1448,16 @@ impl BeaconState { .map(|pubkey| { self.pubkey_cache() .get(pubkey) - .ok_or(Error::PubkeyCacheInconsistent) + .ok_or(BeaconStateError::PubkeyCacheInconsistent) }) .collect() } /// Compute the sync committee indices for the next sync committee. - fn get_next_sync_committee_indices(&self, spec: &ChainSpec) -> Result, Error> { + fn get_next_sync_committee_indices( + &self, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { let epoch = self.current_epoch().safe_add(1)?; let active_validator_indices = self.get_active_validator_indices(epoch, spec)?; @@ -1247,10 +1480,10 @@ impl BeaconState { seed.as_slice(), spec.shuffle_round_count, ) - .ok_or(Error::UnableToShuffle)?; + .ok_or(BeaconStateError::UnableToShuffle)?; let candidate_index = *active_validator_indices .get(shuffled_index) - .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(shuffled_index))?; let random_value = self.shuffling_random_value(i, seed.as_slice())?; let effective_balance = self.get_validator(candidate_index)?.effective_balance; if effective_balance.safe_mul(max_random_value)? @@ -1264,7 +1497,10 @@ impl BeaconState { } /// Compute the next sync committee. - pub fn get_next_sync_committee(&self, spec: &ChainSpec) -> Result, Error> { + pub fn get_next_sync_committee( + &self, + spec: &ChainSpec, + ) -> Result, BeaconStateError> { let sync_committee_indices = self.get_next_sync_committee_indices(spec)?; let pubkeys = sync_committee_indices @@ -1273,7 +1509,7 @@ impl BeaconState { self.validators() .get(index) .map(|v| v.pubkey) - .ok_or(Error::UnknownValidator(index)) + .ok_or(BeaconStateError::UnknownValidator(index)) }) .collect::, _>>()?; let decompressed_pubkeys = pubkeys @@ -1297,7 +1533,7 @@ impl BeaconState { epoch: Epoch, validator_indices: &[u64], spec: &ChainSpec, - ) -> Result, Error>>, Error> { + ) -> Result, BeaconStateError>>, BeaconStateError> { let sync_committee = self.get_built_sync_committee(epoch, spec)?; Ok(validator_indices @@ -1332,7 +1568,7 @@ impl BeaconState { /// Safely obtains the index for latest block roots, given some `slot`. /// /// Spec v0.12.1 - fn get_latest_block_roots_index(&self, slot: Slot) -> Result { + fn get_latest_block_roots_index(&self, slot: Slot) -> Result { if slot < self.slot() && self.slot() <= slot.safe_add(self.block_roots().len() as u64)? { Ok(slot.as_usize().safe_rem(self.block_roots().len())?) } else { @@ -1352,7 +1588,7 @@ impl BeaconState { let i = self.get_latest_block_roots_index(slot)?; self.block_roots() .get(i) - .ok_or(Error::BlockRootsOutOfBounds(i)) + .ok_or(BeaconStateError::BlockRootsOutOfBounds(i)) } /// Return the block root at a recent `epoch`. @@ -1372,12 +1608,12 @@ impl BeaconState { *self .block_roots_mut() .get_mut(i) - .ok_or(Error::BlockRootsOutOfBounds(i))? = block_root; + .ok_or(BeaconStateError::BlockRootsOutOfBounds(i))? = block_root; Ok(()) } /// Fill `randao_mixes` with - pub fn fill_randao_mixes_with(&mut self, index_root: Hash256) -> Result<(), Error> { + pub fn fill_randao_mixes_with(&mut self, index_root: Hash256) -> Result<(), BeaconStateError> { *self.randao_mixes_mut() = Vector::from_elem(index_root)?; Ok(()) } @@ -1389,7 +1625,7 @@ impl BeaconState { &self, epoch: Epoch, allow_next_epoch: AllowNextEpoch, - ) -> Result { + ) -> Result { let current_epoch = self.current_epoch(); let len = E::EpochsPerHistoricalVector::to_u64(); @@ -1398,7 +1634,7 @@ impl BeaconState { { Ok(epoch.as_usize().safe_rem(len as usize)?) } else { - Err(Error::EpochOutOfBounds) + Err(BeaconStateError::EpochOutOfBounds) } } @@ -1414,7 +1650,11 @@ impl BeaconState { /// # Errors: /// /// See `Self::get_randao_mix`. - pub fn update_randao_mix(&mut self, epoch: Epoch, signature: &Signature) -> Result<(), Error> { + pub fn update_randao_mix( + &mut self, + epoch: Epoch, + signature: &Signature, + ) -> Result<(), BeaconStateError> { let i = epoch .as_usize() .safe_rem(E::EpochsPerHistoricalVector::to_usize())?; @@ -1424,36 +1664,36 @@ impl BeaconState { *self .randao_mixes_mut() .get_mut(i) - .ok_or(Error::RandaoMixesOutOfBounds(i))? = + .ok_or(BeaconStateError::RandaoMixesOutOfBounds(i))? = *self.get_randao_mix(epoch)? ^ signature_hash; Ok(()) } /// Return the randao mix at a recent ``epoch``. - pub fn get_randao_mix(&self, epoch: Epoch) -> Result<&Hash256, Error> { + pub fn get_randao_mix(&self, epoch: Epoch) -> Result<&Hash256, BeaconStateError> { let i = self.get_randao_mix_index(epoch, AllowNextEpoch::False)?; self.randao_mixes() .get(i) - .ok_or(Error::RandaoMixesOutOfBounds(i)) + .ok_or(BeaconStateError::RandaoMixesOutOfBounds(i)) } /// Set the randao mix at a recent ``epoch``. /// /// Spec v0.12.1 - pub fn set_randao_mix(&mut self, epoch: Epoch, mix: Hash256) -> Result<(), Error> { + pub fn set_randao_mix(&mut self, epoch: Epoch, mix: Hash256) -> Result<(), BeaconStateError> { let i = self.get_randao_mix_index(epoch, AllowNextEpoch::True)?; *self .randao_mixes_mut() .get_mut(i) - .ok_or(Error::RandaoMixesOutOfBounds(i))? = mix; + .ok_or(BeaconStateError::RandaoMixesOutOfBounds(i))? = mix; Ok(()) } /// Safely obtains the index for latest state roots, given some `slot`. /// /// Spec v0.12.1 - fn get_latest_state_roots_index(&self, slot: Slot) -> Result { + fn get_latest_state_roots_index(&self, slot: Slot) -> Result { if slot < self.slot() && self.slot() <= slot.safe_add(self.state_roots().len() as u64)? { Ok(slot.as_usize().safe_rem(self.state_roots().len())?) } else { @@ -1462,32 +1702,42 @@ impl BeaconState { } /// Gets the state root for some slot. - pub fn get_state_root(&self, slot: Slot) -> Result<&Hash256, Error> { + pub fn get_state_root(&self, slot: Slot) -> Result<&Hash256, BeaconStateError> { let i = self.get_latest_state_roots_index(slot)?; self.state_roots() .get(i) - .ok_or(Error::StateRootsOutOfBounds(i)) + .ok_or(BeaconStateError::StateRootsOutOfBounds(i)) + } + + /// Gets the state root for the start slot of some epoch. + pub fn get_state_root_at_epoch_start(&self, epoch: Epoch) -> Result { + self.get_state_root(epoch.start_slot(E::slots_per_epoch())) + .copied() } /// Gets the oldest (earliest slot) state root. - pub fn get_oldest_state_root(&self) -> Result<&Hash256, Error> { + pub fn get_oldest_state_root(&self) -> Result<&Hash256, BeaconStateError> { let oldest_slot = self.slot().saturating_sub(self.state_roots().len()); self.get_state_root(oldest_slot) } /// Gets the oldest (earliest slot) block root. - pub fn get_oldest_block_root(&self) -> Result<&Hash256, Error> { + pub fn get_oldest_block_root(&self) -> Result<&Hash256, BeaconStateError> { let oldest_slot = self.slot().saturating_sub(self.block_roots().len()); self.get_block_root(oldest_slot) } /// Sets the latest state root for slot. - pub fn set_state_root(&mut self, slot: Slot, state_root: Hash256) -> Result<(), Error> { + pub fn set_state_root( + &mut self, + slot: Slot, + state_root: Hash256, + ) -> Result<(), BeaconStateError> { let i = self.get_latest_state_roots_index(slot)?; *self .state_roots_mut() .get_mut(i) - .ok_or(Error::StateRootsOutOfBounds(i))? = state_root; + .ok_or(BeaconStateError::StateRootsOutOfBounds(i))? = state_root; Ok(()) } @@ -1496,7 +1746,7 @@ impl BeaconState { &self, epoch: Epoch, allow_next_epoch: AllowNextEpoch, - ) -> Result { + ) -> Result { // We allow the slashings vector to be accessed at any cached epoch at or before // the current epoch, or the next epoch if `AllowNextEpoch::True` is passed. let current_epoch = self.current_epoch(); @@ -1507,7 +1757,7 @@ impl BeaconState { .as_usize() .safe_rem(E::EpochsPerSlashingsVector::to_usize())?) } else { - Err(Error::EpochOutOfBounds) + Err(BeaconStateError::EpochOutOfBounds) } } @@ -1517,21 +1767,21 @@ impl BeaconState { } /// Get the total slashed balances for some epoch. - pub fn get_slashings(&self, epoch: Epoch) -> Result { + pub fn get_slashings(&self, epoch: Epoch) -> Result { let i = self.get_slashings_index(epoch, AllowNextEpoch::False)?; self.slashings() .get(i) .copied() - .ok_or(Error::SlashingsOutOfBounds(i)) + .ok_or(BeaconStateError::SlashingsOutOfBounds(i)) } /// Set the total slashed balances for some epoch. - pub fn set_slashings(&mut self, epoch: Epoch, value: u64) -> Result<(), Error> { + pub fn set_slashings(&mut self, epoch: Epoch, value: u64) -> Result<(), BeaconStateError> { let i = self.get_slashings_index(epoch, AllowNextEpoch::True)?; *self .slashings_mut() .get_mut(i) - .ok_or(Error::SlashingsOutOfBounds(i))? = value; + .ok_or(BeaconStateError::SlashingsOutOfBounds(i))? = value; Ok(()) } @@ -1571,10 +1821,10 @@ impl BeaconState { &mut ExitCache, &mut EpochCache, ), - Error, + BeaconStateError, > { match self { - BeaconState::Base(_) => Err(Error::IncorrectStateVariant), + BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), BeaconState::Altair(state) => Ok(( &mut state.validators, &mut state.balances, @@ -1645,22 +1895,35 @@ impl BeaconState { &mut state.exit_cache, &mut state.epoch_cache, )), + BeaconState::Gloas(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), } } /// Get the balance of a single validator. - pub fn get_balance(&self, validator_index: usize) -> Result { + pub fn get_balance(&self, validator_index: usize) -> Result { self.balances() .get(validator_index) - .ok_or(Error::BalancesOutOfBounds(validator_index)) + .ok_or(BeaconStateError::BalancesOutOfBounds(validator_index)) .copied() } /// Get a mutable reference to the balance of a single validator. - pub fn get_balance_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { + pub fn get_balance_mut( + &mut self, + validator_index: usize, + ) -> Result<&mut u64, BeaconStateError> { self.balances_mut() .get_mut(validator_index) - .ok_or(Error::BalancesOutOfBounds(validator_index)) + .ok_or(BeaconStateError::BalancesOutOfBounds(validator_index)) } /// Generate a seed for the given `epoch`. @@ -1669,7 +1932,7 @@ impl BeaconState { epoch: Epoch, domain_type: Domain, spec: &ChainSpec, - ) -> Result { + ) -> Result { // Bypass the safe getter for RANDAO so we can gracefully handle the scenario where `epoch // == 0`. let mix = { @@ -1680,7 +1943,7 @@ impl BeaconState { let i_mod = i.as_usize().safe_rem(self.randao_mixes().len())?; self.randao_mixes() .get(i_mod) - .ok_or(Error::RandaoMixesOutOfBounds(i_mod))? + .ok_or(BeaconStateError::RandaoMixesOutOfBounds(i_mod))? }; let domain_bytes = int_to_bytes4(spec.get_domain_constant(domain_type)); let epoch_bytes = int_to_bytes8(epoch.as_u64()); @@ -1699,17 +1962,20 @@ impl BeaconState { } /// Safe indexer for the `validators` list. - pub fn get_validator(&self, validator_index: usize) -> Result<&Validator, Error> { + pub fn get_validator(&self, validator_index: usize) -> Result<&Validator, BeaconStateError> { self.validators() .get(validator_index) - .ok_or(Error::UnknownValidator(validator_index)) + .ok_or(BeaconStateError::UnknownValidator(validator_index)) } /// Safe mutator for the `validators` list. - pub fn get_validator_mut(&mut self, validator_index: usize) -> Result<&mut Validator, Error> { + pub fn get_validator_mut( + &mut self, + validator_index: usize, + ) -> Result<&mut Validator, BeaconStateError> { self.validators_mut() .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index)) + .ok_or(BeaconStateError::UnknownValidator(validator_index)) } /// Add a validator to the registry and return the validator index that was allocated for it. @@ -1719,7 +1985,7 @@ impl BeaconState { withdrawal_credentials: Hash256, amount: u64, spec: &ChainSpec, - ) -> Result { + ) -> Result { let index = self.validators().len(); let fork_name = self.fork_name_unchecked(); self.validators_mut().push(Validator::from_deposit( @@ -1751,7 +2017,7 @@ impl BeaconState { if pubkey_cache.len() == index { let success = pubkey_cache.insert(pubkey, index); if !success { - return Err(Error::PubkeyCacheInconsistent); + return Err(BeaconStateError::PubkeyCacheInconsistent); } } @@ -1762,14 +2028,14 @@ impl BeaconState { pub fn get_validator_cow( &mut self, validator_index: usize, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { self.validators_mut() .get_cow(validator_index) - .ok_or(Error::UnknownValidator(validator_index)) + .ok_or(BeaconStateError::UnknownValidator(validator_index)) } /// Return the effective balance for a validator with the given `validator_index`. - pub fn get_effective_balance(&self, validator_index: usize) -> Result { + pub fn get_effective_balance(&self, validator_index: usize) -> Result { self.get_validator(validator_index) .map(|v| v.effective_balance) } @@ -1777,20 +2043,27 @@ impl BeaconState { /// Get the inactivity score for a single validator. /// /// Will error if the state lacks an `inactivity_scores` field. - pub fn get_inactivity_score(&self, validator_index: usize) -> Result { + pub fn get_inactivity_score(&self, validator_index: usize) -> Result { self.inactivity_scores()? .get(validator_index) .copied() - .ok_or(Error::InactivityScoresOutOfBounds(validator_index)) + .ok_or(BeaconStateError::InactivityScoresOutOfBounds( + validator_index, + )) } /// Get a mutable reference to the inactivity score for a single validator. /// /// Will error if the state lacks an `inactivity_scores` field. - pub fn get_inactivity_score_mut(&mut self, validator_index: usize) -> Result<&mut u64, Error> { + pub fn get_inactivity_score_mut( + &mut self, + validator_index: usize, + ) -> Result<&mut u64, BeaconStateError> { self.inactivity_scores_mut()? .get_mut(validator_index) - .ok_or(Error::InactivityScoresOutOfBounds(validator_index)) + .ok_or(BeaconStateError::InactivityScoresOutOfBounds( + validator_index, + )) } /// Return the epoch at which an activation or exit triggered in ``epoch`` takes effect. @@ -1800,14 +2073,14 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result { + ) -> Result { Ok(spec.compute_activation_exit_epoch(epoch)?) } /// Return the churn limit for the current epoch (number of validators who can leave per epoch). /// /// Uses the current epoch committee cache, and will error if it isn't initialized. - pub fn get_validator_churn_limit(&self, spec: &ChainSpec) -> Result { + pub fn get_validator_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(std::cmp::max( spec.min_per_epoch_churn_limit, (self @@ -1820,7 +2093,7 @@ impl BeaconState { /// Return the activation churn limit for the current epoch (number of validators who can enter per epoch). /// /// Uses the current epoch committee cache, and will error if it isn't initialized. - pub fn get_activation_churn_limit(&self, spec: &ChainSpec) -> Result { + pub fn get_activation_churn_limit(&self, spec: &ChainSpec) -> Result { Ok(match self { BeaconState::Base(_) | BeaconState::Altair(_) @@ -1828,8 +2101,9 @@ impl BeaconState { | BeaconState::Capella(_) => self.get_validator_churn_limit(spec)?, BeaconState::Deneb(_) | BeaconState::Electra(_) + | BeaconState::Fulu(_) | BeaconState::Eip7805(_) - | BeaconState::Fulu(_) => std::cmp::min( + | BeaconState::Gloas(_) => std::cmp::min( spec.max_per_epoch_activation_churn_limit, self.get_validator_churn_limit(spec)?, ), @@ -1846,7 +2120,7 @@ impl BeaconState { &self, validator_index: usize, relative_epoch: RelativeEpoch, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { let cache = self.committee_cache(relative_epoch)?; Ok(cache.get_attestation_duties(validator_index)) @@ -1858,7 +2132,7 @@ impl BeaconState { validator_index: usize, epoch: Epoch, spec: &ChainSpec, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { let validator_index = validator_index as u64; for slot in epoch.slot_iter(E::slots_per_epoch()) { let committee = self.get_inclusion_list_committee(slot, spec)?; @@ -1879,7 +2153,10 @@ impl BeaconState { /// /// This method should rarely be invoked because single-pass epoch processing keeps the total /// active balance cache up to date. - pub fn compute_total_active_balance_slow(&self, spec: &ChainSpec) -> Result { + pub fn compute_total_active_balance_slow( + &self, + spec: &ChainSpec, + ) -> Result { let current_epoch = self.current_epoch(); let mut total_active_balance = 0; @@ -1901,20 +2178,20 @@ impl BeaconState { /// the current committee cache is. /// /// Returns minimum `EFFECTIVE_BALANCE_INCREMENT`, to avoid div by 0. - pub fn get_total_active_balance(&self) -> Result { + pub fn get_total_active_balance(&self) -> Result { self.get_total_active_balance_at_epoch(self.current_epoch()) } /// Get the cached total active balance while checking that it is for the correct `epoch`. - pub fn get_total_active_balance_at_epoch(&self, epoch: Epoch) -> Result { + pub fn get_total_active_balance_at_epoch(&self, epoch: Epoch) -> Result { let (initialized_epoch, balance) = self .total_active_balance() - .ok_or(Error::TotalActiveBalanceCacheUninitialized)?; + .ok_or(BeaconStateError::TotalActiveBalanceCacheUninitialized)?; if initialized_epoch == epoch { Ok(balance) } else { - Err(Error::TotalActiveBalanceCacheInconsistent { + Err(BeaconStateError::TotalActiveBalanceCacheInconsistent { initialized_epoch, current_epoch: epoch, }) @@ -1933,7 +2210,11 @@ impl BeaconState { } /// Build the total active balance cache for the current epoch if it is not already built. - pub fn build_total_active_balance_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn build_total_active_balance_cache( + &mut self, + spec: &ChainSpec, + ) -> Result<(), BeaconStateError> { if self .get_total_active_balance_at_epoch(self.current_epoch()) .is_err() @@ -1947,7 +2228,7 @@ impl BeaconState { pub fn force_build_total_active_balance_cache( &mut self, spec: &ChainSpec, - ) -> Result<(), Error> { + ) -> Result<(), BeaconStateError> { let total_active_balance = self.compute_total_active_balance_slow(spec)?; *self.total_active_balance_mut() = Some((self.current_epoch(), total_active_balance)); Ok(()) @@ -1964,7 +2245,7 @@ impl BeaconState { epoch: Epoch, previous_epoch: Epoch, current_epoch: Epoch, - ) -> Result<&mut List, Error> { + ) -> Result<&mut List, BeaconStateError> { if epoch == current_epoch { match self { BeaconState::Base(_) => Err(BeaconStateError::IncorrectStateVariant), @@ -1973,8 +2254,9 @@ impl BeaconState { BeaconState::Capella(state) => Ok(&mut state.current_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.current_epoch_participation), BeaconState::Electra(state) => Ok(&mut state.current_epoch_participation), - BeaconState::Eip7805(state) => Ok(&mut state.current_epoch_participation), BeaconState::Fulu(state) => Ok(&mut state.current_epoch_participation), + BeaconState::Eip7805(state) => Ok(&mut state.current_epoch_participation), + BeaconState::Gloas(state) => Ok(&mut state.current_epoch_participation), } } else if epoch == previous_epoch { match self { @@ -1984,8 +2266,9 @@ impl BeaconState { BeaconState::Capella(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Electra(state) => Ok(&mut state.previous_epoch_participation), - BeaconState::Eip7805(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Fulu(state) => Ok(&mut state.previous_epoch_participation), + BeaconState::Eip7805(state) => Ok(&mut state.previous_epoch_participation), + BeaconState::Gloas(state) => Ok(&mut state.previous_epoch_participation), } } else { Err(BeaconStateError::EpochOutOfBounds) @@ -1993,7 +2276,8 @@ impl BeaconState { } /// Build all caches (except the tree hash cache), if they need to be built. - pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { self.build_all_committee_caches(spec)?; self.update_pubkey_cache()?; self.build_exit_cache(spec)?; @@ -2003,7 +2287,8 @@ impl BeaconState { } /// Build all committee caches, if they need to be built. - pub fn build_all_committee_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn build_all_committee_caches(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { self.build_committee_cache(RelativeEpoch::Previous, spec)?; self.build_committee_cache(RelativeEpoch::Current, spec)?; self.build_committee_cache(RelativeEpoch::Next, spec)?; @@ -2011,7 +2296,8 @@ impl BeaconState { } /// Build the exit cache, if it needs to be built. - pub fn build_exit_cache(&mut self, spec: &ChainSpec) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn build_exit_cache(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { if self.exit_cache().check_initialized().is_err() { *self.exit_cache_mut() = ExitCache::new(self.validators(), spec)?; } @@ -2019,7 +2305,8 @@ impl BeaconState { } /// Build the slashings cache if it needs to be built. - pub fn build_slashings_cache(&mut self) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn build_slashings_cache(&mut self) -> Result<(), BeaconStateError> { let latest_block_slot = self.latest_block_header().slot; if !self.slashings_cache().is_initialized(latest_block_slot) { *self.slashings_cache_mut() = SlashingsCache::new(latest_block_slot, self.validators()); @@ -2033,7 +2320,7 @@ impl BeaconState { } /// Drop all caches on the state. - pub fn drop_all_caches(&mut self) -> Result<(), Error> { + pub fn drop_all_caches(&mut self) -> Result<(), BeaconStateError> { self.drop_total_active_balance_cache(); self.drop_committee_cache(RelativeEpoch::Previous)?; self.drop_committee_cache(RelativeEpoch::Current)?; @@ -2056,11 +2343,12 @@ impl BeaconState { } /// Build a committee cache, unless it is has already been built. + #[instrument(skip_all, level = "debug")] pub fn build_committee_cache( &mut self, relative_epoch: RelativeEpoch, spec: &ChainSpec, - ) -> Result<(), Error> { + ) -> Result<(), BeaconStateError> { let i = Self::committee_cache_index(relative_epoch); let is_initialized = self .committee_cache_at_index(i)? @@ -2081,7 +2369,7 @@ impl BeaconState { &mut self, relative_epoch: RelativeEpoch, spec: &ChainSpec, - ) -> Result<(), Error> { + ) -> Result<(), BeaconStateError> { let epoch = relative_epoch.into_epoch(self.current_epoch()); let i = Self::committee_cache_index(relative_epoch); @@ -2097,7 +2385,7 @@ impl BeaconState { &self, epoch: Epoch, spec: &ChainSpec, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { CommitteeCache::initialized(self, epoch, spec) } @@ -2107,7 +2395,7 @@ impl BeaconState { /// /// Note: this function will not build any new committee caches, nor will it update the total /// active balance cache. The total active balance cache must be updated separately. - pub fn advance_caches(&mut self) -> Result<(), Error> { + pub fn advance_caches(&mut self) -> Result<(), BeaconStateError> { self.committee_caches_mut().rotate_left(1); let next = Self::committee_cache_index(RelativeEpoch::Next); @@ -2123,30 +2411,52 @@ impl BeaconState { } } + pub fn is_parent_block_full(&self) -> bool { + match self { + BeaconState::Base(_) | BeaconState::Altair(_) => false, + // TODO(EIP-7732): check the implications of this when we get to forkchoice modifications + BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) + | BeaconState::Eip7805(_) => true, + BeaconState::Gloas(state) => { + state.latest_execution_payload_bid.block_hash == state.latest_block_hash + } + } + } + /// Get the committee cache for some `slot`. /// /// Return an error if the cache for the slot's epoch is not initialized. - fn committee_cache_at_slot(&self, slot: Slot) -> Result<&Arc, Error> { + fn committee_cache_at_slot( + &self, + slot: Slot, + ) -> Result<&Arc, BeaconStateError> { let epoch = slot.epoch(E::slots_per_epoch()); let relative_epoch = RelativeEpoch::from_epoch(self.current_epoch(), epoch)?; self.committee_cache(relative_epoch) } /// Get the committee cache at a given index. - fn committee_cache_at_index(&self, index: usize) -> Result<&Arc, Error> { + fn committee_cache_at_index( + &self, + index: usize, + ) -> Result<&Arc, BeaconStateError> { self.committee_caches() .get(index) - .ok_or(Error::CommitteeCachesOutOfBounds(index)) + .ok_or(BeaconStateError::CommitteeCachesOutOfBounds(index)) } /// Get a mutable reference to the committee cache at a given index. fn committee_cache_at_index_mut( &mut self, index: usize, - ) -> Result<&mut Arc, Error> { + ) -> Result<&mut Arc, BeaconStateError> { self.committee_caches_mut() .get_mut(index) - .ok_or(Error::CommitteeCachesOutOfBounds(index)) + .ok_or(BeaconStateError::CommitteeCachesOutOfBounds(index)) } /// Returns the cache for some `RelativeEpoch`. Returns an error if the cache has not been @@ -2154,19 +2464,24 @@ impl BeaconState { pub fn committee_cache( &self, relative_epoch: RelativeEpoch, - ) -> Result<&Arc, Error> { + ) -> Result<&Arc, BeaconStateError> { let i = Self::committee_cache_index(relative_epoch); let cache = self.committee_cache_at_index(i)?; if cache.is_initialized_at(relative_epoch.into_epoch(self.current_epoch())) { Ok(cache) } else { - Err(Error::CommitteeCacheUninitialized(Some(relative_epoch))) + Err(BeaconStateError::CommitteeCacheUninitialized(Some( + relative_epoch, + ))) } } /// Drops the cache, leaving it in an uninitialized state. - pub fn drop_committee_cache(&mut self, relative_epoch: RelativeEpoch) -> Result<(), Error> { + pub fn drop_committee_cache( + &mut self, + relative_epoch: RelativeEpoch, + ) -> Result<(), BeaconStateError> { *self.committee_cache_at_index_mut(Self::committee_cache_index(relative_epoch))? = Arc::new(CommitteeCache::default()); Ok(()) @@ -2176,7 +2491,8 @@ impl BeaconState { /// /// Adds all `pubkeys` from the `validators` which are not already in the cache. Will /// never re-add a pubkey. - pub fn update_pubkey_cache(&mut self) -> Result<(), Error> { + #[instrument(skip_all, level = "debug")] + pub fn update_pubkey_cache(&mut self) -> Result<(), BeaconStateError> { let mut pubkey_cache = mem::take(self.pubkey_cache_mut()); let start_index = pubkey_cache.len(); @@ -2184,7 +2500,7 @@ impl BeaconState { let index = start_index.safe_add(i)?; let success = pubkey_cache.insert(validator.pubkey, index); if !success { - return Err(Error::PubkeyCacheInconsistent); + return Err(BeaconStateError::PubkeyCacheInconsistent); } } *self.pubkey_cache_mut() = pubkey_cache; @@ -2252,6 +2568,11 @@ impl BeaconState { any_pending_mutations |= self_field.has_pending_updates(); }); } + Self::Gloas(self_inner) => { + map_beacon_state_gloas_tree_list_fields_immutable!(self_inner, |_, self_field| { + any_pending_mutations |= self_field.has_pending_updates(); + }); + } }; any_pending_mutations } @@ -2264,7 +2585,8 @@ impl BeaconState { /// Compute the tree hash root of the state using the tree hash cache. /// /// Initialize the tree hash cache if it isn't already initialized. - pub fn update_tree_hash_cache<'a>(&'a mut self) -> Result { + #[instrument(skip_all, level = "debug")] + pub fn update_tree_hash_cache<'a>(&'a mut self) -> Result { self.apply_pending_mutations()?; map_beacon_state_ref!(&'a _, self.to_ref(), |inner, cons| { let root = inner.tree_hash_root(); @@ -2276,7 +2598,7 @@ impl BeaconState { /// Compute the tree hash root of the validators using the tree hash cache. /// /// Initialize the tree hash cache if it isn't already initialized. - pub fn update_validators_tree_hash_cache(&mut self) -> Result { + pub fn update_validators_tree_hash_cache(&mut self) -> Result { self.validators_mut().apply_updates()?; Ok(self.validators().tree_hash_root()) } @@ -2287,7 +2609,7 @@ impl BeaconState { &self, previous_epoch: Epoch, val: &Validator, - ) -> Result { + ) -> Result { Ok(val.is_active_at(previous_epoch) || (val.slashed && previous_epoch.safe_add(Epoch::new(1))? < val.withdrawable_epoch)) } @@ -2311,7 +2633,7 @@ impl BeaconState { pub fn get_sync_committee_for_next_slot( &self, spec: &ChainSpec, - ) -> Result>, Error> { + ) -> Result>, BeaconStateError> { let next_slot_epoch = self .slot() .saturating_add(Slot::new(1)) @@ -2337,7 +2659,7 @@ impl BeaconState { // ******* Electra accessors ******* /// Return the churn limit for the current epoch. - pub fn get_balance_churn_limit(&self, spec: &ChainSpec) -> Result { + pub fn get_balance_churn_limit(&self, spec: &ChainSpec) -> Result { let total_active_balance = self.get_total_active_balance()?; let churn = std::cmp::max( spec.min_per_epoch_churn_limit_electra, @@ -2348,20 +2670,26 @@ impl BeaconState { } /// Return the churn limit for the current epoch dedicated to activations and exits. - pub fn get_activation_exit_churn_limit(&self, spec: &ChainSpec) -> Result { + pub fn get_activation_exit_churn_limit( + &self, + spec: &ChainSpec, + ) -> Result { Ok(std::cmp::min( spec.max_per_epoch_activation_exit_churn_limit, self.get_balance_churn_limit(spec)?, )) } - pub fn get_consolidation_churn_limit(&self, spec: &ChainSpec) -> Result { + pub fn get_consolidation_churn_limit(&self, spec: &ChainSpec) -> Result { self.get_balance_churn_limit(spec)? .safe_sub(self.get_activation_exit_churn_limit(spec)?) .map_err(Into::into) } - pub fn get_pending_balance_to_withdraw(&self, validator_index: usize) -> Result { + pub fn get_pending_balance_to_withdraw( + &self, + validator_index: usize, + ) -> Result { let mut pending_balance = 0; for withdrawal in self .pending_partial_withdrawals()? @@ -2379,11 +2707,11 @@ impl BeaconState { &mut self, validator_index: usize, spec: &ChainSpec, - ) -> Result<(), Error> { + ) -> Result<(), BeaconStateError> { let balance = self .balances_mut() .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; if *balance > spec.min_activation_balance { let excess_balance = balance.safe_sub(spec.min_activation_balance)?; *balance = spec.min_activation_balance; @@ -2404,11 +2732,11 @@ impl BeaconState { &mut self, validator_index: usize, spec: &ChainSpec, - ) -> Result<(), Error> { + ) -> Result<(), BeaconStateError> { let validator = self .validators_mut() .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; AsMut::<[u8; 32]>::as_mut(&mut validator.withdrawal_credentials)[0] = spec.compounding_withdrawal_prefix_byte; @@ -2420,7 +2748,7 @@ impl BeaconState { &mut self, exit_balance: u64, spec: &ChainSpec, - ) -> Result { + ) -> Result { let mut earliest_exit_epoch = std::cmp::max( self.earliest_exit_epoch()?, self.compute_activation_exit_epoch(self.current_epoch(), spec)?, @@ -2450,8 +2778,11 @@ impl BeaconState { | BeaconState::Altair(_) | BeaconState::Bellatrix(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), - BeaconState::Electra(_) | BeaconState::Eip7805(_) | BeaconState::Fulu(_) => { + | BeaconState::Deneb(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Electra(_) + | BeaconState::Fulu(_) + | BeaconState::Eip7805(_) + | BeaconState::Gloas(_) => { // Consume the balance and update state variables *self.exit_balance_to_consume_mut()? = exit_balance_to_consume.safe_sub(exit_balance)?; @@ -2465,7 +2796,7 @@ impl BeaconState { &mut self, consolidation_balance: u64, spec: &ChainSpec, - ) -> Result { + ) -> Result { let mut earliest_consolidation_epoch = std::cmp::max( self.earliest_consolidation_epoch()?, self.compute_activation_exit_epoch(self.current_epoch(), spec)?, @@ -2497,8 +2828,11 @@ impl BeaconState { | BeaconState::Altair(_) | BeaconState::Bellatrix(_) | BeaconState::Capella(_) - | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), - BeaconState::Electra(_) | BeaconState::Eip7805(_) | BeaconState::Fulu(_) => { + | BeaconState::Deneb(_) => Err(BeaconStateError::IncorrectStateVariant), + BeaconState::Electra(_) + | BeaconState::Fulu(_) + | BeaconState::Eip7805(_) + | BeaconState::Gloas(_) => { // Consume the balance and update state variables. *self.consolidation_balance_to_consume_mut()? = consolidation_balance_to_consume.safe_sub(consolidation_balance)?; @@ -2509,7 +2843,7 @@ impl BeaconState { } #[allow(clippy::arithmetic_side_effects)] - pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), Error> { + pub fn rebase_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), BeaconStateError> { // Required for macros (which use type-hints internally). match (&mut *self, base) { @@ -2561,14 +2895,6 @@ impl BeaconState { ); } (Self::Electra(_), _) => (), - (Self::Eip7805(self_inner), Self::Eip7805(base_inner)) => { - bimap_beacon_state_eip7805_tree_list_fields!( - self_inner, - base_inner, - |_, self_field, base_field| { self_field.rebase_on(base_field) } - ); - } - (Self::Eip7805(_), _) => (), (Self::Fulu(self_inner), Self::Fulu(base_inner)) => { bimap_beacon_state_fulu_tree_list_fields!( self_inner, @@ -2577,22 +2903,36 @@ impl BeaconState { ); } (Self::Fulu(_), _) => (), + (Self::Eip7805(self_inner), Self::Eip7805(base_inner)) => { + bimap_beacon_state_eip7805_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Eip7805(_), _) => (), + (Self::Gloas(self_inner), Self::Gloas(base_inner)) => { + bimap_beacon_state_gloas_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Gloas(_), _) => (), } // Use sync committees from `base` if they are equal. - if let Ok(current_sync_committee) = self.current_sync_committee_mut() { - if let Ok(base_sync_committee) = base.current_sync_committee() { - if current_sync_committee == base_sync_committee { - *current_sync_committee = base_sync_committee.clone(); - } - } + if let Ok(current_sync_committee) = self.current_sync_committee_mut() + && let Ok(base_sync_committee) = base.current_sync_committee() + && current_sync_committee == base_sync_committee + { + *current_sync_committee = base_sync_committee.clone(); } - if let Ok(next_sync_committee) = self.next_sync_committee_mut() { - if let Ok(base_sync_committee) = base.next_sync_committee() { - if next_sync_committee == base_sync_committee { - *next_sync_committee = base_sync_committee.clone(); - } - } + if let Ok(next_sync_committee) = self.next_sync_committee_mut() + && let Ok(base_sync_committee) = base.next_sync_committee() + && next_sync_committee == base_sync_committee + { + *next_sync_committee = base_sync_committee.clone(); } // Rebase caches like the committee caches and the pubkey cache, which are expensive to @@ -2602,13 +2942,23 @@ impl BeaconState { Ok(()) } - pub fn rebase_caches_on(&mut self, base: &Self, spec: &ChainSpec) -> Result<(), Error> { + pub fn rebase_caches_on( + &mut self, + base: &Self, + spec: &ChainSpec, + ) -> Result<(), BeaconStateError> { // Use pubkey cache from `base` if it contains superior information (likely if our cache is - // uninitialized). + // uninitialized). Be careful not to use a cache which has *more* validators than expected, + // as other code expects `self.pubkey_cache().len() <= self.validators.len()`. let num_validators = self.validators().len(); let pubkey_cache = self.pubkey_cache_mut(); let base_pubkey_cache = base.pubkey_cache(); - if pubkey_cache.len() < base_pubkey_cache.len() && pubkey_cache.len() < num_validators { + + let current_cache_is_incomplete = pubkey_cache.len() < num_validators; + let base_cache_is_compatible = base_pubkey_cache.len() <= num_validators; + let base_cache_is_superior = base_pubkey_cache.len() > pubkey_cache.len(); + + if current_cache_is_incomplete && base_cache_is_compatible && base_cache_is_superior { *pubkey_cache = base_pubkey_cache.clone(); } @@ -2636,6 +2986,12 @@ impl BeaconState { } } +impl ForkVersionDecode for BeaconState { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + Ok(map_fork_name!(fork_name, Self, <_>::from_ssz_bytes(bytes)?)) + } +} + impl BeaconState { /// The number of fields of the `BeaconState` rounded up to the nearest power of two. /// @@ -2649,8 +3005,9 @@ impl BeaconState { ForkName::Capella => BeaconStateCapella::::NUM_FIELDS.next_power_of_two(), ForkName::Deneb => BeaconStateDeneb::::NUM_FIELDS.next_power_of_two(), ForkName::Electra => BeaconStateElectra::::NUM_FIELDS.next_power_of_two(), - ForkName::Eip7805 => BeaconStateEip7805::::NUM_FIELDS.next_power_of_two(), ForkName::Fulu => BeaconStateFulu::::NUM_FIELDS.next_power_of_two(), + ForkName::Eip7805 => BeaconStateEip7805::::NUM_FIELDS.next_power_of_two(), + ForkName::Gloas => BeaconStateGloas::::NUM_FIELDS.next_power_of_two(), } } @@ -2679,7 +3036,7 @@ impl BeaconState { } #[allow(clippy::arithmetic_side_effects)] - pub fn apply_pending_mutations(&mut self) -> Result<(), Error> { + pub fn apply_pending_mutations(&mut self) -> Result<(), BeaconStateError> { match self { Self::Base(inner) => { map_beacon_state_base_tree_list_fields!(inner, |_, x| { x.apply_updates() }) @@ -2705,47 +3062,50 @@ impl BeaconState { Self::Fulu(inner) => { map_beacon_state_fulu_tree_list_fields!(inner, |_, x| { x.apply_updates() }) } + Self::Gloas(inner) => { + map_beacon_state_gloas_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } } Ok(()) } - pub fn compute_current_sync_committee_proof(&self) -> Result, Error> { + pub fn compute_current_sync_committee_proof(&self) -> Result, BeaconStateError> { // Sync committees are top-level fields, subtract off the generalized indices // for the internal nodes. Result should be 22 or 23, the field offset of the committee // in the `BeaconState`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate let field_gindex = if self.fork_name_unchecked().electra_enabled() { - light_client_update::CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA + CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA } else { - light_client_update::CURRENT_SYNC_COMMITTEE_INDEX + CURRENT_SYNC_COMMITTEE_INDEX }; let field_index = field_gindex.safe_sub(self.num_fields_pow2())?; let leaves = self.get_beacon_state_leaves(); self.generate_proof(field_index, &leaves) } - pub fn compute_next_sync_committee_proof(&self) -> Result, Error> { + pub fn compute_next_sync_committee_proof(&self) -> Result, BeaconStateError> { // Sync committees are top-level fields, subtract off the generalized indices // for the internal nodes. Result should be 22 or 23, the field offset of the committee // in the `BeaconState`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate let field_gindex = if self.fork_name_unchecked().electra_enabled() { - light_client_update::NEXT_SYNC_COMMITTEE_INDEX_ELECTRA + NEXT_SYNC_COMMITTEE_INDEX_ELECTRA } else { - light_client_update::NEXT_SYNC_COMMITTEE_INDEX + NEXT_SYNC_COMMITTEE_INDEX }; let field_index = field_gindex.safe_sub(self.num_fields_pow2())?; let leaves = self.get_beacon_state_leaves(); self.generate_proof(field_index, &leaves) } - pub fn compute_finalized_root_proof(&self) -> Result, Error> { + pub fn compute_finalized_root_proof(&self) -> Result, BeaconStateError> { // Finalized root is the right child of `finalized_checkpoint`, divide by two to get // the generalized index of `state.finalized_checkpoint`. let checkpoint_root_gindex = if self.fork_name_unchecked().electra_enabled() { - light_client_update::FINALIZED_ROOT_INDEX_ELECTRA + FINALIZED_ROOT_INDEX_ELECTRA } else { - light_client_update::FINALIZED_ROOT_INDEX + FINALIZED_ROOT_INDEX }; let checkpoint_gindex = checkpoint_root_gindex / 2; @@ -2764,13 +3124,13 @@ impl BeaconState { Ok(proof) } - fn generate_proof( + pub fn generate_proof( &self, field_index: usize, leaves: &[Hash256], - ) -> Result, Error> { + ) -> Result, BeaconStateError> { if field_index >= leaves.len() { - return Err(Error::IndexNotSupported(field_index)); + return Err(BeaconStateError::IndexNotSupported(field_index)); } let depth = self.num_fields_pow2().ilog2() as usize; @@ -2779,7 +3139,7 @@ impl BeaconState { Ok(proof) } - fn get_beacon_state_leaves(&self) -> Vec { + pub fn get_beacon_state_leaves(&self) -> Vec { let mut leaves = vec![]; #[allow(clippy::arithmetic_side_effects)] match self { @@ -2823,51 +3183,56 @@ impl BeaconState { leaves.push(field.tree_hash_root()); }); } + BeaconState::Gloas(state) => { + map_beacon_state_gloas_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } }; leaves } } -impl From for Error { - fn from(e: RelativeEpochError) -> Error { - Error::RelativeEpochError(e) +impl From for BeaconStateError { + fn from(e: RelativeEpochError) -> BeaconStateError { + BeaconStateError::RelativeEpochError(e) } } -impl From for Error { - fn from(e: ssz_types::Error) -> Error { - Error::SszTypesError(e) +impl From for BeaconStateError { + fn from(e: ssz_types::Error) -> BeaconStateError { + BeaconStateError::SszTypesError(e) } } -impl From for Error { - fn from(e: bls::Error) -> Error { - Error::BlsError(e) +impl From for BeaconStateError { + fn from(e: bls::Error) -> BeaconStateError { + BeaconStateError::BlsError(e) } } -impl From for Error { - fn from(e: tree_hash::Error) -> Error { - Error::TreeHashError(e) +impl From for BeaconStateError { + fn from(e: tree_hash::Error) -> BeaconStateError { + BeaconStateError::TreeHashError(e) } } -impl From for Error { - fn from(e: merkle_proof::MerkleTreeError) -> Error { - Error::MerkleTreeError(e) +impl From for BeaconStateError { + fn from(e: merkle_proof::MerkleTreeError) -> BeaconStateError { + BeaconStateError::MerkleTreeError(e) } } -impl From for Error { - fn from(e: ArithError) -> Error { - Error::ArithError(e) +impl From for BeaconStateError { + fn from(e: ArithError) -> BeaconStateError { + BeaconStateError::ArithError(e) } } -impl From for Error { - fn from(e: milhouse::Error) -> Self { - Self::MilhouseError(e) +impl From for BeaconStateError { + fn from(e: milhouse::Error) -> BeaconStateError { + BeaconStateError::MilhouseError(e) } } @@ -2882,6 +3247,7 @@ impl CompareFields for BeaconState { (BeaconState::Electra(x), BeaconState::Electra(y)) => x.compare_fields(y), (BeaconState::Eip7805(x), BeaconState::Eip7805(y)) => x.compare_fields(y), (BeaconState::Fulu(x), BeaconState::Fulu(y)) => x.compare_fields(y), + (BeaconState::Gloas(x), BeaconState::Gloas(y)) => x.compare_fields(y), _ => panic!("compare_fields: mismatched state variants",), } } diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/state/committee_cache.rs similarity index 90% rename from consensus/types/src/beacon_state/committee_cache.rs rename to consensus/types/src/state/committee_cache.rs index 161f854157..15f6a4cd37 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/state/committee_cache.rs @@ -1,17 +1,20 @@ #![allow(clippy::arithmetic_side_effects)] -use crate::*; -use core::num::NonZeroUsize; -use derivative::Derivative; +use std::{num::NonZeroUsize, ops::Range, sync::Arc}; + +use educe::Educe; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; -use ssz::{four_byte_option_impl, Decode, DecodeError, Encode}; +use ssz::{Decode, DecodeError, Encode, four_byte_option_impl}; use ssz_derive::{Decode, Encode}; -use std::ops::Range; -use std::sync::Arc; use swap_or_not_shuffle::shuffle_list; -mod tests; +use crate::{ + attestation::{AttestationDuty, BeaconCommittee, CommitteeIndex}, + core::{ChainSpec, Domain, Epoch, EthSpec, Slot}, + state::{BeaconState, BeaconStateError}, + validator::Validator, +}; // Define "legacy" implementations of `Option`, `Option` which use four bytes // for encoding the union selector. @@ -20,13 +23,13 @@ four_byte_option_impl!(four_byte_option_non_zero_usize, NonZeroUsize); /// Computes and stores the shuffling for an epoch. Provides various getters to allow callers to /// read the committees for the given epoch. -#[derive(Derivative, Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] -#[derivative(PartialEq)] +#[derive(Educe, Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[educe(PartialEq)] pub struct CommitteeCache { #[ssz(with = "four_byte_option_epoch")] initialized_epoch: Option, shuffling: Vec, - #[derivative(PartialEq(compare_with = "compare_shuffling_positions"))] + #[educe(PartialEq(method(compare_shuffling_positions)))] shuffling_positions: Vec, committees_per_slot: u64, slots_per_epoch: u64, @@ -66,7 +69,7 @@ impl CommitteeCache { state: &BeaconState, epoch: Epoch, spec: &ChainSpec, - ) -> Result, Error> { + ) -> Result, BeaconStateError> { // Check that the cache is being built for an in-range epoch. // // We allow caches to be constructed for historic epochs, per: @@ -77,23 +80,23 @@ impl CommitteeCache { .saturating_sub(1u64); if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 { - return Err(Error::EpochOutOfBounds); + return Err(BeaconStateError::EpochOutOfBounds); } // May cause divide-by-zero errors. if E::slots_per_epoch() == 0 { - return Err(Error::ZeroSlotsPerEpoch); + return Err(BeaconStateError::ZeroSlotsPerEpoch); } // The use of `NonZeroUsize` reduces the maximum number of possible validators by one. if state.validators().len() == usize::MAX { - return Err(Error::TooManyValidators); + return Err(BeaconStateError::TooManyValidators); } let active_validator_indices = get_active_validator_indices(state.validators(), epoch); if active_validator_indices.is_empty() { - return Err(Error::InsufficientValidators); + return Err(BeaconStateError::InsufficientValidators); } let committees_per_slot = @@ -107,13 +110,14 @@ impl CommitteeCache { &seed[..], false, ) - .ok_or(Error::UnableToShuffle)?; + .ok_or(BeaconStateError::UnableToShuffle)?; let mut shuffling_positions = vec![<_>::default(); state.validators().len()]; for (i, &v) in shuffling.iter().enumerate() { *shuffling_positions .get_mut(v) - .ok_or(Error::ShuffleIndexOutOfBounds(v))? = NonZeroUsize::new(i + 1).into(); + .ok_or(BeaconStateError::ShuffleIndexOutOfBounds(v))? = + NonZeroUsize::new(i + 1).into(); } Ok(Arc::new(CommitteeCache { @@ -159,7 +163,7 @@ impl CommitteeCache { &self, slot: Slot, index: CommitteeIndex, - ) -> Option { + ) -> Option> { if self.initialized_epoch.is_none() || !self.is_initialized_at(slot.epoch(self.slots_per_epoch)) || index >= self.committees_per_slot @@ -185,24 +189,27 @@ impl CommitteeCache { /// Get all the Beacon committees at a given `slot`. /// /// Committees are sorted by ascending index order 0..committees_per_slot - pub fn get_beacon_committees_at_slot(&self, slot: Slot) -> Result, Error> { + pub fn get_beacon_committees_at_slot( + &self, + slot: Slot, + ) -> Result>, BeaconStateError> { if self.initialized_epoch.is_none() { - return Err(Error::CommitteeCacheUninitialized(None)); + return Err(BeaconStateError::CommitteeCacheUninitialized(None)); } (0..self.committees_per_slot()) .map(|index| { self.get_beacon_committee(slot, index) - .ok_or(Error::NoCommittee { slot, index }) + .ok_or(BeaconStateError::NoCommittee { slot, index }) }) .collect() } /// Returns all committees for `self.initialized_epoch`. - pub fn get_all_beacon_committees(&self) -> Result, Error> { + pub fn get_all_beacon_committees(&self) -> Result>, BeaconStateError> { let initialized_epoch = self .initialized_epoch - .ok_or(Error::CommitteeCacheUninitialized(None))?; + .ok_or(BeaconStateError::CommitteeCacheUninitialized(None))?; initialized_epoch.slot_iter(self.slots_per_epoch).try_fold( Vec::with_capacity(self.epoch_committee_count()), @@ -371,6 +378,7 @@ where active } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for CommitteeCache { fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self::default()) diff --git a/consensus/types/src/epoch_cache.rs b/consensus/types/src/state/epoch_cache.rs similarity index 84% rename from consensus/types/src/epoch_cache.rs rename to consensus/types/src/state/epoch_cache.rs index b447e9b71e..cdea0d143d 100644 --- a/consensus/types/src/epoch_cache.rs +++ b/consensus/types/src/state/epoch_cache.rs @@ -1,19 +1,30 @@ -use crate::{ActivationQueue, BeaconStateError, ChainSpec, Epoch, Hash256, Slot}; -use safe_arith::{ArithError, SafeArith}; use std::sync::Arc; +use safe_arith::{ArithError, SafeArith}; + +use crate::{ + core::{ChainSpec, Epoch, Hash256, Slot}, + state::{ActivationQueue, BeaconStateError}, +}; + /// Cache of values which are uniquely determined at the start of an epoch. /// /// The values are fixed with respect to the last block of the _prior_ epoch, which we refer -/// to as the "decision block". This cache is very similar to the `BeaconProposerCache` in that -/// beacon proposers are determined at exactly the same time as the values in this cache, so -/// the keys for the two caches are identical. -#[derive(Debug, PartialEq, Eq, Clone, Default, arbitrary::Arbitrary)] +/// to as the "decision block". +/// +/// Prior to Fulu this cache was similar to the `BeaconProposerCache` in that beacon proposers were +/// determined at exactly the same time as the values in this cache, so the keys for the two caches +/// were identical. +/// +/// Post-Fulu, we use a different key (the proposers have more lookahead). +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct EpochCache { inner: Option>, } -#[derive(Debug, PartialEq, Eq, Clone, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Clone)] struct Inner { /// Unique identifier for this cache, which can be used to check its validity before use /// with any `BeaconState`. @@ -30,7 +41,8 @@ struct Inner { effective_balance_increment: u64, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, arbitrary::Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub struct EpochCacheKey { pub epoch: Epoch, pub decision_block_root: Hash256, diff --git a/consensus/types/src/beacon_state/exit_cache.rs b/consensus/types/src/state/exit_cache.rs similarity index 96% rename from consensus/types/src/beacon_state/exit_cache.rs rename to consensus/types/src/state/exit_cache.rs index 0bb984b667..43809d1af0 100644 --- a/consensus/types/src/beacon_state/exit_cache.rs +++ b/consensus/types/src/state/exit_cache.rs @@ -1,7 +1,13 @@ -use super::{BeaconStateError, ChainSpec, Epoch, Validator}; -use safe_arith::SafeArith; use std::cmp::Ordering; +use safe_arith::SafeArith; + +use crate::{ + core::{ChainSpec, Epoch}, + state::BeaconStateError, + validator::Validator, +}; + /// Map from exit epoch to the number of validators with that exit epoch. #[derive(Debug, Default, Clone, PartialEq)] pub struct ExitCache { @@ -86,6 +92,7 @@ impl ExitCache { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for ExitCache { fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self::default()) diff --git a/consensus/types/src/historical_batch.rs b/consensus/types/src/state/historical_batch.rs similarity index 61% rename from consensus/types/src/historical_batch.rs rename to consensus/types/src/state/historical_batch.rs index 3a02810bba..0167d64f62 100644 --- a/consensus/types/src/historical_batch.rs +++ b/consensus/types/src/state/historical_batch.rs @@ -1,27 +1,25 @@ -use crate::test_utils::TestRandom; -use crate::*; - +use context_deserialize::context_deserialize; +use milhouse::Vector; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{EthSpec, Hash256}, + fork::ForkName, + test_utils::TestRandom, +}; + /// Historical block and state roots. /// /// Spec v0.12.1 -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[arbitrary(bound = "E: EthSpec")] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[context_deserialize(ForkName)] pub struct HistoricalBatch { #[test_random(default)] @@ -33,6 +31,7 @@ pub struct HistoricalBatch { #[cfg(test)] mod tests { use super::*; + use crate::core::MainnetEthSpec; pub type FoundationHistoricalBatch = HistoricalBatch; diff --git a/consensus/types/src/historical_summary.rs b/consensus/types/src/state/historical_summary.rs similarity index 79% rename from consensus/types/src/historical_summary.rs rename to consensus/types/src/state/historical_summary.rs index 7ad423dade..f520e46483 100644 --- a/consensus/types/src/historical_summary.rs +++ b/consensus/types/src/state/historical_summary.rs @@ -1,18 +1,24 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{BeaconState, EthSpec, ForkName, Hash256}; -use compare_fields_derive::CompareFields; +use compare_fields::CompareFields; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use crate::{ + core::{EthSpec, Hash256}, + fork::ForkName, + state::BeaconState, + test_utils::TestRandom, +}; + /// `HistoricalSummary` matches the components of the phase0 `HistoricalBatch` /// making the two hash_tree_root-compatible. This struct is introduced into the beacon state /// in the Capella hard fork. /// /// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#historicalsummary +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( Debug, PartialEq, @@ -27,7 +33,6 @@ use tree_hash_derive::TreeHash; Clone, Copy, Default, - arbitrary::Arbitrary, )] #[context_deserialize(ForkName)] pub struct HistoricalSummary { diff --git a/consensus/types/src/beacon_state/inclusion_list_cache.rs b/consensus/types/src/state/inclusion_list_cache.rs similarity index 91% rename from consensus/types/src/beacon_state/inclusion_list_cache.rs rename to consensus/types/src/state/inclusion_list_cache.rs index f73f10ef7a..bad5df9736 100644 --- a/consensus/types/src/beacon_state/inclusion_list_cache.rs +++ b/consensus/types/src/state/inclusion_list_cache.rs @@ -1,6 +1,4 @@ -use crate::Transactions; - -use super::{EthSpec, SignedInclusionList, Slot, Transaction}; +use crate::{EthSpec, SignedInclusionList, Slot, Transaction, Transactions}; use std::collections::{HashMap, HashSet}; use tracing::info; @@ -104,12 +102,7 @@ impl InclusionListCache { .iter() .cloned() .collect::>(); - Some(il.into()) - } -} - -impl arbitrary::Arbitrary<'_> for InclusionListCache { - fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { - Ok(Self::default()) + // TODO(eip7805) should return an error instead of None? + il.try_into().ok() } } diff --git a/consensus/types/src/beacon_state/iter.rs b/consensus/types/src/state/iter.rs similarity index 95% rename from consensus/types/src/beacon_state/iter.rs rename to consensus/types/src/state/iter.rs index d99c769e40..63f28d74c4 100644 --- a/consensus/types/src/beacon_state/iter.rs +++ b/consensus/types/src/state/iter.rs @@ -1,4 +1,7 @@ -use crate::*; +use crate::{ + core::{EthSpec, Hash256, Slot}, + state::{BeaconState, BeaconStateError}, +}; /// Returns an iterator across the past block roots of `state` in descending slot-order. /// @@ -28,7 +31,7 @@ impl<'a, E: EthSpec> BlockRootsIter<'a, E> { } impl Iterator for BlockRootsIter<'_, E> { - type Item = Result<(Slot, Hash256), Error>; + type Item = Result<(Slot, Hash256), BeaconStateError>; fn next(&mut self) -> Option { if self.prev > self.genesis_slot @@ -53,6 +56,7 @@ impl Iterator for BlockRootsIter<'_, E> { #[cfg(test)] mod test { use crate::*; + use fixed_bytes::FixedBytesExtended; type E = MinimalEthSpec; diff --git a/consensus/types/src/state/mod.rs b/consensus/types/src/state/mod.rs new file mode 100644 index 0000000000..770dc182fe --- /dev/null +++ b/consensus/types/src/state/mod.rs @@ -0,0 +1,37 @@ +mod activation_queue; +mod balance; +mod beacon_state; +#[macro_use] +mod committee_cache; +mod epoch_cache; +mod exit_cache; +mod historical_batch; +mod historical_summary; +mod inclusion_list_cache; +mod iter; +mod progressive_balances_cache; +mod pubkey_cache; +mod slashings_cache; + +pub use activation_queue::ActivationQueue; +pub use balance::Balance; +pub use beacon_state::{ + BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateBellatrix, BeaconStateCapella, + BeaconStateDeneb, BeaconStateEip7805, BeaconStateElectra, BeaconStateError, BeaconStateFulu, + BeaconStateGloas, BeaconStateHash, BeaconStateRef, CACHED_EPOCHS, +}; +pub use committee_cache::{ + CommitteeCache, compute_committee_index_in_epoch, compute_committee_range_in_epoch, + epoch_committee_count, get_active_validator_indices, +}; +pub use epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; +pub use exit_cache::ExitCache; +pub use historical_batch::HistoricalBatch; +pub use historical_summary::HistoricalSummary; +pub use inclusion_list_cache::InclusionListCache; +pub use iter::BlockRootsIter; +pub use progressive_balances_cache::{ + EpochTotalBalances, ProgressiveBalancesCache, is_progressive_balances_enabled, +}; +pub use pubkey_cache::PubkeyCache; +pub use slashings_cache::SlashingsCache; diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/state/progressive_balances_cache.rs similarity index 95% rename from consensus/types/src/beacon_state/progressive_balances_cache.rs rename to consensus/types/src/state/progressive_balances_cache.rs index 8e8a1a6aa9..1e4c311f9a 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/state/progressive_balances_cache.rs @@ -1,24 +1,29 @@ -use crate::beacon_state::balance::Balance; +#[cfg(feature = "arbitrary")] +use arbitrary::Arbitrary; +use safe_arith::SafeArith; + use crate::{ - consts::altair::{ + attestation::ParticipationFlags, + core::consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }, - BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, + core::{ChainSpec, Epoch, EthSpec}, + state::{Balance, BeaconState, BeaconStateError}, }; -use arbitrary::Arbitrary; -use safe_arith::SafeArith; /// This cache keeps track of the accumulated target attestation balance for the current & previous /// epochs. The cached values can be utilised by fork choice to calculate unrealized justification /// and finalization instead of converting epoch participation arrays to balances for each block we /// process. -#[derive(Default, Debug, PartialEq, Arbitrary, Clone)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Default, Debug, PartialEq, Clone)] pub struct ProgressiveBalancesCache { inner: Option, } -#[derive(Debug, PartialEq, Arbitrary, Clone)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, PartialEq, Clone)] struct Inner { pub current_epoch: Epoch, pub previous_epoch_cache: EpochTotalBalances, @@ -26,7 +31,8 @@ struct Inner { } /// Caches the participation values for one epoch (either the previous or current). -#[derive(PartialEq, Debug, Clone, Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(PartialEq, Debug, Clone)] pub struct EpochTotalBalances { /// Stores the sum of the balances for all validators in `self.unslashed_participating_indices` /// for all flags in `NUM_FLAG_INDICES`. diff --git a/consensus/types/src/beacon_state/pubkey_cache.rs b/consensus/types/src/state/pubkey_cache.rs similarity index 96% rename from consensus/types/src/beacon_state/pubkey_cache.rs rename to consensus/types/src/state/pubkey_cache.rs index d58dd7bc1d..e62fafb53a 100644 --- a/consensus/types/src/beacon_state/pubkey_cache.rs +++ b/consensus/types/src/state/pubkey_cache.rs @@ -1,4 +1,4 @@ -use crate::*; +use bls::PublicKeyBytes; use rpds::HashTrieMapSync as HashTrieMap; type ValidatorIndex = usize; @@ -43,6 +43,7 @@ impl PubkeyCache { } } +#[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for PubkeyCache { fn arbitrary(_u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self::default()) diff --git a/consensus/types/src/beacon_state/slashings_cache.rs b/consensus/types/src/state/slashings_cache.rs similarity index 87% rename from consensus/types/src/beacon_state/slashings_cache.rs rename to consensus/types/src/state/slashings_cache.rs index 45d8f7e212..b6ed583df8 100644 --- a/consensus/types/src/beacon_state/slashings_cache.rs +++ b/consensus/types/src/state/slashings_cache.rs @@ -1,12 +1,15 @@ -use crate::{BeaconStateError, Slot, Validator}; +#[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; use rpds::HashTrieSetSync as HashTrieSet; +use crate::{core::Slot, state::BeaconStateError, validator::Validator}; + /// Persistent (cheap to clone) cache of all slashed validator indices. -#[derive(Debug, Default, Clone, PartialEq, Arbitrary)] +#[cfg_attr(feature = "arbitrary", derive(Arbitrary))] +#[derive(Debug, Default, Clone, PartialEq)] pub struct SlashingsCache { latest_block_slot: Option, - #[arbitrary(default)] + #[cfg_attr(feature = "arbitrary", arbitrary(default))] slashed_validators: HashTrieSet, } diff --git a/consensus/types/src/contribution_and_proof.rs b/consensus/types/src/sync_committee/contribution_and_proof.rs similarity index 79% rename from consensus/types/src/contribution_and_proof.rs rename to consensus/types/src/sync_committee/contribution_and_proof.rs index e918beacb0..2a344b89de 100644 --- a/consensus/types/src/contribution_and_proof.rs +++ b/consensus/types/src/sync_committee/contribution_and_proof.rs @@ -1,29 +1,25 @@ -use super::{ - ChainSpec, EthSpec, Fork, ForkName, Hash256, SecretKey, Signature, SignedRoot, - SyncCommitteeContribution, SyncSelectionProof, -}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; +use bls::{SecretKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, EthSpec, Hash256, SignedRoot}, + fork::{Fork, ForkName}, + sync_committee::{SyncCommitteeContribution, SyncSelectionProof}, + test_utils::TestRandom, +}; + /// A Validators aggregate sync committee contribution and selection proof. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[context_deserialize(ForkName)] pub struct ContributionAndProof { /// The index of the validator that created the sync contribution. diff --git a/consensus/types/src/sync_committee/mod.rs b/consensus/types/src/sync_committee/mod.rs new file mode 100644 index 0000000000..5a75975fe0 --- /dev/null +++ b/consensus/types/src/sync_committee/mod.rs @@ -0,0 +1,25 @@ +mod contribution_and_proof; +mod signed_contribution_and_proof; +mod sync_aggregate; +mod sync_aggregator_selection_data; +mod sync_committee; +mod sync_committee_contribution; +mod sync_committee_message; +mod sync_committee_subscription; +mod sync_duty; +mod sync_selection_proof; +mod sync_subnet_id; + +pub use contribution_and_proof::ContributionAndProof; +pub use signed_contribution_and_proof::SignedContributionAndProof; +pub use sync_aggregate::{Error as SyncAggregateError, SyncAggregate}; +pub use sync_aggregator_selection_data::SyncAggregatorSelectionData; +pub use sync_committee::{Error as SyncCommitteeError, SyncCommittee}; +pub use sync_committee_contribution::{ + Error as SyncCommitteeContributionError, SyncCommitteeContribution, SyncContributionData, +}; +pub use sync_committee_message::SyncCommitteeMessage; +pub use sync_committee_subscription::SyncCommitteeSubscription; +pub use sync_duty::SyncDuty; +pub use sync_selection_proof::SyncSelectionProof; +pub use sync_subnet_id::{SyncSubnetId, sync_subnet_id_to_string}; diff --git a/consensus/types/src/signed_contribution_and_proof.rs b/consensus/types/src/sync_committee/signed_contribution_and_proof.rs similarity index 78% rename from consensus/types/src/signed_contribution_and_proof.rs rename to consensus/types/src/sync_committee/signed_contribution_and_proof.rs index 42115bfbc0..0027003b9f 100644 --- a/consensus/types/src/signed_contribution_and_proof.rs +++ b/consensus/types/src/sync_committee/signed_contribution_and_proof.rs @@ -1,30 +1,26 @@ -use super::{ - ChainSpec, ContributionAndProof, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, - Signature, SignedRoot, SyncCommitteeContribution, SyncSelectionProof, -}; -use crate::context_deserialize; -use crate::test_utils::TestRandom; +use bls::{SecretKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot}, + fork::{Fork, ForkName}, + sync_committee::{ContributionAndProof, SyncCommitteeContribution, SyncSelectionProof}, + test_utils::TestRandom, +}; + /// A Validators signed contribution proof to publish on the `sync_committee_contribution_and_proof` /// gossipsub topic. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[context_deserialize(ForkName)] pub struct SignedContributionAndProof { /// The `ContributionAndProof` that was signed. diff --git a/consensus/types/src/sync_aggregate.rs b/consensus/types/src/sync_committee/sync_aggregate.rs similarity index 83% rename from consensus/types/src/sync_aggregate.rs rename to consensus/types/src/sync_committee/sync_aggregate.rs index 4f810db22a..e5848aa22c 100644 --- a/consensus/types/src/sync_aggregate.rs +++ b/consensus/types/src/sync_committee/sync_aggregate.rs @@ -1,14 +1,20 @@ -use crate::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{AggregateSignature, BitVector, EthSpec, ForkName, SyncCommitteeContribution}; -use derivative::Derivative; +use bls::AggregateSignature; +use context_deserialize::context_deserialize; +use educe::Educe; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{EthSpec, consts::altair::SYNC_COMMITTEE_SUBNET_COUNT}, + fork::ForkName, + sync_committee::SyncCommitteeContribution, + test_utils::TestRandom, +}; + #[derive(Debug, PartialEq)] pub enum Error { SszTypesError(ssz_types::Error), @@ -21,22 +27,14 @@ impl From for Error { Error::ArithError(e) } } - -#[derive( - Debug, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - Derivative, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, Educe)] +#[educe(PartialEq, Hash(bound(E: EthSpec)))] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[context_deserialize(ForkName)] pub struct SyncAggregate { pub sync_committee_bits: BitVector, diff --git a/consensus/types/src/sync_aggregator_selection_data.rs b/consensus/types/src/sync_committee/sync_aggregator_selection_data.rs similarity index 61% rename from consensus/types/src/sync_aggregator_selection_data.rs rename to consensus/types/src/sync_committee/sync_aggregator_selection_data.rs index a61cd47d04..e905ca036b 100644 --- a/consensus/types/src/sync_aggregator_selection_data.rs +++ b/consensus/types/src/sync_committee/sync_aggregator_selection_data.rs @@ -1,23 +1,18 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{ForkName, SignedRoot, Slot}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{SignedRoot, Slot}, + fork::ForkName, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Hash, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Clone, Serialize, Deserialize, Hash, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct SyncAggregatorSelectionData { diff --git a/consensus/types/src/sync_committee.rs b/consensus/types/src/sync_committee/sync_committee.rs similarity index 89% rename from consensus/types/src/sync_committee.rs rename to consensus/types/src/sync_committee/sync_committee.rs index c7ec7bdcc3..5448411800 100644 --- a/consensus/types/src/sync_committee.rs +++ b/consensus/types/src/sync_committee/sync_committee.rs @@ -1,14 +1,16 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{EthSpec, FixedVector, ForkName, SyncSubnetId}; +use std::collections::HashMap; + use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use std::collections::HashMap; +use ssz_types::FixedVector; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::EthSpec, fork::ForkName, sync_committee::SyncSubnetId, test_utils::TestRandom}; + #[derive(Debug, PartialEq)] pub enum Error { ArithError(ArithError), @@ -25,20 +27,13 @@ impl From for Error { } } -#[derive( - Debug, - PartialEq, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[context_deserialize(ForkName)] pub struct SyncCommittee { pub pubkeys: FixedVector, diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee/sync_committee_contribution.rs similarity index 88% rename from consensus/types/src/sync_committee_contribution.rs rename to consensus/types/src/sync_committee/sync_committee_contribution.rs index e2ac414cfa..09376fbe5c 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee/sync_committee_contribution.rs @@ -1,12 +1,18 @@ -use super::{AggregateSignature, EthSpec, ForkName, SignedRoot}; -use crate::context_deserialize; -use crate::slot_data::SlotData; -use crate::{test_utils::TestRandom, BitVector, Hash256, Slot, SyncCommitteeMessage}; +use bls::AggregateSignature; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{EthSpec, Hash256, SignedRoot, Slot, SlotData}, + fork::ForkName, + sync_committee::SyncCommitteeMessage, + test_utils::TestRandom, +}; + #[derive(Debug, PartialEq)] pub enum Error { SszTypesError(ssz_types::Error), @@ -15,20 +21,13 @@ pub enum Error { } /// An aggregation of `SyncCommitteeMessage`s, used in creating a `SignedContributionAndProof`. -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - arbitrary::Arbitrary, +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") )] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[serde(bound = "E: EthSpec")] -#[arbitrary(bound = "E: EthSpec")] #[context_deserialize(ForkName)] pub struct SyncCommitteeContribution { pub slot: Slot, diff --git a/consensus/types/src/sync_committee_message.rs b/consensus/types/src/sync_committee/sync_committee_message.rs similarity index 79% rename from consensus/types/src/sync_committee_message.rs rename to consensus/types/src/sync_committee/sync_committee_message.rs index 4b442b3053..ed42555c43 100644 --- a/consensus/types/src/sync_committee_message.rs +++ b/consensus/types/src/sync_committee/sync_committee_message.rs @@ -1,27 +1,19 @@ -use crate::context_deserialize; -use crate::slot_data::SlotData; -use crate::test_utils::TestRandom; -use crate::{ - ChainSpec, Domain, EthSpec, Fork, ForkName, Hash256, SecretKey, Signature, SignedRoot, Slot, -}; +use bls::{SecretKey, Signature}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + core::{ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot, SlotData}, + fork::{Fork, ForkName}, + test_utils::TestRandom, +}; + /// The data upon which a `SyncCommitteeContribution` is based. -#[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, -)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] #[context_deserialize(ForkName)] pub struct SyncCommitteeMessage { pub slot: Slot, diff --git a/consensus/types/src/sync_committee_subscription.rs b/consensus/types/src/sync_committee/sync_committee_subscription.rs similarity index 96% rename from consensus/types/src/sync_committee_subscription.rs rename to consensus/types/src/sync_committee/sync_committee_subscription.rs index 8e040279d7..6365b015dd 100644 --- a/consensus/types/src/sync_committee_subscription.rs +++ b/consensus/types/src/sync_committee/sync_committee_subscription.rs @@ -1,7 +1,8 @@ -use crate::Epoch; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use crate::core::Epoch; + /// A sync committee subscription created when a validator subscribes to sync committee subnets to perform /// sync committee duties. #[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] diff --git a/consensus/types/src/sync_duty.rs b/consensus/types/src/sync_committee/sync_duty.rs similarity index 96% rename from consensus/types/src/sync_duty.rs rename to consensus/types/src/sync_committee/sync_duty.rs index 59fbc960db..773cc008f9 100644 --- a/consensus/types/src/sync_duty.rs +++ b/consensus/types/src/sync_committee/sync_duty.rs @@ -1,8 +1,13 @@ -use crate::{EthSpec, SyncCommittee, SyncSubnetId}; +use std::collections::HashSet; + use bls::PublicKeyBytes; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; + +use crate::{ + core::EthSpec, + sync_committee::{SyncCommittee, SyncSubnetId}, +}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SyncDuty { diff --git a/consensus/types/src/sync_selection_proof.rs b/consensus/types/src/sync_committee/sync_selection_proof.rs similarity index 85% rename from consensus/types/src/sync_selection_proof.rs rename to consensus/types/src/sync_committee/sync_selection_proof.rs index 4adb90b26e..723f0c06c9 100644 --- a/consensus/types/src/sync_selection_proof.rs +++ b/consensus/types/src/sync_committee/sync_selection_proof.rs @@ -1,17 +1,24 @@ -use crate::consts::altair::{ - SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, -}; -use crate::{ - ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, Signature, SignedRoot, Slot, - SyncAggregatorSelectionData, -}; -use ethereum_hashing::hash; -use safe_arith::{ArithError, SafeArith}; -use ssz::Encode; -use ssz_types::typenum::Unsigned; use std::cmp; -#[derive(arbitrary::Arbitrary, PartialEq, Debug, Clone)] +use bls::{PublicKey, SecretKey, Signature}; +use ethereum_hashing::hash; +use safe_arith::{ArithError, SafeArith}; +use serde::{Deserialize, Serialize}; +use ssz::Encode; +use typenum::Unsigned; + +use crate::{ + core::{ + ChainSpec, Domain, EthSpec, Hash256, SignedRoot, Slot, + consts::altair::{SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE}, + }, + fork::Fork, + sync_committee::SyncAggregatorSelectionData, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct SyncSelectionProof(Signature); impl SyncSelectionProof { @@ -105,8 +112,9 @@ impl From for SyncSelectionProof { #[cfg(test)] mod test { use super::*; - use crate::{FixedBytesExtended, MainnetEthSpec}; + use crate::MainnetEthSpec; use eth2_interop_keypairs::keypair; + use fixed_bytes::FixedBytesExtended; #[test] fn proof_sign_and_verify() { diff --git a/consensus/types/src/sync_subnet_id.rs b/consensus/types/src/sync_committee/sync_subnet_id.rs similarity index 85% rename from consensus/types/src/sync_subnet_id.rs rename to consensus/types/src/sync_committee/sync_subnet_id.rs index 245ac5a6c4..6cb11f6b03 100644 --- a/consensus/types/src/sync_subnet_id.rs +++ b/consensus/types/src/sync_committee/sync_subnet_id.rs @@ -1,13 +1,16 @@ //! Identifies each sync committee subnet by an integer identifier. -use crate::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; -use crate::EthSpec; +use std::{ + collections::HashSet, + fmt::{self, Display}, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; -use ssz_types::typenum::Unsigned; -use std::collections::HashSet; -use std::fmt::{self, Display}; -use std::ops::{Deref, DerefMut}; -use std::sync::LazyLock; +use typenum::Unsigned; + +use crate::core::{EthSpec, consts::altair::SYNC_COMMITTEE_SUBNET_COUNT}; static SYNC_SUBNET_ID_TO_STRING: LazyLock> = LazyLock::new(|| { let mut v = Vec::with_capacity(SYNC_COMMITTEE_SUBNET_COUNT as usize); @@ -18,7 +21,8 @@ static SYNC_SUBNET_ID_TO_STRING: LazyLock> = LazyLock::new(|| { v }); -#[derive(arbitrary::Arbitrary, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct SyncSubnetId(#[serde(with = "serde_utils::quoted_u64")] u64); diff --git a/consensus/types/src/test_utils/generate_deterministic_keypairs.rs b/consensus/types/src/test_utils/generate_deterministic_keypairs.rs index f30afda257..5ccd748c25 100644 --- a/consensus/types/src/test_utils/generate_deterministic_keypairs.rs +++ b/consensus/types/src/test_utils/generate_deterministic_keypairs.rs @@ -1,7 +1,8 @@ -use crate::*; +use std::path::PathBuf; + +use bls::Keypair; use eth2_interop_keypairs::{keypair, keypairs_from_yaml_file}; use rayon::prelude::*; -use std::path::PathBuf; use tracing::debug; /// Generates `validator_count` keypairs where the secret key is derived solely from the index of diff --git a/consensus/types/src/test_utils/generate_random_block_and_blobs.rs b/consensus/types/src/test_utils/generate_random_block_and_blobs.rs index cf240c3f1f..cf7b5df891 100644 --- a/consensus/types/src/test_utils/generate_random_block_and_blobs.rs +++ b/consensus/types/src/test_utils/generate_random_block_and_blobs.rs @@ -1,11 +1,16 @@ +use bls::Signature; +use kzg::{KzgCommitment, KzgProof}; use rand::Rng; -use kzg::{KzgCommitment, KzgProof}; - -use crate::beacon_block_body::KzgCommitments; -use crate::*; - -use super::*; +use crate::{ + block::{BeaconBlock, SignedBeaconBlock}, + core::{EthSpec, MainnetEthSpec}, + data::{Blob, BlobSidecar, BlobsList}, + execution::FullPayload, + fork::{ForkName, map_fork_name}, + kzg_ext::{KzgCommitments, KzgProofs}, + test_utils::TestRandom, +}; type BlobsBundle = (KzgCommitments, KzgProofs, BlobsList); @@ -72,12 +77,13 @@ pub fn generate_blobs(n_blobs: usize) -> Result, Stri #[cfg(test)] mod test { use super::*; - use rand::thread_rng; + use rand::rng; + use ssz_types::FixedVector; #[test] fn test_verify_blob_inclusion_proof() { let (_block, blobs) = - generate_rand_block_and_blobs::(ForkName::Deneb, 6, &mut thread_rng()); + generate_rand_block_and_blobs::(ForkName::Deneb, 2, &mut rng()); for blob in blobs { assert!(blob.verify_blob_sidecar_inclusion_proof()); } @@ -86,7 +92,7 @@ mod test { #[test] fn test_verify_blob_inclusion_proof_from_existing_proof() { let (block, mut blob_sidecars) = - generate_rand_block_and_blobs::(ForkName::Deneb, 1, &mut thread_rng()); + generate_rand_block_and_blobs::(ForkName::Deneb, 1, &mut rng()); let BlobSidecar { index, blob, @@ -115,10 +121,10 @@ mod test { #[test] fn test_verify_blob_inclusion_proof_invalid() { let (_block, blobs) = - generate_rand_block_and_blobs::(ForkName::Deneb, 6, &mut thread_rng()); + generate_rand_block_and_blobs::(ForkName::Deneb, 1, &mut rng()); for mut blob in blobs { - blob.kzg_commitment_inclusion_proof = FixedVector::random_for_test(&mut thread_rng()); + blob.kzg_commitment_inclusion_proof = FixedVector::random_for_test(&mut rng()); assert!(!blob.verify_blob_sidecar_inclusion_proof()); } } diff --git a/consensus/types/src/test_utils/macros.rs b/consensus/types/src/test_utils/macros.rs index 4fd7720689..662527f5a4 100644 --- a/consensus/types/src/test_utils/macros.rs +++ b/consensus/types/src/test_utils/macros.rs @@ -13,7 +13,7 @@ macro_rules! ssz_tests { ($type: ty) => { #[test] pub fn test_ssz_round_trip() { - use ssz::{ssz_encode, Decode}; + use ssz::{Decode, ssz_encode}; use $crate::test_utils::{SeedableRng, TestRandom, XorShiftRng}; let mut rng = XorShiftRng::from_seed([42; 16]); diff --git a/consensus/types/src/test_utils/mod.rs b/consensus/types/src/test_utils/mod.rs index 9599bcd364..c4409b4392 100644 --- a/consensus/types/src/test_utils/mod.rs +++ b/consensus/types/src/test_utils/mod.rs @@ -1,17 +1,5 @@ #![allow(clippy::arithmetic_side_effects)] -use std::fmt::Debug; - -pub use rand::{RngCore, SeedableRng}; -pub use rand_xorshift::XorShiftRng; - -pub use generate_deterministic_keypairs::generate_deterministic_keypair; -pub use generate_deterministic_keypairs::generate_deterministic_keypairs; -pub use generate_deterministic_keypairs::load_keypairs_from_yaml; -use ssz::{ssz_encode, Decode, Encode}; -pub use test_random::{test_random_instance, TestRandom}; -use tree_hash::TreeHash; - #[macro_use] mod macros; mod generate_deterministic_keypairs; @@ -19,6 +7,18 @@ mod generate_deterministic_keypairs; mod generate_random_block_and_blobs; mod test_random; +pub use generate_deterministic_keypairs::generate_deterministic_keypair; +pub use generate_deterministic_keypairs::generate_deterministic_keypairs; +pub use generate_deterministic_keypairs::load_keypairs_from_yaml; +pub use test_random::{TestRandom, test_random_instance}; + +pub use rand::{RngCore, SeedableRng}; +pub use rand_xorshift::XorShiftRng; + +use ssz::{Decode, Encode, ssz_encode}; +use std::fmt::Debug; +use tree_hash::TreeHash; + pub fn test_ssz_tree_hash_pair(v1: &T, v2: &U) where T: TreeHash + Encode + Decode + Debug + PartialEq, diff --git a/consensus/types/src/test_utils/test_random/address.rs b/consensus/types/src/test_utils/test_random/address.rs index 421801ce53..2f601cb91e 100644 --- a/consensus/types/src/test_utils/test_random/address.rs +++ b/consensus/types/src/test_utils/test_random/address.rs @@ -1,7 +1,7 @@ -use super::*; +use crate::{core::Address, test_utils::TestRandom}; impl TestRandom for Address { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let mut key_bytes = vec![0; 20]; rng.fill_bytes(&mut key_bytes); Address::from_slice(&key_bytes[..]) diff --git a/consensus/types/src/test_utils/test_random/aggregate_signature.rs b/consensus/types/src/test_utils/test_random/aggregate_signature.rs index 772f284431..f9f3dd9567 100644 --- a/consensus/types/src/test_utils/test_random/aggregate_signature.rs +++ b/consensus/types/src/test_utils/test_random/aggregate_signature.rs @@ -1,7 +1,9 @@ -use super::*; +use bls::{AggregateSignature, Signature}; + +use crate::test_utils::TestRandom; impl TestRandom for AggregateSignature { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let signature = Signature::random_for_test(rng); let mut aggregate_signature = AggregateSignature::infinity(); aggregate_signature.add_assign(&signature); diff --git a/consensus/types/src/test_utils/test_random/bitfield.rs b/consensus/types/src/test_utils/test_random/bitfield.rs index e335ac7fe8..762f41eb34 100644 --- a/consensus/types/src/test_utils/test_random/bitfield.rs +++ b/consensus/types/src/test_utils/test_random/bitfield.rs @@ -1,8 +1,11 @@ -use super::*; use smallvec::smallvec; +use ssz_types::{BitList, BitVector}; +use typenum::Unsigned; + +use crate::test_utils::TestRandom; impl TestRandom for BitList { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let initial_len = std::cmp::max(1, N::to_usize().div_ceil(8)); let mut raw_bytes = smallvec![0; initial_len]; rng.fill_bytes(&mut raw_bytes); @@ -23,7 +26,7 @@ impl TestRandom for BitList { } impl TestRandom for BitVector { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let mut raw_bytes = smallvec![0; std::cmp::max(1, N::to_usize().div_ceil(8))]; rng.fill_bytes(&mut raw_bytes); // If N isn't divisible by 8 diff --git a/consensus/types/src/test_utils/test_random/hash256.rs b/consensus/types/src/test_utils/test_random/hash256.rs index 21d443c0e2..4d7570fb55 100644 --- a/consensus/types/src/test_utils/test_random/hash256.rs +++ b/consensus/types/src/test_utils/test_random/hash256.rs @@ -1,7 +1,7 @@ -use super::*; +use crate::{core::Hash256, test_utils::TestRandom}; impl TestRandom for Hash256 { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let mut key_bytes = vec![0; 32]; rng.fill_bytes(&mut key_bytes); Hash256::from_slice(&key_bytes[..]) diff --git a/consensus/types/src/test_utils/test_random/kzg_commitment.rs b/consensus/types/src/test_utils/test_random/kzg_commitment.rs index a4030f2b6a..31e316a198 100644 --- a/consensus/types/src/test_utils/test_random/kzg_commitment.rs +++ b/consensus/types/src/test_utils/test_random/kzg_commitment.rs @@ -1,4 +1,6 @@ -use super::*; +use kzg::KzgCommitment; + +use crate::test_utils::TestRandom; impl TestRandom for KzgCommitment { fn random_for_test(rng: &mut impl rand::RngCore) -> Self { diff --git a/consensus/types/src/test_utils/test_random/kzg_proof.rs b/consensus/types/src/test_utils/test_random/kzg_proof.rs index 7e771ca566..4465d5ab39 100644 --- a/consensus/types/src/test_utils/test_random/kzg_proof.rs +++ b/consensus/types/src/test_utils/test_random/kzg_proof.rs @@ -1,8 +1,9 @@ -use super::*; -use kzg::BYTES_PER_COMMITMENT; +use kzg::{BYTES_PER_COMMITMENT, KzgProof}; + +use crate::test_utils::TestRandom; impl TestRandom for KzgProof { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let mut bytes = [0; BYTES_PER_COMMITMENT]; rng.fill_bytes(&mut bytes); Self(bytes) diff --git a/consensus/types/src/test_utils/test_random/mod.rs b/consensus/types/src/test_utils/test_random/mod.rs new file mode 100644 index 0000000000..41812593fa --- /dev/null +++ b/consensus/types/src/test_utils/test_random/mod.rs @@ -0,0 +1,15 @@ +mod address; +mod aggregate_signature; +mod bitfield; +mod hash256; +mod kzg_commitment; +mod kzg_proof; +mod public_key; +mod public_key_bytes; +mod secret_key; +mod signature; +mod signature_bytes; +mod test_random; +mod uint256; + +pub use test_random::{TestRandom, test_random_instance}; diff --git a/consensus/types/src/test_utils/test_random/public_key.rs b/consensus/types/src/test_utils/test_random/public_key.rs index d33e9ac704..9d287c23d7 100644 --- a/consensus/types/src/test_utils/test_random/public_key.rs +++ b/consensus/types/src/test_utils/test_random/public_key.rs @@ -1,7 +1,9 @@ -use super::*; +use bls::{PublicKey, SecretKey}; + +use crate::test_utils::TestRandom; impl TestRandom for PublicKey { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { SecretKey::random_for_test(rng).public_key() } } diff --git a/consensus/types/src/test_utils/test_random/public_key_bytes.rs b/consensus/types/src/test_utils/test_random/public_key_bytes.rs index 6e5cafc4f0..587c3baf8f 100644 --- a/consensus/types/src/test_utils/test_random/public_key_bytes.rs +++ b/consensus/types/src/test_utils/test_random/public_key_bytes.rs @@ -1,9 +1,9 @@ -use bls::PUBLIC_KEY_BYTES_LEN; +use bls::{PUBLIC_KEY_BYTES_LEN, PublicKey, PublicKeyBytes}; -use super::*; +use crate::test_utils::TestRandom; impl TestRandom for PublicKeyBytes { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { //50-50 chance for signature to be "valid" or invalid if bool::random_for_test(rng) { //valid signature diff --git a/consensus/types/src/test_utils/test_random/secret_key.rs b/consensus/types/src/test_utils/test_random/secret_key.rs index da1614aa24..a8295d968a 100644 --- a/consensus/types/src/test_utils/test_random/secret_key.rs +++ b/consensus/types/src/test_utils/test_random/secret_key.rs @@ -1,7 +1,9 @@ -use super::*; +use bls::SecretKey; + +use crate::test_utils::TestRandom; impl TestRandom for SecretKey { - fn random_for_test(_rng: &mut impl RngCore) -> Self { + fn random_for_test(_rng: &mut impl rand::RngCore) -> Self { // TODO: Not deterministic generation. Using `SecretKey::deserialize` results in // `BlstError(BLST_BAD_ENCODING)`, need to debug with blst source on what encoding expects. SecretKey::random() diff --git a/consensus/types/src/test_utils/test_random/signature.rs b/consensus/types/src/test_utils/test_random/signature.rs index 8bc0d71110..006aba9650 100644 --- a/consensus/types/src/test_utils/test_random/signature.rs +++ b/consensus/types/src/test_utils/test_random/signature.rs @@ -1,7 +1,9 @@ -use super::*; +use bls::Signature; + +use crate::test_utils::TestRandom; impl TestRandom for Signature { - fn random_for_test(_rng: &mut impl RngCore) -> Self { + fn random_for_test(_rng: &mut impl rand::RngCore) -> Self { // TODO: `SecretKey::random_for_test` does not return a deterministic signature. Since this // signature will not pass verification we could just return the generator point or the // generator point multiplied by a random scalar if we want disctint signatures. diff --git a/consensus/types/src/test_utils/test_random/signature_bytes.rs b/consensus/types/src/test_utils/test_random/signature_bytes.rs index 2117a48232..6992e57467 100644 --- a/consensus/types/src/test_utils/test_random/signature_bytes.rs +++ b/consensus/types/src/test_utils/test_random/signature_bytes.rs @@ -1,9 +1,9 @@ -use bls::SIGNATURE_BYTES_LEN; +use bls::{SIGNATURE_BYTES_LEN, Signature, SignatureBytes}; -use super::*; +use crate::test_utils::TestRandom; impl TestRandom for SignatureBytes { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { //50-50 chance for signature to be "valid" or invalid if bool::random_for_test(rng) { //valid signature diff --git a/consensus/types/src/test_utils/test_random.rs b/consensus/types/src/test_utils/test_random/test_random.rs similarity index 89% rename from consensus/types/src/test_utils/test_random.rs rename to consensus/types/src/test_utils/test_random/test_random.rs index 00355779d2..101fbec51b 100644 --- a/consensus/types/src/test_utils/test_random.rs +++ b/consensus/types/src/test_utils/test_random/test_random.rs @@ -1,23 +1,10 @@ -use crate::*; -use rand::RngCore; -use rand::SeedableRng; -use rand_xorshift::XorShiftRng; -use smallvec::{smallvec, SmallVec}; -use std::marker::PhantomData; -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; -mod address; -mod aggregate_signature; -mod bitfield; -mod hash256; -mod kzg_commitment; -mod kzg_proof; -mod public_key; -mod public_key_bytes; -mod secret_key; -mod signature; -mod signature_bytes; -mod uint256; +use rand::{RngCore, SeedableRng}; +use rand_xorshift::XorShiftRng; +use smallvec::{SmallVec, smallvec}; +use ssz_types::VariableList; +use typenum::Unsigned; pub fn test_random_instance() -> T { let mut rng = XorShiftRng::from_seed([0x42; 16]); @@ -115,7 +102,7 @@ where } } - output.into() + output.try_into().unwrap() } } diff --git a/consensus/types/src/test_utils/test_random/uint256.rs b/consensus/types/src/test_utils/test_random/uint256.rs index 30077f0e0f..eccf476595 100644 --- a/consensus/types/src/test_utils/test_random/uint256.rs +++ b/consensus/types/src/test_utils/test_random/uint256.rs @@ -1,7 +1,7 @@ -use super::*; +use crate::{core::Uint256, test_utils::TestRandom}; impl TestRandom for Uint256 { - fn random_for_test(rng: &mut impl RngCore) -> Self { + fn random_for_test(rng: &mut impl rand::RngCore) -> Self { let mut key_bytes = [0; 32]; rng.fill_bytes(&mut key_bytes); Self::from_le_slice(&key_bytes[..]) diff --git a/consensus/types/src/validator/mod.rs b/consensus/types/src/validator/mod.rs new file mode 100644 index 0000000000..8a67407821 --- /dev/null +++ b/consensus/types/src/validator/mod.rs @@ -0,0 +1,9 @@ +mod proposer_preparation_data; +mod validator; +mod validator_registration_data; +mod validator_subscription; + +pub use proposer_preparation_data::ProposerPreparationData; +pub use validator::{Validator, is_compounding_withdrawal_credential}; +pub use validator_registration_data::{SignedValidatorRegistrationData, ValidatorRegistrationData}; +pub use validator_subscription::ValidatorSubscription; diff --git a/consensus/types/src/proposer_preparation_data.rs b/consensus/types/src/validator/proposer_preparation_data.rs similarity index 95% rename from consensus/types/src/proposer_preparation_data.rs rename to consensus/types/src/validator/proposer_preparation_data.rs index 477fb3b9d1..8ef675de4f 100644 --- a/consensus/types/src/proposer_preparation_data.rs +++ b/consensus/types/src/validator/proposer_preparation_data.rs @@ -1,6 +1,7 @@ -use crate::*; use serde::{Deserialize, Serialize}; +use crate::core::Address; + /// A proposer preparation, created when a validator prepares the beacon node for potential proposers /// by supplying information required when proposing blocks for the given validators. #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator/validator.rs similarity index 96% rename from consensus/types/src/validator.rs rename to consensus/types/src/validator/validator.rs index 165f477ff4..7898ab9073 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator/validator.rs @@ -1,28 +1,25 @@ -use crate::context_deserialize; -use crate::{ - test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, - FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, -}; +use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{ + attestation::Checkpoint, + core::{Address, ChainSpec, Epoch, EthSpec, Hash256}, + fork::ForkName, + state::BeaconState, + test_utils::TestRandom, +}; + /// Information about a `BeaconChain` validator. /// /// Spec v0.12.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Eq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, TestRandom, TreeHash, )] #[context_deserialize(ForkName)] pub struct Validator { diff --git a/consensus/types/src/validator_registration_data.rs b/consensus/types/src/validator/validator_registration_data.rs similarity index 93% rename from consensus/types/src/validator_registration_data.rs rename to consensus/types/src/validator/validator_registration_data.rs index 345771074c..a0a1df7dc5 100644 --- a/consensus/types/src/validator_registration_data.rs +++ b/consensus/types/src/validator/validator_registration_data.rs @@ -1,8 +1,10 @@ -use crate::*; +use bls::{PublicKeyBytes, Signature}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; +use crate::core::{Address, ChainSpec, SignedRoot}; + /// Validator registration, for use in interacting with servers implementing the builder API. #[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] pub struct SignedValidatorRegistrationData { diff --git a/consensus/types/src/validator_subscription.rs b/consensus/types/src/validator/validator_subscription.rs similarity index 93% rename from consensus/types/src/validator_subscription.rs rename to consensus/types/src/validator/validator_subscription.rs index 62932638ec..92fb200e10 100644 --- a/consensus/types/src/validator_subscription.rs +++ b/consensus/types/src/validator/validator_subscription.rs @@ -1,7 +1,8 @@ -use crate::*; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use crate::{attestation::CommitteeIndex, core::Slot}; + /// A validator subscription, created when a validator subscribes to a slot to perform optional aggregation /// duties. #[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode, Eq, PartialOrd, Ord)] diff --git a/consensus/types/src/withdrawal/mod.rs b/consensus/types/src/withdrawal/mod.rs new file mode 100644 index 0000000000..bac80d00be --- /dev/null +++ b/consensus/types/src/withdrawal/mod.rs @@ -0,0 +1,9 @@ +mod pending_partial_withdrawal; +mod withdrawal; +mod withdrawal_credentials; +mod withdrawal_request; + +pub use pending_partial_withdrawal::PendingPartialWithdrawal; +pub use withdrawal::{Withdrawal, Withdrawals}; +pub use withdrawal_credentials::WithdrawalCredentials; +pub use withdrawal_request::WithdrawalRequest; diff --git a/consensus/types/src/pending_partial_withdrawal.rs b/consensus/types/src/withdrawal/pending_partial_withdrawal.rs similarity index 64% rename from consensus/types/src/pending_partial_withdrawal.rs rename to consensus/types/src/withdrawal/pending_partial_withdrawal.rs index ca49032859..cd866369a4 100644 --- a/consensus/types/src/pending_partial_withdrawal.rs +++ b/consensus/types/src/withdrawal/pending_partial_withdrawal.rs @@ -1,24 +1,14 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{Epoch, ForkName}; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Epoch, fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct PendingPartialWithdrawal { diff --git a/consensus/types/src/withdrawal/withdrawal.rs b/consensus/types/src/withdrawal/withdrawal.rs new file mode 100644 index 0000000000..d75bd4f501 --- /dev/null +++ b/consensus/types/src/withdrawal/withdrawal.rs @@ -0,0 +1,37 @@ +use context_deserialize::context_deserialize; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; +use test_random_derive::TestRandom; +use tree_hash_derive::TreeHash; + +use crate::{ + core::{Address, EthSpec}, + fork::ForkName, + test_utils::TestRandom, +}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, +)] +#[context_deserialize(ForkName)] +pub struct Withdrawal { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + #[serde(with = "serde_utils::address_hex")] + pub address: Address, + #[serde(with = "serde_utils::quoted_u64")] + pub amount: u64, +} + +pub type Withdrawals = VariableList::MaxWithdrawalsPerPayload>; + +#[cfg(test)] +mod tests { + use super::*; + + ssz_and_tree_hash_tests!(Withdrawal); +} diff --git a/consensus/types/src/withdrawal_credentials.rs b/consensus/types/src/withdrawal/withdrawal_credentials.rs similarity index 91% rename from consensus/types/src/withdrawal_credentials.rs rename to consensus/types/src/withdrawal/withdrawal_credentials.rs index 52d51ed559..b732222ca1 100644 --- a/consensus/types/src/withdrawal_credentials.rs +++ b/consensus/types/src/withdrawal/withdrawal_credentials.rs @@ -1,5 +1,6 @@ -use crate::*; -use bls::get_withdrawal_credentials; +use bls::{PublicKey, get_withdrawal_credentials}; + +use crate::core::{Address, ChainSpec, Hash256}; pub struct WithdrawalCredentials(Hash256); @@ -27,7 +28,7 @@ impl From for Hash256 { #[cfg(test)] mod test { use super::*; - use crate::test_utils::generate_deterministic_keypair; + use crate::{EthSpec, MainnetEthSpec, test_utils::generate_deterministic_keypair}; use std::str::FromStr; #[test] diff --git a/consensus/types/src/withdrawal_request.rs b/consensus/types/src/withdrawal/withdrawal_request.rs similarity index 71% rename from consensus/types/src/withdrawal_request.rs rename to consensus/types/src/withdrawal/withdrawal_request.rs index 57c6e798eb..98a40016f9 100644 --- a/consensus/types/src/withdrawal_request.rs +++ b/consensus/types/src/withdrawal/withdrawal_request.rs @@ -1,25 +1,16 @@ -use crate::context_deserialize; -use crate::test_utils::TestRandom; -use crate::{Address, ForkName, PublicKeyBytes}; +use bls::PublicKeyBytes; +use context_deserialize::context_deserialize; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; +use crate::{core::Address, fork::ForkName, test_utils::TestRandom}; + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive( - arbitrary::Arbitrary, - Debug, - PartialEq, - Eq, - Hash, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, + Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom, )] #[context_deserialize(ForkName)] pub struct WithdrawalRequest { diff --git a/consensus/types/src/beacon_state/committee_cache/tests.rs b/consensus/types/tests/committee_cache.rs similarity index 97% rename from consensus/types/src/beacon_state/committee_cache/tests.rs rename to consensus/types/tests/committee_cache.rs index 1d2ca4ccdb..751ef05d29 100644 --- a/consensus/types/src/beacon_state/committee_cache/tests.rs +++ b/consensus/types/tests/committee_cache.rs @@ -1,9 +1,14 @@ #![cfg(test)] -use crate::test_utils::*; -use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; -use beacon_chain::types::*; use std::sync::LazyLock; + +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use bls::Keypair; +use fixed_bytes::FixedBytesExtended; +use milhouse::Vector; use swap_or_not_shuffle::shuffle_list; +use types::*; + +use crate::test_utils::generate_deterministic_keypairs; pub const VALIDATOR_COUNT: usize = 16; diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/tests/state.rs similarity index 97% rename from consensus/types/src/beacon_state/tests.rs rename to consensus/types/tests/state.rs index bfa7bb86d2..63ab3b8084 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/tests/state.rs @@ -1,15 +1,17 @@ #![cfg(test)] -use crate::test_utils::*; -use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; -use beacon_chain::types::{ - test_utils::TestRandom, BeaconState, BeaconStateAltair, BeaconStateBase, BeaconStateError, - ChainSpec, Domain, Epoch, EthSpec, FixedBytesExtended, Hash256, Keypair, MainnetEthSpec, - MinimalEthSpec, RelativeEpoch, Slot, Vector, -}; -use ssz::Encode; use std::ops::Mul; use std::sync::LazyLock; + +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use bls::Keypair; +use fixed_bytes::FixedBytesExtended; +use milhouse::Vector; +use rand::SeedableRng; +use rand_xorshift::XorShiftRng; +use ssz::Encode; use swap_or_not_shuffle::compute_shuffled_index; +use types::test_utils::{TestRandom, generate_deterministic_keypairs}; +use types::*; pub const MAX_VALIDATOR_COUNT: usize = 129; pub const SLOT_OFFSET: Slot = Slot::new(1); diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index d02e01b80c..4661288679 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -4,6 +4,14 @@ version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } +[features] +arbitrary = [] +default = ["supranational"] +fake_crypto = [] +supranational = ["blst"] +supranational-portable = ["supranational", "blst/portable"] +supranational-force-adx = ["supranational", "blst/force-adx"] + [dependencies] alloy-primitives = { workspace = true } arbitrary = { workspace = true } @@ -18,11 +26,3 @@ safe_arith = { workspace = true } serde = { workspace = true } tree_hash = { workspace = true } zeroize = { workspace = true } - -[features] -arbitrary = [] -default = ["supranational"] -fake_crypto = [] -supranational = ["blst"] -supranational-portable = ["supranational", "blst/portable"] -supranational-force-adx = ["supranational", "blst/force-adx"] diff --git a/crypto/bls/src/generic_aggregate_public_key.rs b/crypto/bls/src/generic_aggregate_public_key.rs index 426e165fb7..aea23ca63c 100644 --- a/crypto/bls/src/generic_aggregate_public_key.rs +++ b/crypto/bls/src/generic_aggregate_public_key.rs @@ -1,6 +1,6 @@ use crate::{ - generic_public_key::{GenericPublicKey, TPublicKey}, Error, + generic_public_key::{GenericPublicKey, TPublicKey}, }; use std::fmt::{self, Debug}; use std::marker::PhantomData; diff --git a/crypto/bls/src/generic_aggregate_signature.rs b/crypto/bls/src/generic_aggregate_signature.rs index e6e53253f6..35674394a0 100644 --- a/crypto/bls/src/generic_aggregate_signature.rs +++ b/crypto/bls/src/generic_aggregate_signature.rs @@ -1,8 +1,8 @@ use crate::{ + Error, Hash256, INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN, generic_aggregate_public_key::TAggregatePublicKey, generic_public_key::{GenericPublicKey, TPublicKey}, generic_signature::{GenericSignature, TSignature}, - Error, Hash256, INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN, }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; @@ -124,13 +124,15 @@ where /// Aggregates a signature onto `self`. pub fn add_assign(&mut self, other: &GenericSignature) { if let Some(other_point) = other.point() { - self.is_infinity = self.is_infinity && other.is_infinity; if let Some(self_point) = &mut self.point { - self_point.add_assign(other_point) + self_point.add_assign(other_point); + self.is_infinity = self.is_infinity && other.is_infinity; } else { let mut self_point = AggSig::infinity(); self_point.add_assign(other_point); - self.point = Some(self_point) + self.point = Some(self_point); + // the result is infinity, if `other` is + self.is_infinity = other.is_infinity; } } } @@ -138,13 +140,15 @@ where /// Aggregates an aggregate signature onto `self`. pub fn add_assign_aggregate(&mut self, other: &Self) { if let Some(other_point) = other.point() { - self.is_infinity = self.is_infinity && other.is_infinity; if let Some(self_point) = &mut self.point { - self_point.add_assign_aggregate(other_point) + self_point.add_assign_aggregate(other_point); + self.is_infinity = self.is_infinity && other.is_infinity; } else { let mut self_point = AggSig::infinity(); self_point.add_assign_aggregate(other_point); - self.point = Some(self_point) + self.point = Some(self_point); + // the result is infinity, if `other` is + self.is_infinity = other.is_infinity; } } } diff --git a/crypto/bls/src/generic_public_key.rs b/crypto/bls/src/generic_public_key.rs index 80b42dfa71..122a47c15d 100644 --- a/crypto/bls/src/generic_public_key.rs +++ b/crypto/bls/src/generic_public_key.rs @@ -1,5 +1,5 @@ -use crate::generic_public_key_bytes::GenericPublicKeyBytes; use crate::Error; +use crate::generic_public_key_bytes::GenericPublicKeyBytes; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; use serde_utils::hex::encode as hex_encode; diff --git a/crypto/bls/src/generic_public_key_bytes.rs b/crypto/bls/src/generic_public_key_bytes.rs index 985bff745c..6df4f3b0b0 100644 --- a/crypto/bls/src/generic_public_key_bytes.rs +++ b/crypto/bls/src/generic_public_key_bytes.rs @@ -1,6 +1,6 @@ use crate::{ - generic_public_key::{GenericPublicKey, TPublicKey}, Error, PUBLIC_KEY_BYTES_LEN, + generic_public_key::{GenericPublicKey, TPublicKey}, }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; diff --git a/crypto/bls/src/generic_secret_key.rs b/crypto/bls/src/generic_secret_key.rs index 62bfc1467d..813693ee0a 100644 --- a/crypto/bls/src/generic_secret_key.rs +++ b/crypto/bls/src/generic_secret_key.rs @@ -1,7 +1,7 @@ use crate::{ + Error, Hash256, ZeroizeHash, generic_public_key::{GenericPublicKey, TPublicKey}, generic_signature::{GenericSignature, TSignature}, - Error, Hash256, ZeroizeHash, }; use std::marker::PhantomData; diff --git a/crypto/bls/src/generic_signature.rs b/crypto/bls/src/generic_signature.rs index 0b375d3edd..e59efa3b3e 100644 --- a/crypto/bls/src/generic_signature.rs +++ b/crypto/bls/src/generic_signature.rs @@ -1,6 +1,6 @@ use crate::{ - generic_public_key::{GenericPublicKey, TPublicKey}, Error, Hash256, + generic_public_key::{GenericPublicKey, TPublicKey}, }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; diff --git a/crypto/bls/src/generic_signature_bytes.rs b/crypto/bls/src/generic_signature_bytes.rs index b291adb735..b6d0a7d8b0 100644 --- a/crypto/bls/src/generic_signature_bytes.rs +++ b/crypto/bls/src/generic_signature_bytes.rs @@ -1,7 +1,7 @@ use crate::{ + Error, INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN, generic_public_key::TPublicKey, generic_signature::{GenericSignature, TSignature}, - Error, INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN, }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; diff --git a/crypto/bls/src/generic_signature_set.rs b/crypto/bls/src/generic_signature_set.rs index a64db7adef..bfcf149201 100644 --- a/crypto/bls/src/generic_signature_set.rs +++ b/crypto/bls/src/generic_signature_set.rs @@ -1,9 +1,9 @@ use crate::{ + Hash256, generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::{GenericAggregateSignature, TAggregateSignature}, generic_public_key::{GenericPublicKey, TPublicKey}, generic_signature::{GenericSignature, TSignature}, - Hash256, }; use std::borrow::Cow; use std::marker::PhantomData; diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index 6ca0fe09b2..c1ed2c7177 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -1,29 +1,28 @@ use crate::{ + BlstError, Error, Hash256, INFINITY_SIGNATURE, ZeroizeHash, generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, generic_public_key::{ - GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + GenericPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, TPublicKey, }, generic_secret_key::TSecretKey, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, - BlstError, Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, + generic_signature::{SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN, TSignature}, }; pub use blst::min_pk as blst_core; -use blst::{blst_scalar, BLST_ERROR}; +use blst::{BLST_ERROR, blst_scalar}; use rand::Rng; - pub const DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; pub const RAND_BITS: usize = 64; /// Provides the externally-facing, core BLS types. pub mod types { + pub use super::BlstAggregatePublicKey as AggregatePublicKey; + pub use super::BlstAggregateSignature as AggregateSignature; + pub use super::SignatureSet; pub use super::blst_core::PublicKey; pub use super::blst_core::SecretKey; pub use super::blst_core::Signature; pub use super::verify_signature_sets; - pub use super::BlstAggregatePublicKey as AggregatePublicKey; - pub use super::BlstAggregateSignature as AggregateSignature; - pub use super::SignatureSet; } pub type SignatureSet<'a> = crate::generic_signature_set::GenericSignatureSet< @@ -43,7 +42,7 @@ pub fn verify_signature_sets<'a>( return false; } - let rng = &mut rand::thread_rng(); + let rng = &mut rand::rng(); let mut rands: Vec = Vec::with_capacity(sets.len()); let mut msgs_refs = Vec::with_capacity(sets.len()); @@ -55,7 +54,7 @@ pub fn verify_signature_sets<'a>( let mut vals = [0u64; 4]; while vals[0] == 0 { // Do not use zero - vals[0] = rng.gen(); + vals[0] = rng.random(); } let mut rand_i = std::mem::MaybeUninit::::uninit(); @@ -284,8 +283,8 @@ impl TAggregateSignature for blst_core::SecretKey { fn random() -> Self { - let rng = &mut rand::thread_rng(); - let ikm: [u8; 32] = rng.gen(); + let rng = &mut rand::rng(); + let ikm: [u8; 32] = rng.random(); Self::key_gen(&ikm, &[]).unwrap() } diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index 7273697597..e7eee05077 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -1,23 +1,23 @@ use crate::{ + Error, Hash256, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, ZeroizeHash, generic_aggregate_public_key::TAggregatePublicKey, generic_aggregate_signature::TAggregateSignature, generic_public_key::{ - GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, + GenericPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, TPublicKey, }, - generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, - Error, Hash256, ZeroizeHash, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, + generic_secret_key::{SECRET_KEY_BYTES_LEN, TSecretKey}, + generic_signature::{SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN, TSignature}, }; /// Provides the externally-facing, core BLS types. pub mod types { - pub use super::verify_signature_sets; pub use super::AggregatePublicKey; pub use super::AggregateSignature; pub use super::PublicKey; pub use super::SecretKey; pub use super::Signature; pub use super::SignatureSet; + pub use super::verify_signature_sets; } pub type SignatureSet<'a> = crate::generic_signature_set::GenericSignatureSet< diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index ac2d83b204..433eaef4f2 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -94,7 +94,7 @@ macro_rules! define_mod { use crate::generics::*; - pub use bls_variant::{verify_signature_sets, SignatureSet}; + pub use bls_variant::{SignatureSet, verify_signature_sets}; pub type PublicKey = GenericPublicKey; pub type PublicKeyBytes = GenericPublicKeyBytes; diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index 611dabbd64..1827ea9921 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -356,6 +356,17 @@ macro_rules! test_suite { .assert_single_message_verify(true) } + #[test] + fn empty_aggregate_plus_infinity_should_be_infinity() { + let mut agg = AggregateSignature::empty(); + let infinity_sig = Signature::deserialize(&INFINITY_SIGNATURE).unwrap(); + agg.add_assign(&infinity_sig); + assert!( + agg.is_infinity(), + "is_infinity flag should be true after adding infinity to empty" + ); + } + #[test] fn deserialize_infinity_public_key() { PublicKey::deserialize(&bls::INFINITY_PUBLIC_KEY).unwrap_err(); @@ -370,7 +381,7 @@ macro_rules! test_suite { } impl OwnedSignatureSet { - pub fn multiple_pubkeys(&self) -> SignatureSet { + pub fn multiple_pubkeys(&self) -> SignatureSet<'_> { let signing_keys = self.signing_keys.iter().map(Cow::Borrowed).collect(); SignatureSet::multiple_pubkeys(&self.signature, signing_keys, self.message) } diff --git a/crypto/eth2_key_derivation/Cargo.toml b/crypto/eth2_key_derivation/Cargo.toml index a893a9360d..b8976b8ccb 100644 --- a/crypto/eth2_key_derivation/Cargo.toml +++ b/crypto/eth2_key_derivation/Cargo.toml @@ -3,6 +3,7 @@ name = "eth2_key_derivation" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } +autotests = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -14,3 +15,7 @@ zeroize = { workspace = true } [dev-dependencies] hex = { workspace = true } + +[[test]] +name = "eth2_key_derivation_tests" +path = "tests/main.rs" diff --git a/crypto/eth2_key_derivation/src/derived_key.rs b/crypto/eth2_key_derivation/src/derived_key.rs index 21f98796d4..bdcb689bfc 100644 --- a/crypto/eth2_key_derivation/src/derived_key.rs +++ b/crypto/eth2_key_derivation/src/derived_key.rs @@ -1,6 +1,6 @@ -use crate::{lamport_secret_key::LamportSecretKey, secret_bytes::SecretBytes, ZeroizeHash}; +use crate::{ZeroizeHash, lamport_secret_key::LamportSecretKey, secret_bytes::SecretBytes}; use num_bigint_dig::BigUint; -use ring::hkdf::{KeyType, Prk, Salt, HKDF_SHA256}; +use ring::hkdf::{HKDF_SHA256, KeyType, Prk, Salt}; use sha2::{Digest, Sha256}; use zeroize::Zeroize; @@ -333,8 +333,7 @@ mod test { fn get_raw_vector() -> RawTestVector { RawTestVector { seed: "0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", - master_sk: - "6083874454709270928345386274498605044986640685124978867557563392430687146096", + master_sk: "6083874454709270928345386274498605044986640685124978867557563392430687146096", child_index: 0, lamport_0: vec![ "0xe345d0ad7be270737de05cf036f688f385d5f99c7fddb054837658bdd2ebd519", @@ -850,10 +849,8 @@ mod test { "0x8b28838382e6892f59c42a7709d6d38396495d3af5a8d5b0a60f172a6a8940bd", "0x261a605fa5f2a9bdc7cffac530edcf976e7ea7af4e443b625fe01ed39dad44b6", ], - compressed_lamport_pk: - "0xdd635d27d1d52b9a49df9e5c0c622360a4dd17cba7db4e89bce3cb048fb721a5", - child_sk: - "20397789859736650942317412262472558107875392172444076792671091975210932703118", + compressed_lamport_pk: "0xdd635d27d1d52b9a49df9e5c0c622360a4dd17cba7db4e89bce3cb048fb721a5", + child_sk: "20397789859736650942317412262472558107875392172444076792671091975210932703118", } } } diff --git a/crypto/eth2_key_derivation/tests/main.rs b/crypto/eth2_key_derivation/tests/main.rs new file mode 100644 index 0000000000..a239eaa618 --- /dev/null +++ b/crypto/eth2_key_derivation/tests/main.rs @@ -0,0 +1,2 @@ +mod eip2333_vectors; +mod tests; diff --git a/crypto/eth2_keystore/Cargo.toml b/crypto/eth2_keystore/Cargo.toml index 61d2722efb..290a10adc9 100644 --- a/crypto/eth2_keystore/Cargo.toml +++ b/crypto/eth2_keystore/Cargo.toml @@ -3,6 +3,7 @@ name = "eth2_keystore" version = "0.1.0" authors = ["Pawan Dhananjay "] edition = { workspace = true } +autotests = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -24,3 +25,7 @@ zeroize = { workspace = true } [dev-dependencies] tempfile = { workspace = true } + +[[test]] +name = "eth2_keystore_tests" +path = "tests/main.rs" diff --git a/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs b/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs index e891693040..51ac785637 100644 --- a/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs +++ b/crypto/eth2_keystore/src/json_keystore/hex_bytes.rs @@ -37,7 +37,7 @@ impl TryFrom for HexBytes { fn try_from(s: String) -> Result { // Left-pad with a zero if there is not an even number of hex digits to ensure // `hex::decode` doesn't return an error. - let s = if s.len() % 2 != 0 { + let s = if !s.len().is_multiple_of(2) { format!("0{}", s) } else { s diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 16a979cf63..b31e32eb4a 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -1,23 +1,24 @@ //! Provides a JSON keystore for a BLS keypair, as specified by //! [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335). +use crate::Uuid; use crate::derived_key::DerivedKey; use crate::json_keystore::{ Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, JsonKeystore, Kdf, KdfModule, Scrypt, Sha256Checksum, Version, }; -use crate::Uuid; +use aes::Aes128Ctr as AesCtr; use aes::cipher::generic_array::GenericArray; use aes::cipher::{NewCipher, StreamCipher}; -use aes::Aes128Ctr as AesCtr; use bls::{Keypair, PublicKey, SecretKey, ZeroizeHash}; use eth2_key_derivation::PlainText; use hmac::Hmac; use pbkdf2::pbkdf2; use rand::prelude::*; use scrypt::{ + Params as ScryptParams, errors::{InvalidOutputLen, InvalidParams}, - scrypt, Params as ScryptParams, + scrypt, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -103,8 +104,8 @@ impl<'a> KeystoreBuilder<'a> { if password.is_empty() { Err(Error::EmptyPassword) } else { - let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); - let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + let salt = rand::rng().random::<[u8; SALT_SIZE]>(); + let iv = rand::rng().random::<[u8; IV_SIZE]>().to_vec().into(); Ok(Self { keypair, @@ -574,7 +575,10 @@ fn validate_parameters(kdf: &Kdf) -> Result<(), Error> { let default_kdf = Scrypt::default_scrypt(vec![0u8; 32]); let default_npr = 128 * default_kdf.n * default_kdf.p * default_kdf.r; if npr < default_npr { - eprintln!("WARN: Scrypt parameters are too weak (n: {}, p: {}, r: {}), we recommend (n: {}, p: {}, r: {})", params.n, params.p, params.r, default_kdf.n, default_kdf.p, default_kdf.r); + eprintln!( + "WARN: Scrypt parameters are too weak (n: {}, p: {}, r: {}), we recommend (n: {}, p: {}, r: {})", + params.n, params.p, params.r, default_kdf.n, default_kdf.p, default_kdf.r + ); } // Validate `salt` length. diff --git a/crypto/eth2_keystore/src/lib.rs b/crypto/eth2_keystore/src/lib.rs index afa5e75de3..e1740c0a41 100644 --- a/crypto/eth2_keystore/src/lib.rs +++ b/crypto/eth2_keystore/src/lib.rs @@ -9,7 +9,7 @@ pub mod json_keystore; pub use bls::ZeroizeHash; pub use eth2_key_derivation::PlainText; pub use keystore::{ - decrypt, default_kdf, encrypt, keypair_from_secret, Error, Keystore, KeystoreBuilder, DKLEN, - HASH_SIZE, IV_SIZE, SALT_SIZE, + DKLEN, Error, HASH_SIZE, IV_SIZE, Keystore, KeystoreBuilder, SALT_SIZE, decrypt, default_kdf, + encrypt, keypair_from_secret, }; pub use uuid::Uuid; diff --git a/crypto/eth2_keystore/tests/main.rs b/crypto/eth2_keystore/tests/main.rs new file mode 100644 index 0000000000..79b31d5eda --- /dev/null +++ b/crypto/eth2_keystore/tests/main.rs @@ -0,0 +1,4 @@ +mod eip2335_vectors; +mod json; +mod params; +mod tests; diff --git a/crypto/eth2_keystore/tests/tests.rs b/crypto/eth2_keystore/tests/tests.rs index 20bf9f1653..6849adbbdd 100644 --- a/crypto/eth2_keystore/tests/tests.rs +++ b/crypto/eth2_keystore/tests/tests.rs @@ -3,9 +3,8 @@ use bls::Keypair; use eth2_keystore::{ - default_kdf, + DKLEN, Error, Keystore, KeystoreBuilder, default_kdf, json_keystore::{Kdf, Pbkdf2, Prf, Scrypt}, - Error, Keystore, KeystoreBuilder, DKLEN, }; use std::fs::File; use tempfile::tempdir; diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index 5327bdc163..0d454016a6 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -3,6 +3,7 @@ name = "eth2_wallet" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } +autotests = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -18,3 +19,7 @@ uuid = { workspace = true } [dev-dependencies] hex = { workspace = true } tempfile = { workspace = true } + +[[test]] +name = "eth2_wallet_tests" +path = "tests/main.rs" diff --git a/crypto/eth2_wallet/src/lib.rs b/crypto/eth2_wallet/src/lib.rs index 492024d26e..27b7e830b3 100644 --- a/crypto/eth2_wallet/src/lib.rs +++ b/crypto/eth2_wallet/src/lib.rs @@ -4,8 +4,8 @@ mod wallet; pub mod json_wallet; pub use bip39; -pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE}; +pub use validator_path::{COIN_TYPE, KeyType, PURPOSE, ValidatorPath}; pub use wallet::{ - recover_validator_secret, recover_validator_secret_from_mnemonic, DerivedKey, Error, - KeystoreError, PlainText, Uuid, ValidatorKeystores, Wallet, WalletBuilder, + DerivedKey, Error, KeystoreError, PlainText, Uuid, ValidatorKeystores, Wallet, WalletBuilder, + recover_validator_secret, recover_validator_secret_from_mnemonic, }; diff --git a/crypto/eth2_wallet/src/wallet.rs b/crypto/eth2_wallet/src/wallet.rs index 8bf7091216..bd9cb10ab2 100644 --- a/crypto/eth2_wallet/src/wallet.rs +++ b/crypto/eth2_wallet/src/wallet.rs @@ -1,17 +1,17 @@ use crate::{ + KeyType, ValidatorPath, json_wallet::{ Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, JsonWallet, Kdf, KdfModule, Sha256Checksum, TypeField, Version, }, - KeyType, ValidatorPath, }; pub use bip39::{Mnemonic, Seed as Bip39Seed}; pub use eth2_key_derivation::{DerivedKey, DerivedKeyError}; -use eth2_keystore::{ - decrypt, default_kdf, encrypt, keypair_from_secret, Keystore, KeystoreBuilder, IV_SIZE, - SALT_SIZE, -}; pub use eth2_keystore::{Error as KeystoreError, PlainText}; +use eth2_keystore::{ + IV_SIZE, Keystore, KeystoreBuilder, SALT_SIZE, decrypt, default_kdf, encrypt, + keypair_from_secret, +}; use rand::prelude::*; use serde::{Deserialize, Serialize}; use std::io::{Read, Write}; @@ -90,8 +90,8 @@ impl<'a> WalletBuilder<'a> { } else if seed.is_empty() { Err(Error::EmptySeed) } else { - let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); - let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + let salt = rand::rng().random::<[u8; SALT_SIZE]>(); + let iv = rand::rng().random::<[u8; IV_SIZE]>().to_vec().into(); Ok(Self { seed: seed.to_vec().into(), diff --git a/crypto/eth2_wallet/tests/main.rs b/crypto/eth2_wallet/tests/main.rs new file mode 100644 index 0000000000..d59ccff639 --- /dev/null +++ b/crypto/eth2_wallet/tests/main.rs @@ -0,0 +1,3 @@ +mod eip2386_vectors; +mod json; +mod tests; diff --git a/crypto/eth2_wallet/tests/tests.rs b/crypto/eth2_wallet/tests/tests.rs index 3dc073f764..812d33247e 100644 --- a/crypto/eth2_wallet/tests/tests.rs +++ b/crypto/eth2_wallet/tests/tests.rs @@ -1,8 +1,9 @@ #![cfg(not(debug_assertions))] use eth2_wallet::{ + DerivedKey, Error, KeyType, KeystoreError, Wallet, WalletBuilder, bip39::{Language, Mnemonic, Seed}, - recover_validator_secret, DerivedKey, Error, KeyType, KeystoreError, Wallet, WalletBuilder, + recover_validator_secret, }; use std::fs::File; use tempfile::tempdir; diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index bfe0f19cd0..5a36eb74f7 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -8,15 +8,17 @@ edition = "2021" [dependencies] arbitrary = { workspace = true } c-kzg = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } hex = { workspace = true } +rayon = { workspace = true } rust_eth_kzg = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tracing = { workspace = true } tree_hash = { workspace = true } [dev-dependencies] diff --git a/crypto/kzg/benches/benchmark.rs b/crypto/kzg/benches/benchmark.rs index 234e624698..432d84654a 100644 --- a/crypto/kzg/benches/benchmark.rs +++ b/crypto/kzg/benches/benchmark.rs @@ -1,16 +1,17 @@ use c_kzg::KzgSettings; use criterion::{criterion_group, criterion_main, Criterion}; -use kzg::{trusted_setup::get_trusted_setup, TrustedSetup}; +use kzg::{trusted_setup::get_trusted_setup, TrustedSetup, NO_PRECOMPUTE}; use rust_eth_kzg::{DASContext, TrustedSetup as PeerDASTrustedSetup}; pub fn bench_init_context(c: &mut Criterion) { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) + let trusted_setup_bytes = get_trusted_setup(); + let trusted_setup_json = std::str::from_utf8(&trusted_setup_bytes) .map_err(|e| format!("Unable to read trusted setup file: {}", e)) .expect("should have trusted setup"); c.bench_function("Initialize context rust_eth_kzg", |b| { b.iter(|| { - let trusted_setup = PeerDASTrustedSetup::from(&trusted_setup); + let trusted_setup = PeerDASTrustedSetup::from_json(trusted_setup_json); DASContext::new( &trusted_setup, rust_eth_kzg::UsePrecomp::Yes { @@ -22,11 +23,16 @@ pub fn bench_init_context(c: &mut Criterion) { c.bench_function("Initialize context c-kzg (4844)", |b| { b.iter(|| { let trusted_setup: TrustedSetup = - serde_json::from_reader(get_trusted_setup().as_slice()) + serde_json::from_reader(trusted_setup_bytes.as_slice()) .map_err(|e| format!("Unable to read trusted setup file: {}", e)) .expect("should have trusted setup"); - KzgSettings::load_trusted_setup(&trusted_setup.g1_points(), &trusted_setup.g2_points()) - .unwrap() + KzgSettings::load_trusted_setup( + &trusted_setup.g1_monomial(), + &trusted_setup.g1_lagrange(), + &trusted_setup.g2_monomial(), + NO_PRECOMPUTE, + ) + .unwrap() }) }); } diff --git a/crypto/kzg/src/kzg_commitment.rs b/crypto/kzg/src/kzg_commitment.rs index cfab09f63e..5a5e689429 100644 --- a/crypto/kzg/src/kzg_commitment.rs +++ b/crypto/kzg/src/kzg_commitment.rs @@ -1,5 +1,5 @@ use c_kzg::BYTES_PER_COMMITMENT; -use derivative::Derivative; +use educe::Educe; use ethereum_hashing::hash_fixed; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; @@ -11,8 +11,8 @@ use tree_hash::{Hash256, PackedEncoding, TreeHash}; pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; -#[derive(Derivative, Clone, Copy, Encode, Decode)] -#[derivative(PartialEq, Eq, Hash)] +#[derive(Educe, Clone, Copy, Encode, Decode)] +#[educe(PartialEq, Eq, Hash)] #[ssz(struct_behaviour = "transparent")] pub struct KzgCommitment(pub [u8; c_kzg::BYTES_PER_COMMITMENT]); diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 5d752cc0a5..0fe95b7723 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -3,6 +3,7 @@ mod kzg_proof; pub mod trusted_setup; use rust_eth_kzg::{CellIndex, DASContext}; +use std::collections::HashMap; use std::fmt::Debug; pub use crate::{ @@ -16,13 +17,23 @@ pub use c_kzg::{ BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, }; +use crate::trusted_setup::load_trusted_setup; +use rayon::prelude::*; pub use rust_eth_kzg::{ constants::{BYTES_PER_CELL, CELLS_PER_EXT_BLOB}, Cell, CellIndex as CellID, CellRef, TrustedSetup as PeerDASTrustedSetup, }; +use tracing::{instrument, Span}; -// Note: `spec.number_of_columns` is a config and should match `CELLS_PER_EXT_BLOB` - however this -// is a constant in the KZG library - be aware that overriding `number_of_columns` will break KZG +/// Disables the fixed-base multi-scalar multiplication optimization for computing +/// cell KZG proofs, because `rust-eth-kzg` already handles the precomputation. +/// +/// Details about `precompute` parameter can be found here: +/// +pub const NO_PRECOMPUTE: u64 = 0; + +// Note: Both `NUMBER_OF_COLUMNS` and `CELLS_PER_EXT_BLOB` are preset values - however this +// is a constant in the KZG library - be aware that overriding `NUMBER_OF_COLUMNS` will break KZG // operations. pub type CellsAndKzgProofs = ([Cell; CELLS_PER_EXT_BLOB], [KzgProof; CELLS_PER_EXT_BLOB]); @@ -30,6 +41,8 @@ pub type KzgBlobRef<'a> = &'a [u8; BYTES_PER_BLOB]; #[derive(Debug)] pub enum Error { + /// An error from initialising the trusted setup. + TrustedSetupError(String), /// An error from the underlying kzg library. Kzg(c_kzg::Error), /// A prover/verifier error from the rust-eth-kzg library. @@ -58,52 +71,30 @@ pub struct Kzg { } impl Kzg { - pub fn new_from_trusted_setup_no_precomp(trusted_setup: TrustedSetup) -> Result { - let peerdas_trusted_setup = PeerDASTrustedSetup::from(&trusted_setup); - - let context = DASContext::new(&peerdas_trusted_setup, rust_eth_kzg::UsePrecomp::No); + pub fn new_from_trusted_setup_no_precomp(trusted_setup: &[u8]) -> Result { + let (ckzg_trusted_setup, rkzg_trusted_setup) = load_trusted_setup(trusted_setup)?; + let context = DASContext::new(&rkzg_trusted_setup, rust_eth_kzg::UsePrecomp::No); Ok(Self { trusted_setup: KzgSettings::load_trusted_setup( - &trusted_setup.g1_points(), - &trusted_setup.g2_points(), + &ckzg_trusted_setup.g1_monomial(), + &ckzg_trusted_setup.g1_lagrange(), + &ckzg_trusted_setup.g2_monomial(), + NO_PRECOMPUTE, )?, context, }) } /// Load the kzg trusted setup parameters from a vec of G1 and G2 points. - pub fn new_from_trusted_setup(trusted_setup: TrustedSetup) -> Result { - let peerdas_trusted_setup = PeerDASTrustedSetup::from(&trusted_setup); - - let context = DASContext::new( - &peerdas_trusted_setup, - rust_eth_kzg::UsePrecomp::Yes { - width: rust_eth_kzg::constants::RECOMMENDED_PRECOMP_WIDTH, - }, - ); - - Ok(Self { - trusted_setup: KzgSettings::load_trusted_setup( - &trusted_setup.g1_points(), - &trusted_setup.g2_points(), - )?, - context, - }) - } - - pub fn new_from_trusted_setup_das_enabled(trusted_setup: TrustedSetup) -> Result { - // Initialize the trusted setup using default parameters - // - // Note: One can also use `from_json` to initialize it from the consensus-specs - // json string. - let peerdas_trusted_setup = PeerDASTrustedSetup::from(&trusted_setup); + pub fn new_from_trusted_setup(trusted_setup: &[u8]) -> Result { + let (ckzg_trusted_setup, rkzg_trusted_setup) = load_trusted_setup(trusted_setup)?; // It's not recommended to change the config parameter for precomputation as storage // grows exponentially, but the speedup is exponential - after a while the speedup // starts to become sublinear. let context = DASContext::new( - &peerdas_trusted_setup, + &rkzg_trusted_setup, rust_eth_kzg::UsePrecomp::Yes { width: rust_eth_kzg::constants::RECOMMENDED_PRECOMP_WIDTH, }, @@ -111,8 +102,10 @@ impl Kzg { Ok(Self { trusted_setup: KzgSettings::load_trusted_setup( - &trusted_setup.g1_points(), - &trusted_setup.g2_points(), + &ckzg_trusted_setup.g1_monomial(), + &ckzg_trusted_setup.g1_lagrange(), + &ckzg_trusted_setup.g2_monomial(), + NO_PRECOMPUTE, )?, context, }) @@ -128,7 +121,8 @@ impl Kzg { blob: &Blob, kzg_commitment: KzgCommitment, ) -> Result { - c_kzg::KzgProof::compute_blob_kzg_proof(blob, &kzg_commitment.into(), &self.trusted_setup) + self.trusted_setup + .compute_blob_kzg_proof(blob, &kzg_commitment.into()) .map(|proof| KzgProof(proof.to_bytes().into_inner())) .map_err(Into::into) } @@ -140,11 +134,10 @@ impl Kzg { kzg_commitment: KzgCommitment, kzg_proof: KzgProof, ) -> Result<(), Error> { - if !c_kzg::KzgProof::verify_blob_kzg_proof( + if !self.trusted_setup.verify_blob_kzg_proof( blob, &kzg_commitment.into(), &kzg_proof.into(), - &self.trusted_setup, )? { Err(Error::KzgVerificationFailed) } else { @@ -172,11 +165,10 @@ impl Kzg { .map(|proof| Bytes48::from(*proof)) .collect::>(); - if !c_kzg::KzgProof::verify_blob_kzg_proof_batch( + if !self.trusted_setup.verify_blob_kzg_proof_batch( blobs, &commitments_bytes, &proofs_bytes, - &self.trusted_setup, )? { Err(Error::KzgVerificationFailed) } else { @@ -186,7 +178,8 @@ impl Kzg { /// Converts a blob to a kzg commitment. pub fn blob_to_kzg_commitment(&self, blob: &Blob) -> Result { - c_kzg::KzgCommitment::blob_to_kzg_commitment(blob, &self.trusted_setup) + self.trusted_setup + .blob_to_kzg_commitment(blob) .map(|commitment| KzgCommitment(commitment.to_bytes().into_inner())) .map_err(Into::into) } @@ -197,7 +190,8 @@ impl Kzg { blob: &Blob, z: &Bytes32, ) -> Result<(KzgProof, Bytes32), Error> { - c_kzg::KzgProof::compute_kzg_proof(blob, z, &self.trusted_setup) + self.trusted_setup + .compute_kzg_proof(blob, z) .map(|(proof, y)| (KzgProof(proof.to_bytes().into_inner()), y)) .map_err(Into::into) } @@ -210,14 +204,9 @@ impl Kzg { y: &Bytes32, kzg_proof: KzgProof, ) -> Result { - c_kzg::KzgProof::verify_kzg_proof( - &kzg_commitment.into(), - z, - y, - &kzg_proof.into(), - &self.trusted_setup, - ) - .map_err(Into::into) + self.trusted_setup + .verify_kzg_proof(&kzg_commitment.into(), z, y, &kzg_proof.into()) + .map_err(Into::into) } /// Computes the cells and associated proofs for a given `blob`. @@ -243,31 +232,87 @@ impl Kzg { } /// Verifies a batch of cell-proof-commitment triplets. + #[instrument(skip_all, level = "debug", fields(cells = cells.len()))] pub fn verify_cell_proof_batch( &self, cells: &[CellRef<'_>], kzg_proofs: &[Bytes48], - columns: Vec, + indices: Vec, kzg_commitments: &[Bytes48], - ) -> Result<(), Error> { - let proofs: Vec<_> = kzg_proofs.iter().map(|proof| proof.as_ref()).collect(); - let commitments: Vec<_> = kzg_commitments - .iter() - .map(|commitment| commitment.as_ref()) - .collect(); - let verification_result = self.context().verify_cell_kzg_proof_batch( - commitments.to_vec(), - columns, - cells.to_vec(), - proofs.to_vec(), - ); + ) -> Result<(), (Option, Error)> { + let mut column_groups: HashMap> = HashMap::new(); - // Modify the result so it matches roughly what the previous method was doing. - match verification_result { - Ok(_) => Ok(()), - Err(e) if e.invalid_proof() => Err(Error::KzgVerificationFailed), - Err(e) => Err(Error::PeerDASKZG(e)), + let expected_len = cells.len(); + + // This check is already made in `validate_data_columns`. However we add it here so that ef consensus spec tests pass + // and to avoid any potential footguns in the future. Note that by catching the error here and not in `validate_data_columns` + // the error becomes non-attributable. + if kzg_proofs.len() != expected_len + || indices.len() != expected_len + || kzg_commitments.len() != expected_len + { + return Err(( + None, + Error::InconsistentArrayLength("Invalid data column".to_string()), + )); } + + for (((cell, proof), &index), commitment) in cells + .iter() + .zip(kzg_proofs.iter()) + .zip(indices.iter()) + .zip(kzg_commitments.iter()) + { + column_groups + .entry(index) + .or_default() + .push((cell, *proof, *commitment)); + } + + let span = Span::current(); + column_groups + .into_par_iter() + .map(|(column_index, column_data)| { + let mut cells = Vec::new(); + let mut proofs = Vec::new(); + let mut commitments = Vec::new(); + + for (cell, proof, commitment) in &column_data { + cells.push(*cell); + proofs.push(proof.as_ref()); + commitments.push(commitment.as_ref()); + } + + // Create per-chunk tracing span for visualizing parallel processing. + // This is safe from span explosion as we have at most 128 chunks, + // i.e. the number of column indices. + let _span = tracing::debug_span!( + parent: span.clone(), + "verify_cell_proof_chunk", + cells = cells.len(), + column_index, + verification_result = tracing::field::Empty, + ) + .entered(); + + let verification_result = self.context().verify_cell_kzg_proof_batch( + commitments, + &vec![column_index; cells.len()], // All column_data here is from the same index + cells, + proofs, + ); + + match verification_result { + Ok(_) => Ok(()), + Err(e) if e.is_proof_invalid() => { + Err((Some(column_index), Error::KzgVerificationFailed)) + } + Err(e) => Err((Some(column_index), Error::PeerDASKZG(e))), + } + }) + .collect::, (Option, Error)>>()?; + + Ok(()) } pub fn recover_cells_and_compute_kzg_proofs( diff --git a/crypto/kzg/src/trusted_setup.rs b/crypto/kzg/src/trusted_setup.rs index 7aaa1d9919..75884b8199 100644 --- a/crypto/kzg/src/trusted_setup.rs +++ b/crypto/kzg/src/trusted_setup.rs @@ -1,10 +1,14 @@ -use crate::PeerDASTrustedSetup; -use c_kzg::{BYTES_PER_G1_POINT, BYTES_PER_G2_POINT}; +use crate::{Error, PeerDASTrustedSetup}; use serde::{ de::{self, Deserializer, Visitor}, Deserialize, Serialize, }; +// Number of bytes per G1 point. +const BYTES_PER_G1_POINT: usize = 48; +// Number of bytes per G2 point. +const BYTES_PER_G2_POINT: usize = 96; + pub const TRUSTED_SETUP_BYTES: &[u8] = include_bytes!("../trusted_setup.json"); pub fn get_trusted_setup() -> Vec { @@ -23,52 +27,31 @@ struct G2Point([u8; BYTES_PER_G2_POINT]); /// `c_kzg::KzgSettings` object. /// /// The serialize/deserialize implementations are written according to -/// the format specified in the the ethereum consensus specs trusted setup files. +/// the format specified in the ethereum consensus specs trusted setup files. /// /// See https://github.com/ethereum/consensus-specs/blob/dev/presets/mainnet/trusted_setups/trusted_setup_4096.json #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TrustedSetup { - #[serde(rename = "g1_monomial")] - g1_monomial_points: Vec, - #[serde(rename = "g1_lagrange")] - g1_points: Vec, - #[serde(rename = "g2_monomial")] - g2_points: Vec, + g1_monomial: Vec, + g1_lagrange: Vec, + g2_monomial: Vec, } impl TrustedSetup { - pub fn g1_points(&self) -> Vec<[u8; BYTES_PER_G1_POINT]> { - self.g1_points.iter().map(|p| p.0).collect() + pub fn g1_monomial(&self) -> Vec { + self.g1_monomial.iter().flat_map(|p| p.0).collect() } - pub fn g2_points(&self) -> Vec<[u8; BYTES_PER_G2_POINT]> { - self.g2_points.iter().map(|p| p.0).collect() + pub fn g1_lagrange(&self) -> Vec { + self.g1_lagrange.iter().flat_map(|p| p.0).collect() + } + + pub fn g2_monomial(&self) -> Vec { + self.g2_monomial.iter().flat_map(|p| p.0).collect() } pub fn g1_len(&self) -> usize { - self.g1_points.len() - } -} - -impl From<&TrustedSetup> for PeerDASTrustedSetup { - fn from(trusted_setup: &TrustedSetup) -> Self { - Self { - g1_monomial: trusted_setup - .g1_monomial_points - .iter() - .map(|g1_point| format!("0x{}", hex::encode(g1_point.0))) - .collect::>(), - g1_lagrange: trusted_setup - .g1_points - .iter() - .map(|g1_point| format!("0x{}", hex::encode(g1_point.0))) - .collect::>(), - g2_monomial: trusted_setup - .g2_points - .iter() - .map(|g2_point| format!("0x{}", hex::encode(g2_point.0))) - .collect::>(), - } + self.g1_lagrange.len() } } @@ -171,3 +154,20 @@ fn strip_prefix(s: &str) -> &str { s } } + +/// Loads the trusted setup from JSON. +/// +/// ## Note: +/// Currently we load both c-kzg and rust-eth-kzg trusted setup structs, because c-kzg is still being +/// used for 4844. Longer term we're planning to switch all KZG operations to the rust-eth-kzg +/// crate, and we'll be able to maintain a single trusted setup struct. +pub(crate) fn load_trusted_setup( + trusted_setup: &[u8], +) -> Result<(TrustedSetup, PeerDASTrustedSetup), Error> { + let ckzg_trusted_setup: TrustedSetup = serde_json::from_slice(trusted_setup) + .map_err(|e| Error::TrustedSetupError(format!("{e:?}")))?; + let trusted_setup_json = std::str::from_utf8(trusted_setup) + .map_err(|e| Error::TrustedSetupError(format!("{e:?}")))?; + let rkzg_trusted_setup = PeerDASTrustedSetup::from_json(trusted_setup_json); + Ok((ckzg_trusted_setup, rkzg_trusted_setup)) +} diff --git a/database_manager/src/cli.rs b/database_manager/src/cli.rs index c62da1206f..cb332546f9 100644 --- a/database_manager/src/cli.rs +++ b/database_manager/src/cli.rs @@ -1,6 +1,6 @@ pub use clap::{Arg, ArgAction, Args, Command, FromArgMatches, Parser}; -use clap_utils::get_color_style; use clap_utils::FLAG_HEADER; +use clap_utils::get_color_style; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use store::hdiff::HierarchyConfig; diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index f38c28d8b0..608400fa7e 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -3,10 +3,9 @@ use crate::cli::DatabaseManager; use crate::cli::Migrate; use crate::cli::PruneStates; use beacon_chain::{ - builder::Witness, eth1_chain::CachingEth1Backend, schema_change::migrate_schema, - slot_clock::SystemTimeSlotClock, + builder::Witness, schema_change::migrate_schema, slot_clock::SystemTimeSlotClock, }; -use beacon_node::{get_data_dir, ClientConfig}; +use beacon_node::{ClientConfig, get_data_dir}; use clap::ArgMatches; use clap::ValueEnum; use cli::{Compact, Inspect}; @@ -17,12 +16,12 @@ use std::io::Write; use std::path::PathBuf; use store::KeyValueStore; use store::{ + DBColumn, HotColdDB, database::interface::BeaconNodeBackend, errors::Error, - metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, - DBColumn, HotColdDB, + metadata::{CURRENT_SCHEMA_VERSION, SchemaVersion}, }; -use strum::{EnumString, EnumVariantNames}; +use strum::{EnumString, VariantNames}; use tracing::{info, warn}; use types::{BeaconState, EthSpec, Slot}; @@ -81,7 +80,7 @@ pub fn display_db_version( } #[derive( - Debug, PartialEq, Eq, Clone, EnumString, Deserialize, Serialize, EnumVariantNames, ValueEnum, + Debug, PartialEq, Eq, Clone, EnumString, Deserialize, Serialize, VariantNames, ValueEnum, )] pub enum InspectTarget { #[strum(serialize = "sizes")] @@ -301,7 +300,6 @@ fn parse_migrate_config(migrate_config: &Migrate) -> Result( migrate_config: MigrateConfig, client_config: ClientConfig, - mut genesis_state: BeaconState, runtime_context: &RuntimeContext, ) -> Result<(), Error> { let spec = runtime_context.eth2_config.spec.clone(); @@ -329,13 +327,7 @@ pub fn migrate_db( "Migrating database schema" ); - let genesis_state_root = genesis_state.canonical_root()?; - migrate_schema::, _, _, _>>( - db, - Some(genesis_state_root), - from, - to, - ) + migrate_schema::>(db, from, to) } pub fn prune_payloads( @@ -487,8 +479,7 @@ pub fn run( match &db_manager_config.subcommand { cli::DatabaseManagerSubcommand::Migrate(migrate_config) => { let migrate_config = parse_migrate_config(migrate_config)?; - let genesis_state = get_genesis_state()?; - migrate_db(migrate_config, client_config, genesis_state, &context).map_err(format_err) + migrate_db(migrate_config, client_config, &context).map_err(format_err) } cli::DatabaseManagerSubcommand::Inspect(inspect_config) => { let inspect_config = parse_inspect_config(inspect_config)?; diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000000..398a173dfa --- /dev/null +++ b/deny.toml @@ -0,0 +1,23 @@ +# cargo-deny configuration for Lighthouse +# See https://embarkstudios.github.io/cargo-deny/ + +[bans] +# Allow multiple versions by default. Change this to "warn" to see all multiple versions. +multiple-versions = "allow" +deny = [ + { crate = "ethers", reason = "legacy Ethereum crate, use alloy instead" }, + { crate = "ethereum-types", reason = "legacy Ethereum crate, use alloy-primitives instead" }, + { crate = "protobuf", reason = "use quick-protobuf instead" }, + { crate = "derivative", reason = "use educe or derive_more instead" }, + { crate = "ark-ff", reason = "present in Cargo.lock but not needed by Lighthouse" }, + { crate = "strum", deny-multiple-versions = true, reason = "takes a long time to compile" }, + { crate = "reqwest", deny-multiple-versions = true, reason = "takes a long time to compile" } +] + +[sources] +unknown-registry = "deny" +unknown-git = "warn" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] + +[sources.allow-org] +github = ["sigp"] diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 9acbe2569c..43e361b60d 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,14 +1,16 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "7.1.0-beta.0" +version = { workspace = true } authors = ["Paul Hauner "] edition = { workspace = true } +[package.metadata.cargo-udeps.ignore] +normal = ["malloc_utils"] + [features] portable = ["bls/supranational-portable"] fake_crypto = ['bls/fake_crypto'] -jemalloc = ["malloc_utils/jemalloc"] [dependencies] account_utils = { workspace = true } @@ -17,7 +19,6 @@ bls = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } deposit_contract = { workspace = true } -env_logger = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } eth2_network_config = { workspace = true } @@ -25,11 +26,12 @@ eth2_wallet = { workspace = true } ethereum_hashing = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } +fixed_bytes = { workspace = true } hex = { workspace = true } lighthouse_network = { workspace = true } lighthouse_version = { workspace = true } log = { workspace = true } -malloc_utils = { workspace = true } +network_utils = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -43,5 +45,8 @@ tree_hash = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } -[package.metadata.cargo-udeps.ignore] -normal = ["malloc_utils"] +[target.'cfg(not(target_os = "windows"))'.dependencies] +malloc_utils = { workspace = true, features = ["jemalloc"] } + +[target.'cfg(target_os = "windows")'.dependencies] +malloc_utils = { workspace = true, features = [] } diff --git a/lcli/Dockerfile b/lcli/Dockerfile index 67bc290112..f1e4bd8ee0 100644 --- a/lcli/Dockerfile +++ b/lcli/Dockerfile @@ -1,7 +1,7 @@ # `lcli` requires the full project to be in scope, so this should be built either: # - from the `lighthouse` dir with the command: `docker build -f ./lcli/Dockerflie .` # - from the current directory with the command: `docker build -f ./Dockerfile ../` -FROM rust:1.84.0-bullseye AS builder +FROM rust:1.88.0-bullseye AS builder RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse ARG FEATURES diff --git a/lcli/src/block_root.rs b/lcli/src/block_root.rs index 3c07d4f9ef..497ce1a438 100644 --- a/lcli/src/block_root.rs +++ b/lcli/src/block_root.rs @@ -30,7 +30,7 @@ use crate::transition_blocks::load_from_ssz_with; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; -use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts, types::BlockId}; use eth2_network_config::Eth2NetworkConfig; use std::path::PathBuf; use std::time::{Duration, Instant}; diff --git a/lcli/src/check_deposit_data.rs b/lcli/src/check_deposit_data.rs index 47c2c7addf..e4d985b5eb 100644 --- a/lcli/src/check_deposit_data.rs +++ b/lcli/src/check_deposit_data.rs @@ -1,6 +1,6 @@ use clap::ArgMatches; use clap_utils::{parse_required, parse_ssz_required}; -use deposit_contract::{decode_eth1_tx_data, DEPOSIT_DATA_LEN}; +use deposit_contract::{DEPOSIT_DATA_LEN, decode_eth1_tx_data}; use tree_hash::TreeHash; pub fn run(matches: &ArgMatches) -> Result<(), String> { diff --git a/lcli/src/generate_bootnode_enr.rs b/lcli/src/generate_bootnode_enr.rs index e1acac12df..620539a95f 100644 --- a/lcli/src/generate_bootnode_enr.rs +++ b/lcli/src/generate_bootnode_enr.rs @@ -1,14 +1,16 @@ use clap::ArgMatches; +use fixed_bytes::FixedBytesExtended; use lighthouse_network::{ - discovery::{build_enr, CombinedKey, CombinedKeyExt, ENR_FILENAME}, + NETWORK_KEY_FILENAME, NetworkConfig, + discovery::{CombinedKey, ENR_FILENAME, build_enr}, libp2p::identity::secp256k1, - NetworkConfig, NETWORK_KEY_FILENAME, }; +use network_utils::enr_ext::CombinedKeyExt; use std::io::Write; use std::path::PathBuf; use std::{fs, net::Ipv4Addr}; use std::{fs::File, num::NonZeroU16}; -use types::{ChainSpec, EnrForkId, Epoch, EthSpec, FixedBytesExtended, Hash256}; +use types::{ChainSpec, EnrForkId, Epoch, EthSpec, Hash256}; pub fn run(matches: &ArgMatches, spec: &ChainSpec) -> Result<(), String> { let ip: Ipv4Addr = clap_utils::parse_required(matches, "ip")?; @@ -32,13 +34,21 @@ pub fn run(matches: &ArgMatches, spec: &ChainSpec) -> Result<(), Str let secp256k1_keypair = secp256k1::Keypair::generate(); let enr_key = CombinedKey::from_secp256k1(&secp256k1_keypair); + let genesis_fork_digest = spec.compute_fork_digest(Hash256::zero(), Epoch::new(0)); let enr_fork_id = EnrForkId { - fork_digest: ChainSpec::compute_fork_digest(genesis_fork_version, Hash256::zero()), + fork_digest: genesis_fork_digest, next_fork_version: genesis_fork_version, next_fork_epoch: Epoch::max_value(), // FAR_FUTURE_EPOCH }; - let enr = build_enr::(&enr_key, &config, &enr_fork_id, spec) - .map_err(|e| format!("Unable to create ENR: {:?}", e))?; + let enr = build_enr::( + &enr_key, + &config, + &enr_fork_id, + spec.custody_requirement, + genesis_fork_digest, + spec, + ) + .map_err(|e| format!("Unable to create ENR: {:?}", e))?; fs::create_dir_all(&output_dir).map_err(|e| format!("Unable to create output-dir: {:?}", e))?; diff --git a/lcli/src/http_sync.rs b/lcli/src/http_sync.rs index cb6a9d2b1d..6a0eb2a0e1 100644 --- a/lcli/src/http_sync.rs +++ b/lcli/src/http_sync.rs @@ -2,8 +2,8 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{ - types::{BlockId, ChainSpec, ForkName, PublishBlockRequest, SignedBlockContents}, BeaconNodeHttpClient, Error, SensitiveUrl, Timeouts, + types::{BlockId, ChainSpec, ForkName, PublishBlockRequest, SignedBlockContents}, }; use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; @@ -64,11 +64,11 @@ pub async fn run_async( next_block_id = BlockId::Root(block.parent_root()); blocks.push((block.slot(), publish_block_req)); - if let Some(ref common_ancestor_block) = maybe_common_ancestor_block { - if common_ancestor_block == &next_block_id { - println!("reached known common ancestor: {next_block_id:?}"); - break; - } + if let Some(ref common_ancestor_block) = maybe_common_ancestor_block + && common_ancestor_block == &next_block_id + { + println!("reached known common ancestor: {next_block_id:?}"); + break; } let block_exists_in_target = target @@ -86,12 +86,13 @@ pub async fn run_async( for (slot, block) in blocks.iter().rev() { println!("posting block at slot {slot}"); if let Err(e) = target.post_beacon_blocks(block).await { - if let Error::ServerMessage(ref e) = e { - if e.code == 202 { - println!("duplicate block detected while posting block at slot {slot}"); - continue; - } + if let Error::ServerMessage(ref e) = e + && e.code == 202 + { + println!("duplicate block detected while posting block at slot {slot}"); + continue; } + return Err(format!("error posting {slot}: {e:?}")); } else { println!("success"); @@ -123,7 +124,7 @@ async fn get_block_from_source( .unwrap() .unwrap(); let blobs_from_source = source - .get_blobs::(block_id, None, spec) + .get_blob_sidecars::(block_id, None, spec) .await .unwrap() .unwrap() @@ -131,15 +132,14 @@ async fn get_block_from_source( let (kzg_proofs, blobs): (Vec<_>, Vec<_>) = blobs_from_source .iter() - .cloned() .map(|sidecar| (sidecar.kzg_proof, sidecar.blob.clone())) .unzip(); let block_root = block_from_source.canonical_root(); let block_contents = SignedBlockContents { signed_block: Arc::new(block_from_source), - kzg_proofs: kzg_proofs.into(), - blobs: blobs.into(), + kzg_proofs: kzg_proofs.try_into().unwrap(), + blobs: blobs.try_into().unwrap(), }; let publish_block_req = PublishBlockRequest::BlockContents(block_contents); diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 05f4900c46..a21dfd4386 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -11,19 +11,17 @@ mod state_root; mod transition_blocks; use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::{parse_optional, FLAG_HEADER}; +use clap_utils::{FLAG_HEADER, parse_optional}; use environment::{EnvironmentBuilder, LoggerConfig}; use eth2_network_config::Eth2NetworkConfig; use parse_ssz::run_parse_ssz; use std::path::PathBuf; use std::process; use std::str::FromStr; -use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::{filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt}; use types::{EthSpec, EthSpecId}; fn main() { - env_logger::init(); - let matches = Command::new("Lighthouse CLI Tool") .version(lighthouse_version::VERSION) .display_order(0) @@ -653,7 +651,7 @@ fn main() { } fn run(env_builder: EnvironmentBuilder, matches: &ArgMatches) -> Result<(), String> { - let (env_builder, _file_logging_layer, _stdout_logging_layer, _sse_logging_layer_opt) = + let (env_builder, file_logging_layer, stdout_logging_layer, _sse_logging_layer_opt) = env_builder .multi_threaded_tokio_runtime() .map_err(|e| format!("should start tokio runtime: {:?}", e))? @@ -675,12 +673,25 @@ fn run(env_builder: EnvironmentBuilder, matches: &ArgMatches) -> extra_info: false, }, "", + 0o600, ); let env = env_builder .build() .map_err(|e| format!("should build env: {:?}", e))?; + let mut logging_layers = vec![file_logging_layer]; + if let Some(stdout) = stdout_logging_layer { + logging_layers.push(stdout); + } + let logging_result = tracing_subscriber::registry() + .with(logging_layers) + .try_init(); + + if let Err(e) = logging_result { + eprintln!("Failed to initialize logger: {e}"); + } + // Determine testnet-dir path or network name depending on CLI flags. let (testnet_dir, network_name) = if let Some(testnet_dir) = parse_optional::(matches, "testnet-dir")? { diff --git a/lcli/src/mnemonic_validators.rs b/lcli/src/mnemonic_validators.rs index 2653aee149..cc1f0cc2c7 100644 --- a/lcli/src/mnemonic_validators.rs +++ b/lcli/src/mnemonic_validators.rs @@ -1,9 +1,9 @@ -use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; +use account_utils::eth2_keystore::{Keystore, KeystoreBuilder, keypair_from_secret}; use account_utils::random_password; use clap::ArgMatches; use eth2_wallet::bip39::Seed; use eth2_wallet::bip39::{Language, Mnemonic}; -use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType}; +use eth2_wallet::{KeyType, recover_validator_secret_from_mnemonic}; use rayon::prelude::*; use std::fs; use std::path::PathBuf; diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index b77f37a59f..b54c136f43 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -4,7 +4,7 @@ use environment::Environment; use execution_layer::{ auth::JwtKey, test_utils::{ - Config, MockExecutionConfig, MockServer, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, + Config, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, MockExecutionConfig, MockServer, }, }; use std::net::Ipv4Addr; @@ -22,6 +22,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< let prague_time = parse_optional(matches, "prague-time")?; let eip7805_time = parse_optional(matches, "eip7805-time")?; let osaka_time = parse_optional(matches, "osaka-time")?; + let amsterdam_time = parse_optional(matches, "amsterdam-time")?; let handle = env.core_context().executor.handle().unwrap(); let spec = Arc::new(E::default_spec()); @@ -42,9 +43,10 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< cancun_time, prague_time, osaka_time, + amsterdam_time, }; let kzg = None; - let server: MockServer = MockServer::new_with_config(&handle, config, spec, kzg); + let server: MockServer = MockServer::new_with_config(&handle, config, kzg); if all_payloads_valid { eprintln!( diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 9456f34570..88332c1a85 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -48,11 +48,11 @@ use crate::transition_blocks::load_from_ssz_with; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; -use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts, types::StateId}; use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; -use state_processing::state_advance::{complete_state_advance, partial_state_advance}; use state_processing::AllCaches; +use state_processing::state_advance::{complete_state_advance, partial_state_advance}; use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; diff --git a/lcli/src/state_root.rs b/lcli/src/state_root.rs index 7b10ab9362..b4bbae36c8 100644 --- a/lcli/src/state_root.rs +++ b/lcli/src/state_root.rs @@ -2,7 +2,7 @@ use crate::transition_blocks::load_from_ssz_with; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; -use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts, types::StateId}; use eth2_network_config::Eth2NetworkConfig; use std::path::PathBuf; use std::time::{Duration, Instant}; diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 2226105c34..69d3975d09 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -68,15 +68,15 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{ - types::{BlockId, StateId}, BeaconNodeHttpClient, SensitiveUrl, Timeouts, + types::{BlockId, StateId}, }; use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; use state_processing::state_advance::complete_state_advance; use state_processing::{ - block_signature_verifier::BlockSignatureVerifier, per_block_processing, AllCaches, - BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + AllCaches, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + block_signature_verifier::BlockSignatureVerifier, per_block_processing, }; use std::borrow::Cow; use std::fs::File; @@ -184,7 +184,7 @@ pub fn run( return Err( "must supply *both* --pre-state-path and --block-path *or* only --beacon-url" .into(), - ) + ); } }; @@ -354,10 +354,9 @@ fn do_transition( let mut ctxt = if let Some(ctxt) = saved_ctxt { ctxt.clone() } else { - let ctxt = ConsensusContext::new(pre_state.slot()) + ConsensusContext::new(pre_state.slot()) .set_current_block_root(block_root) - .set_proposer_index(block.message().proposer_index()); - ctxt + .set_proposer_index(block.message().proposer_index()) }; if !config.no_signature_verification { diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 04c8efcdba..ebe00c9be5 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,10 +1,15 @@ [package] name = "lighthouse" -version = "7.1.0-beta.0" +version = { workspace = true } authors = ["Sigma Prime "] edition = { workspace = true } autotests = false -rust-version = "1.83.0" +rust-version = "1.88.0" + +# Prevent cargo-udeps from flagging the dummy package `target_check`, which exists only +# to assert properties of the compilation target. +[package.metadata.cargo-udeps.ignore] +normal = ["target_check"] [features] default = ["slasher-lmdb", "beacon-node-leveldb"] @@ -28,15 +33,10 @@ slasher-redb = ["slasher/redb"] beacon-node-leveldb = ["store/leveldb"] # Supports beacon node redb backend. beacon-node-redb = ["store/redb"] - -# Deprecated. This is now enabled by default on non windows targets. -jemalloc = [] - -[target.'cfg(not(target_os = "windows"))'.dependencies] -malloc_utils = { workspace = true, features = ["jemalloc"] } - -[target.'cfg(target_os = "windows")'.dependencies] -malloc_utils = { workspace = true } +# Supports console subscriber for debugging +console-subscriber = ["console-subscriber/default"] +# Force the use of the system memory allocator rather than jemalloc. +sysmalloc = ["malloc_utils/sysmalloc"] [dependencies] account_manager = { "path" = "../account_manager" } @@ -46,16 +46,21 @@ bls = { workspace = true } boot_node = { path = "../boot_node" } clap = { workspace = true } clap_utils = { workspace = true } +console-subscriber = { workspace = true, optional = true } database_manager = { path = "../database_manager" } directory = { workspace = true } environment = { workspace = true } eth2_network_config = { workspace = true } ethereum_hashing = { workspace = true } futures = { workspace = true } +lighthouse_tracing = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } -malloc_utils = { workspace = true } metrics = { workspace = true } +network_utils = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-otlp = { workspace = true } +opentelemetry_sdk = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } @@ -63,16 +68,22 @@ slasher = { workspace = true } store = { workspace = true } task_executor = { workspace = true } tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true } types = { workspace = true } -unused_port = { workspace = true } validator_client = { workspace = true } validator_manager = { path = "../validator_manager" } +[target.'cfg(not(target_os = "windows"))'.dependencies] +malloc_utils = { workspace = true, features = ["jemalloc"] } + +[target.'cfg(target_os = "windows")'.dependencies] +malloc_utils = { workspace = true, features = [] } + [dev-dependencies] +beacon_node = { workspace = true, features = ["testing"] } beacon_node_fallback = { workspace = true } beacon_processor = { workspace = true } -eth1 = { workspace = true } eth2 = { workspace = true } initialized_validators = { workspace = true } lighthouse_network = { workspace = true } @@ -85,8 +96,3 @@ zeroize = { workspace = true } [[test]] name = "lighthouse_tests" path = "tests/main.rs" - -# Prevent cargo-udeps from flagging the dummy package `target_check`, which exists only -# to assert properties of the compilation target. -[package.metadata.cargo-udeps.ignore] -normal = ["target_check"] diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 9b0284e06d..13a5a7a803 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -9,10 +9,10 @@ use eth2_config::Eth2Config; use eth2_network_config::Eth2NetworkConfig; -use futures::channel::mpsc::{channel, Receiver, Sender}; -use futures::{future, StreamExt}; -use logging::tracing_logging_layer::LoggingLayer; +use futures::channel::mpsc::{Receiver, Sender, channel}; +use futures::{StreamExt, future}; use logging::SSELoggingComponents; +use logging::tracing_logging_layer::LoggingLayer; use logroller::{Compression, LogRollerBuilder, Rotation, RotationSize}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -26,15 +26,8 @@ use types::{EthSpec, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; #[cfg(target_family = "unix")] use { futures::Future, - std::{ - fs::{read_dir, set_permissions, Permissions}, - os::unix::fs::PermissionsExt, - path::Path, - pin::Pin, - task::Context, - task::Poll, - }, - tokio::signal::unix::{signal, Signal, SignalKind}, + std::{pin::Pin, task::Context, task::Poll}, + tokio::signal::unix::{Signal, SignalKind, signal}, }; #[cfg(not(target_family = "unix"))] @@ -208,6 +201,7 @@ impl EnvironmentBuilder { mut self, config: LoggerConfig, logfile_prefix: &str, + file_mode: u32, ) -> ( Self, LoggingLayer, @@ -220,9 +214,6 @@ impl EnvironmentBuilder { _ => logfile_prefix, }; - #[cfg(target_family = "unix")] - let file_mode = if config.is_restricted { 0o600 } else { 0o644 }; - let file_logging_layer = match config.path { None => { eprintln!("No logfile path provided, logging to file is disabled"); @@ -239,7 +230,8 @@ impl EnvironmentBuilder { .max_keep_files(config.max_log_number.try_into().unwrap_or_else(|e| { eprintln!("Failed to convert max_log_number to u64: {}", e); 10 - })); + })) + .file_mode(file_mode); if config.compression { appender = appender.compression(Compression::Gzip); @@ -247,9 +239,6 @@ impl EnvironmentBuilder { match appender.build() { Ok(file_appender) => { - #[cfg(target_family = "unix")] - set_logfile_permissions(&path, filename_prefix, file_mode); - let (writer, guard) = tracing_appender::non_blocking(file_appender); Some(LoggingLayer::new( writer, @@ -543,37 +532,3 @@ impl Future for SignalFuture { } } } - -#[cfg(target_family = "unix")] -fn set_logfile_permissions(log_dir: &Path, filename_prefix: &str, file_mode: u32) { - let newest = read_dir(log_dir) - .ok() - .into_iter() - .flat_map(|entries| entries.filter_map(Result::ok)) - .filter_map(|entry| { - let path = entry.path(); - let fname = path.file_name()?.to_string_lossy(); - if path.is_file() && fname.starts_with(filename_prefix) && fname.ends_with(".log") { - let modified = entry.metadata().ok()?.modified().ok()?; - Some((path, modified)) - } else { - None - } - }) - .max_by_key(|(_path, mtime)| *mtime); - - match newest { - Some((file, _mtime)) => { - if let Err(e) = set_permissions(&file, Permissions::from_mode(file_mode)) { - eprintln!("Failed to set permissions on {}: {}", file.display(), e); - } - } - None => { - eprintln!( - "Couldn't find a newly created logfile in {} matching prefix \"{}\".", - log_dir.display(), - filename_prefix - ); - } - } -} diff --git a/lighthouse/environment/src/tracing_common.rs b/lighthouse/environment/src/tracing_common.rs index b1e5078af1..5ba014f759 100644 --- a/lighthouse/environment/src/tracing_common.rs +++ b/lighthouse/environment/src/tracing_common.rs @@ -2,7 +2,7 @@ use crate::{EnvironmentBuilder, LoggerConfig}; use clap::ArgMatches; use logging::Libp2pDiscv5TracingLayer; use logging::{ - create_libp2p_discv5_tracing_layer, tracing_logging_layer::LoggingLayer, SSELoggingComponents, + SSELoggingComponents, create_libp2p_discv5_tracing_layer, tracing_logging_layer::LoggingLayer, }; use std::process; @@ -33,8 +33,14 @@ pub fn construct_logger( let subcommand_name = matches.subcommand_name(); let logfile_prefix = subcommand_name.unwrap_or("lighthouse"); + let file_mode = if logger_config.is_restricted { + 0o600 + } else { + 0o644 + }; + let (builder, stdout_logging_layer, file_logging_layer, sse_logging_layer_opt) = - environment_builder.init_tracing(logger_config.clone(), logfile_prefix); + environment_builder.init_tracing(logger_config.clone(), logfile_prefix, file_mode); let libp2p_discv5_layer = if let Some(subcommand_name) = subcommand_name { if subcommand_name == "beacon_node" @@ -49,6 +55,7 @@ pub fn construct_logger( create_libp2p_discv5_tracing_layer( logger_config.path.clone(), logger_config.max_log_size, + file_mode, ) } } else { diff --git a/lighthouse/environment/tests/environment_builder.rs b/lighthouse/environment/tests/environment_builder.rs index a98caf8df5..71acafeca8 100644 --- a/lighthouse/environment/tests/environment_builder.rs +++ b/lighthouse/environment/tests/environment_builder.rs @@ -1,7 +1,7 @@ #![cfg(test)] use environment::EnvironmentBuilder; -use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK}; +use eth2_network_config::{DEFAULT_HARDCODED_NETWORK, Eth2NetworkConfig}; use std::path::PathBuf; use types::{Config, MainnetEthSpec}; diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index 3f72e2ea6c..24c4a67225 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -101,5 +101,4 @@ ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # DAS CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 -NUMBER_OF_COLUMNS: 128 SAMPLES_PER_SLOT: 8 \ No newline at end of file diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 7ddf04db01..c93016a0f5 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -7,26 +7,29 @@ use clap::FromArgMatches; use clap::Subcommand; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::{ - flags::DISABLE_MALLOC_TUNING_FLAG, get_color_style, get_eth2_network_config, FLAG_HEADER, + FLAG_HEADER, flags::DISABLE_MALLOC_TUNING_FLAG, get_color_style, get_eth2_network_config, }; use cli::LighthouseSubcommands; -use directory::{parse_path_or_default, DEFAULT_BEACON_NODE_DIR, DEFAULT_VALIDATOR_DIR}; +use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_VALIDATOR_DIR, parse_path_or_default}; use environment::tracing_common; use environment::{EnvironmentBuilder, LoggerConfig}; -use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK, HARDCODED_NET_NAMES}; +use eth2_network_config::{DEFAULT_HARDCODED_NETWORK, Eth2NetworkConfig, HARDCODED_NET_NAMES}; use ethereum_hashing::have_sha_extensions; use futures::TryFutureExt; use lighthouse_version::VERSION; -use logging::{build_workspace_filter, crit, MetricsLayer}; +use logging::{MetricsLayer, build_workspace_filter, crit}; use malloc_utils::configure_memory_allocator; +use opentelemetry::trace::TracerProvider; +use opentelemetry_otlp::tonic_types::transport::ClientTlsConfig; +use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; use std::backtrace::Backtrace; use std::io::IsTerminal; use std::path::PathBuf; use std::process::exit; use std::sync::LazyLock; use task_executor::ShutdownReason; -use tracing::{info, warn, Level}; -use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt, Layer}; +use tracing::{Level, info}; +use tracing_subscriber::{Layer, filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use types::{EthSpec, EthSpecId}; use validator_client::ProductionValidatorClient; @@ -74,15 +77,7 @@ fn bls_hardware_acceleration() -> bool { } fn allocator_name() -> String { - #[cfg(target_os = "windows")] - { - "system".to_string() - } - #[cfg(not(target_os = "windows"))] - match malloc_utils::jemalloc::page_size() { - Ok(page_size) => format!("jemalloc ({}K)", page_size / 1024), - Err(e) => format!("jemalloc (error: {e:?})"), - } + malloc_utils::allocator_name() } fn build_profile_name() -> String { @@ -99,7 +94,12 @@ fn build_profile_name() -> String { fn main() { // Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided. if std::env::var("RUST_BACKTRACE").is_err() { - std::env::set_var("RUST_BACKTRACE", "1"); + // `set_var` is marked unsafe because it is unsafe to use if there are multiple threads + // reading or writing from the environment. We are at the very beginning of execution and + // have not spun up any threads or the tokio runtime, so it is safe to use. + unsafe { + std::env::set_var("RUST_BACKTRACE", "1"); + } } // Parse the CLI parameters. @@ -268,6 +268,32 @@ fn main() { .default_value("info") .display_order(0) ) + .arg( + Arg::new("telemetry-collector-url") + .long("telemetry-collector-url") + .value_name("URL") + .help( + "URL of the OpenTelemetry collector to export tracing spans \ + (e.g., http://localhost:4317). If not set, tracing export is disabled.", + ) + .action(ArgAction::Set) + .global(true) + .display_order(0) + ) + .arg( + Arg::new("telemetry-service-name") + .long("telemetry-service-name") + .value_name("NAME") + .help( + "Override the OpenTelemetry service name. \ + Defaults to 'lighthouse-bn' for beacon node, 'lighthouse-vc' for validator \ + client, or 'lighthouse' for other subcommands." + ) + .requires("telemetry-collector-url") + .action(ArgAction::Set) + .global(true) + .display_order(0) + ) .arg( Arg::new("datadir") .long("datadir") @@ -349,48 +375,6 @@ fn main() { .global(true) .display_order(0) ) - .arg( - Arg::new("terminal-total-difficulty-override") - .long("terminal-total-difficulty-override") - .value_name("INTEGER") - .help("DEPRECATED") - .action(ArgAction::Set) - .global(true) - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("terminal-block-hash-override") - .long("terminal-block-hash-override") - .value_name("TERMINAL_BLOCK_HASH") - .help("DEPRECATED") - .requires("terminal-block-hash-epoch-override") - .action(ArgAction::Set) - .global(true) - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("terminal-block-hash-epoch-override") - .long("terminal-block-hash-epoch-override") - .value_name("EPOCH") - .help("DEPRECATED") - .requires("terminal-block-hash-override") - .action(ArgAction::Set) - .global(true) - .display_order(0) - .hide(true) - ) - .arg( - Arg::new("safe-slots-to-import-optimistically") - .long("safe-slots-to-import-optimistically") - .value_name("INTEGER") - .help("DEPRECATED") - .action(ArgAction::Set) - .global(true) - .display_order(0) - .hide(true) - ) .arg( Arg::new("genesis-state-url") .long("genesis-state-url") @@ -442,15 +426,16 @@ fn main() { // Only apply this optimization for the beacon node. It's the only process with a substantial // memory footprint. let is_beacon_node = matches.subcommand_name() == Some("beacon_node"); - if is_beacon_node && !matches.get_flag(DISABLE_MALLOC_TUNING_FLAG) { - if let Err(e) = configure_memory_allocator() { - eprintln!( - "Unable to configure the memory allocator: {} \n\ + if is_beacon_node + && !matches.get_flag(DISABLE_MALLOC_TUNING_FLAG) + && let Err(e) = configure_memory_allocator() + { + eprintln!( + "Unable to configure the memory allocator: {} \n\ Try providing the --{} flag", - e, DISABLE_MALLOC_TUNING_FLAG - ); - exit(1) - } + e, DISABLE_MALLOC_TUNING_FLAG + ); + exit(1) } let result = get_eth2_network_config(&matches).and_then(|eth2_network_config| { @@ -640,13 +625,17 @@ fn run( logging_layers.push( file_logging_layer .with_filter(logger_config.logfile_debug_level) - .with_filter(workspace_filter) + .with_filter(workspace_filter.clone()) .boxed(), ); } if let Some(sse_logging_layer) = sse_logging_layer_opt { - logging_layers.push(sse_logging_layer.boxed()); + logging_layers.push( + sse_logging_layer + .with_filter(workspace_filter.clone()) + .boxed(), + ); } if let Some(libp2p_discv5_layer) = libp2p_discv5_layer { @@ -663,6 +652,55 @@ fn run( logging_layers.push(MetricsLayer.boxed()); + let mut environment = builder + .multi_threaded_tokio_runtime()? + .eth2_network_config(eth2_network_config)? + .build()?; + + if let Some(telemetry_collector_url) = matches.get_one::("telemetry-collector-url") { + let telemetry_layer = environment.runtime().block_on(async { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_tls_config(ClientTlsConfig::new().with_native_roots()) + .with_endpoint(telemetry_collector_url) + .build() + .map_err(|e| format!("Failed to create OTLP exporter: {:?}", e))?; + + let service_name = matches + .get_one::("telemetry-service-name") + .cloned() + .unwrap_or_else(|| match matches.subcommand() { + Some(("beacon_node", _)) => "lighthouse-bn".to_string(), + Some(("validator_client", _)) => "lighthouse-vc".to_string(), + _ => "lighthouse".to_string(), + }); + + let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name(service_name) + .build(), + ) + .build(); + + let tracer = provider.tracer("lighthouse"); + Ok::<_, String>( + tracing_opentelemetry::layer() + .with_tracer(tracer) + .with_filter(workspace_filter), + ) + })?; + + logging_layers.push(telemetry_layer.boxed()); + } + + #[cfg(feature = "console-subscriber")] + { + let console_layer = console_subscriber::spawn(); + logging_layers.push(console_layer.boxed()); + } + let logging_result = tracing_subscriber::registry() .with(logging_layers) .try_init(); @@ -671,11 +709,6 @@ fn run( eprintln!("Failed to initialize logger: {e}"); } - let mut environment = builder - .multi_threaded_tokio_runtime()? - .eth2_network_config(eth2_network_config)? - .build()?; - // Log panics properly. { std::panic::set_hook(Box::new(move |info| { @@ -703,20 +736,6 @@ fn run( ); } - // Warn for DEPRECATED global flags. This code should be removed when we finish deleting these - // flags. - let deprecated_flags = [ - "terminal-total-difficulty-override", - "terminal-block-hash-override", - "terminal-block-hash-epoch-override", - "safe-slots-to-import-optimistically", - ]; - for flag in deprecated_flags { - if matches.get_one::(flag).is_some() { - warn!("The {} flag is deprecated and does nothing", flag); - } - } - // Note: the current code technically allows for starting a beacon node _and_ a validator // client at the same time. // diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index d53d042fa4..9bfcae85e5 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -1,31 +1,32 @@ use account_manager::{ + CMD as ACCOUNT_CMD, WALLETS_DIR_FLAG, validator::{ + CMD as VALIDATOR_CMD, create::*, import::{self, CMD as IMPORT_CMD}, modify::{ALL, CMD as MODIFY_CMD, DISABLE, ENABLE, PUBKEY_FLAG}, - CMD as VALIDATOR_CMD, }, wallet::{ + CMD as WALLET_CMD, create::{CMD as CREATE_CMD, *}, list::CMD as LIST_CMD, - CMD as WALLET_CMD, }, - CMD as ACCOUNT_CMD, WALLETS_DIR_FLAG, *, + *, }; use account_utils::{ + STDIN_INPUTS_FLAG, eth2_keystore::KeystoreBuilder, validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions}, - STDIN_INPUTS_FLAG, }; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use bls::{Keypair, PublicKey}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use std::env; use std::fs::{self, File}; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output, Stdio}; use std::str::from_utf8; -use tempfile::{tempdir, TempDir}; -use types::{Keypair, PublicKey}; +use tempfile::{TempDir, tempdir}; use validator_dir::ValidatorDir; use zeroize::Zeroizing; diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index ea4716c010..207324ea33 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1,15 +1,19 @@ use crate::exec::{CommandLineTestExec, CompletedTest}; use beacon_node::beacon_chain::chain_config::{ - DisallowedReOrgOffsets, DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, + DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_SYNC_TOLERANCE_EPOCHS, + DisallowedReOrgOffsets, }; +use beacon_node::beacon_chain::custody_context::NodeCustodyType; use beacon_node::{ - beacon_chain::graffiti_calculator::GraffitiOrigin, - beacon_chain::store::config::DatabaseBackend as BeaconNodeBackend, ClientConfig as Config, + ClientConfig as Config, beacon_chain::graffiti_calculator::GraffitiOrigin, + beacon_chain::store::config::DatabaseBackend as BeaconNodeBackend, }; use beacon_processor::BeaconProcessorConfig; -use eth1::Eth1Endpoint; use lighthouse_network::PeerId; +use network_utils::unused_port::{ + unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port, +}; use std::fs::File; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -22,7 +26,6 @@ use std::time::Duration; use tempfile::TempDir; use types::non_zero_usize::new_non_zero_usize; use types::{Address, Checkpoint, Epoch, Hash256, MainnetEthSpec}; -use unused_port::{unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port}; const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; const DEFAULT_EXECUTION_JWT_SECRET_KEY: &str = @@ -115,11 +118,6 @@ fn staking_flag() { .run_with_zero_port() .with_config(|config| { assert!(config.http_api.enabled); - assert!(config.sync_eth1_chain); - assert_eq!( - config.eth1.endpoint.get_endpoint().to_string(), - DEFAULT_EXECUTION_ENDPOINT - ); }); } @@ -395,53 +393,34 @@ fn genesis_backfill_with_historic_flag() { .with_config(|config| assert!(config.chain.genesis_backfill)); } -// Tests for Eth1 flags. -// DEPRECATED but should not crash #[test] -fn dummy_eth1_flag() { +fn complete_blob_backfill_default() { CommandLineTest::new() - .flag("dummy-eth1", None) - .run_with_zero_port(); -} -// DEPRECATED but should not crash -#[test] -fn eth1_flag() { - CommandLineTest::new() - .flag("eth1", None) .run_with_zero_port() - .with_config(|config| assert!(config.sync_eth1_chain)); + .with_config(|config| assert!(!config.chain.complete_blob_backfill)); } + #[test] -fn eth1_blocks_per_log_query_flag() { - CommandLineTest::new() - .flag("eth1-blocks-per-log-query", Some("500")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.eth1.blocks_per_log_query, 500)); -} -#[test] -fn eth1_purge_cache_flag() { - CommandLineTest::new() - .flag("eth1-purge-cache", None) - .run_with_zero_port() - .with_config(|config| assert!(config.eth1.purge_cache)); -} -#[test] -fn eth1_cache_follow_distance_default() { +fn complete_blob_backfill_flag() { CommandLineTest::new() + .flag("complete-blob-backfill", None) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.eth1.cache_follow_distance, None); - assert_eq!(config.eth1.cache_follow_distance(), 3 * 2048 / 4); + assert!(config.chain.complete_blob_backfill); + assert!(!config.store.prune_blobs); }); } + +// Even if `--prune-blobs true` is provided, `--complete-blob-backfill` should override it to false. #[test] -fn eth1_cache_follow_distance_manual() { +fn complete_blob_backfill_and_prune_blobs_true() { CommandLineTest::new() - .flag("eth1-cache-follow-distance", Some("128")) + .flag("complete-blob-backfill", None) + .flag("prune-blobs", Some("true")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.eth1.cache_follow_distance, Some(128)); - assert_eq!(config.eth1.cache_follow_distance(), 128); + assert!(config.chain.complete_blob_backfill); + assert!(!config.store.prune_blobs); }); } @@ -502,7 +481,12 @@ fn run_execution_jwt_secret_key_is_persisted() { .with_config(|config| { let config = config.execution_layer.as_ref().unwrap(); assert_eq!( - config.execution_endpoint.as_ref().unwrap().full.to_string(), + config + .execution_endpoint + .as_ref() + .unwrap() + .expose_full() + .to_string(), "http://localhost:8551/" ); let mut file_jwt_secret_key = String::new(); @@ -553,7 +537,12 @@ fn bellatrix_jwt_secrets_flag() { .with_config(|config| { let config = config.execution_layer.as_ref().unwrap(); assert_eq!( - config.execution_endpoint.as_ref().unwrap().full.to_string(), + config + .execution_endpoint + .as_ref() + .unwrap() + .expose_full() + .to_string(), "http://localhost:8551/" ); assert_eq!( @@ -755,8 +744,6 @@ fn test_builder_disable_ssz_flag() { } fn run_jwt_optional_flags_test(jwt_flag: &str, jwt_id_flag: &str, jwt_version_flag: &str) { - use sensitive_url::SensitiveUrl; - let dir = TempDir::new().expect("Unable to create temporary directory"); let execution_endpoint = "http://meow.cats"; let jwt_file = "jwt-file"; @@ -772,15 +759,6 @@ fn run_jwt_optional_flags_test(jwt_flag: &str, jwt_id_flag: &str, jwt_version_fl let el_config = config.execution_layer.as_ref().unwrap(); assert_eq!(el_config.jwt_id, Some(id.to_string())); assert_eq!(el_config.jwt_version, Some(version.to_string())); - assert_eq!( - config.eth1.endpoint, - Eth1Endpoint::Auth { - endpoint: SensitiveUrl::parse(execution_endpoint).unwrap(), - jwt_path: dir.path().join(jwt_file), - jwt_id: Some(id.to_string()), - jwt_version: Some(version.to_string()), - } - ); }); } #[test] @@ -791,31 +769,6 @@ fn jwt_optional_flags() { fn jwt_optional_alias_flags() { run_jwt_optional_flags_test("jwt-secrets", "jwt-id", "jwt-version"); } -// DEPRECATED. This flag is deprecated but should not cause a crash. -#[test] -fn terminal_total_difficulty_override_flag() { - CommandLineTest::new() - .flag("terminal-total-difficulty-override", Some("1337424242")) - .run_with_zero_port(); -} -// DEPRECATED. This flag is deprecated but should not cause a crash. -#[test] -fn terminal_block_hash_and_activation_epoch_override_flags() { - CommandLineTest::new() - .flag("terminal-block-hash-epoch-override", Some("1337")) - .flag( - "terminal-block-hash-override", - Some("0x4242424242424242424242424242424242424242424242424242424242424242"), - ) - .run_with_zero_port(); -} -// DEPRECATED. This flag is deprecated but should not cause a crash. -#[test] -fn safe_slots_to_import_optimistically_flag() { - CommandLineTest::new() - .flag("safe-slots-to-import-optimistically", Some("421337")) - .run_with_zero_port(); -} // Tests for Network flags. #[test] @@ -840,14 +793,38 @@ fn network_subscribe_all_data_column_subnets_flag() { CommandLineTest::new() .flag("subscribe-all-data-column-subnets", None) .run_with_zero_port() - .with_config(|config| assert!(config.network.subscribe_all_data_column_subnets)); + .with_config(|config| { + assert_eq!(config.chain.node_custody_type, NodeCustodyType::Supernode) + }); } #[test] -fn network_enable_sampling_flag() { +fn network_supernode_flag() { CommandLineTest::new() - .flag("enable-sampling", None) + .flag("supernode", None) .run_with_zero_port() - .with_config(|config| assert!(config.chain.enable_sampling)); + .with_config(|config| { + assert_eq!(config.chain.node_custody_type, NodeCustodyType::Supernode) + }); +} +#[test] +fn network_semi_supernode_flag() { + CommandLineTest::new() + .flag("semi-supernode", None) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.node_custody_type, + NodeCustodyType::SemiSupernode + ) + }); +} +#[test] +fn network_node_custody_type_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.chain.node_custody_type, NodeCustodyType::Fullnode) + }); } #[test] fn blob_publication_batches() { @@ -870,12 +847,6 @@ fn blob_publication_batch_interval() { }); } -#[test] -fn network_enable_sampling_flag_default() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| assert!(!config.chain.enable_sampling)); -} #[test] fn network_subscribe_all_subnets_flag() { CommandLineTest::new() @@ -1862,18 +1833,31 @@ fn slots_per_restore_point_flag() { .run_with_zero_port(); } +#[test] +fn block_cache_size_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert_eq!(config.store.block_cache_size, 0)); +} #[test] fn block_cache_size_flag() { CommandLineTest::new() .flag("block-cache-size", Some("4")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.store.block_cache_size, new_non_zero_usize(4))); + .with_config(|config| assert_eq!(config.store.block_cache_size, 4)); +} +#[test] +fn block_cache_size_zero() { + CommandLineTest::new() + .flag("block-cache-size", Some("0")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.store.block_cache_size, 0)); } #[test] fn state_cache_size_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert_eq!(config.store.state_cache_size, new_non_zero_usize(32))); + .with_config(|config| assert_eq!(config.store.state_cache_size, new_non_zero_usize(128))); } #[test] fn state_cache_size_flag() { @@ -1927,22 +1911,43 @@ fn hdiff_buffer_cache_size_flag() { .flag("hdiff-buffer-cache-size", Some("1")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.store.hdiff_buffer_cache_size.get(), 1); + assert_eq!(config.store.cold_hdiff_buffer_cache_size.get(), 1); }); } #[test] fn hdiff_buffer_cache_size_default() { - use beacon_node::beacon_chain::store::config::DEFAULT_HDIFF_BUFFER_CACHE_SIZE; + use beacon_node::beacon_chain::store::config::DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE; CommandLineTest::new() .run_with_zero_port() .with_config(|config| { assert_eq!( - config.store.hdiff_buffer_cache_size, - DEFAULT_HDIFF_BUFFER_CACHE_SIZE + config.store.cold_hdiff_buffer_cache_size, + DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE ); }); } #[test] +fn hot_hdiff_buffer_cache_size_default() { + use beacon_node::beacon_chain::store::config::DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE; + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.store.hot_hdiff_buffer_cache_size, + DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE + ); + }); +} +#[test] +fn hot_hdiff_buffer_cache_size_flag() { + CommandLineTest::new() + .flag("hot-hdiff-buffer-cache-size", Some("3")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.store.hot_hdiff_buffer_cache_size.get(), 3); + }); +} +#[test] fn auto_compact_db_flag() { CommandLineTest::new() .flag("auto-compact-db", Some("false")) @@ -2499,54 +2504,6 @@ fn logfile_format_flag() { ) }); } -#[test] -fn sync_eth1_chain_default() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| assert!(config.sync_eth1_chain)); -} - -#[test] -fn sync_eth1_chain_execution_endpoints_flag() { - let dir = TempDir::new().expect("Unable to create temporary directory"); - CommandLineTest::new_with_no_execution_endpoint() - .flag("execution-endpoints", Some("http://localhost:8551/")) - .flag( - "execution-jwt", - dir.path().join("jwt-file").as_os_str().to_str(), - ) - .run_with_zero_port() - .with_config(|config| assert!(config.sync_eth1_chain)); -} - -#[test] -fn sync_eth1_chain_disable_deposit_contract_sync_flag() { - let dir = TempDir::new().expect("Unable to create temporary directory"); - CommandLineTest::new_with_no_execution_endpoint() - .flag("disable-deposit-contract-sync", None) - .flag("execution-endpoints", Some("http://localhost:8551/")) - .flag( - "execution-jwt", - dir.path().join("jwt-file").as_os_str().to_str(), - ) - .run_with_zero_port() - .with_config(|config| assert!(!config.sync_eth1_chain)); -} - -#[test] -#[should_panic] -fn disable_deposit_contract_sync_conflicts_with_staking() { - let dir = TempDir::new().expect("Unable to create temporary directory"); - CommandLineTest::new_with_no_execution_endpoint() - .flag("disable-deposit-contract-sync", None) - .flag("staking", None) - .flag("execution-endpoints", Some("http://localhost:8551/")) - .flag( - "execution-jwt", - dir.path().join("jwt-file").as_os_str().to_str(), - ) - .run_with_zero_port(); -} #[test] fn light_client_server_default() { @@ -2561,7 +2518,6 @@ fn light_client_server_default() { #[test] fn light_client_server_enabled() { CommandLineTest::new() - .flag("light-client-server", None) .run_with_zero_port() .with_config(|config| { assert!(config.network.enable_light_client_server); @@ -2580,6 +2536,25 @@ fn light_client_server_disabled() { }); } +#[test] +fn get_blobs_disabled() { + CommandLineTest::new() + .flag("disable-get-blobs", None) + .run_with_zero_port() + .with_config(|config| { + assert!(config.chain.disable_get_blobs); + }); +} + +#[test] +fn get_blobs_enabled() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert!(!config.chain.disable_get_blobs); + }); +} + #[test] fn light_client_http_server_disabled() { CommandLineTest::new() @@ -2679,6 +2654,16 @@ fn invalid_gossip_verified_blocks_path() { }); } +#[test] +fn advertise_false_custody_group_count() { + CommandLineTest::new() + .flag("advertise-false-custody-group-count", Some("64")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.network.advertise_false_custody_group_count, Some(64)) + }); +} + #[test] fn beacon_processor() { CommandLineTest::new() @@ -2837,10 +2822,12 @@ fn invalid_block_roots_default_holesky() { .run_with_zero_port() .with_config(|config| { assert_eq!(config.chain.invalid_block_roots.len(), 1); - assert!(config - .chain - .invalid_block_roots - .contains(&*INVALID_HOLESKY_BLOCK_ROOT)); + assert!( + config + .chain + .invalid_block_roots + .contains(&*INVALID_HOLESKY_BLOCK_ROOT) + ); }) } diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index b243cd6001..38111ca0ef 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -3,8 +3,8 @@ use boot_node::config::BootNodeConfigSerialization; use crate::exec::{CommandLineTestExec, CompletedTest}; use clap::ArgMatches; use clap_utils::get_eth2_network_config; -use lighthouse_network::discovery::ENR_FILENAME; -use lighthouse_network::Enr; +use lighthouse_network::{Enr, discovery::ENR_FILENAME}; +use network_utils::unused_port::unused_udp4_port; use std::fs::File; use std::io::Write; use std::net::Ipv4Addr; @@ -12,7 +12,6 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; use tempfile::TempDir; -use unused_port::unused_udp4_port; const IP_ADDRESS: &str = "192.168.2.108"; diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index f99fc3c460..ee3e910b36 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -1,4 +1,4 @@ -use beacon_node_fallback::{beacon_node_health::BeaconNodeSyncDistanceTiers, ApiTopic}; +use beacon_node_fallback::{ApiTopic, beacon_node_health::BeaconNodeSyncDistanceTiers}; use crate::exec::CommandLineTestExec; use bls::{Keypair, PublicKeyBytes}; @@ -109,12 +109,12 @@ fn beacon_nodes_flag() { .run() .with_config(|config| { assert_eq!( - config.beacon_nodes[0].full.to_string(), + config.beacon_nodes[0].expose_full().to_string(), "http://localhost:1001/" ); assert_eq!(config.beacon_nodes[0].to_string(), "http://localhost:1001/"); assert_eq!( - config.beacon_nodes[1].full.to_string(), + config.beacon_nodes[1].expose_full().to_string(), "https://project:secret@infura.io/" ); assert_eq!(config.beacon_nodes[1].to_string(), "https://infura.io/"); @@ -505,7 +505,7 @@ fn no_doppelganger_protection_flag() { fn no_gas_limit_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(config.validator_store.gas_limit == Some(36_000_000))); + .with_config(|config| assert!(config.validator_store.gas_limit == Some(60_000_000))); } #[test] fn gas_limit_flag() { diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 04e3eafe6e..d6d720a561 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -1,3 +1,4 @@ +use bls::PublicKeyBytes; use eth2::SensitiveUrl; use serde::de::DeserializeOwned; use std::fs; @@ -5,11 +6,12 @@ use std::marker::PhantomData; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::str::FromStr; -use tempfile::{tempdir, TempDir}; +use tempfile::{TempDir, tempdir}; use types::*; use validator_manager::{ create_validators::CreateConfig, delete_validators::DeleteConfig, + exit_validators::ExitConfig, import_validators::ImportConfig, list_validators::ListConfig, move_validators::{MoveConfig, PasswordSource, Validators}, @@ -119,6 +121,12 @@ impl CommandLineTest { } } +impl CommandLineTest { + fn validators_exit() -> Self { + Self::default().flag("exit", None) + } +} + #[test] pub fn validator_create_without_output_path() { CommandLineTest::validators_create().assert_failed(); @@ -360,7 +368,7 @@ pub fn validator_move_misc_flags_1() { dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), dest_vc_token_path: PathBuf::from("./2.json"), validators: Validators::Specific(vec![ - PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap() + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), ]), builder_proposals: Some(false), builder_boost_factor: None, @@ -392,7 +400,7 @@ pub fn validator_move_misc_flags_2() { dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), dest_vc_token_path: PathBuf::from("./2.json"), validators: Validators::Specific(vec![ - PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap() + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), ]), builder_proposals: Some(false), builder_boost_factor: Some(100), @@ -443,6 +451,8 @@ pub fn validator_list_defaults() { let expected = ListConfig { vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), vc_token_path: PathBuf::from("./token.json"), + beacon_url: None, + validators_to_display: vec![], }; assert_eq!(expected, config); }); @@ -468,3 +478,106 @@ pub fn validator_delete_defaults() { assert_eq!(expected, config); }); } + +#[test] +pub fn validator_delete_missing_validator_flag() { + CommandLineTest::validators_delete() + .flag("--vc-token", Some("./token.json")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_defaults() { + CommandLineTest::validators_exit() + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--vc-token", Some("./token.json")) + .flag("--beacon-node", Some("http://localhost:5052")) + .assert_success(|config| { + let expected = ExitConfig { + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + validators_to_exit: vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), + ], + beacon_url: Some(SensitiveUrl::parse("http://localhost:5052").unwrap()), + exit_epoch: None, + presign: false, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_exit_exit_epoch_and_presign_flags() { + CommandLineTest::validators_exit() + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--vc-token", Some("./token.json")) + .flag("--exit-epoch", Some("1234567")) + .flag("--presign", None) + .assert_success(|config| { + let expected = ExitConfig { + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + validators_to_exit: vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), + ], + beacon_url: None, + exit_epoch: Some(Epoch::new(1234567)), + presign: true, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_exit_missing_validator_flag() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_using_beacon_and_presign_flags() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--presign", None) + .assert_failed(); +} + +#[test] +pub fn validator_exit_using_beacon_and_exit_epoch_flags() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--exit-epoch", Some("1234567")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_exit_epoch_flag_without_presign_flag() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--exit-epoch", Some("1234567")) + .assert_failed(); +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000000..f216078d96 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2024" diff --git a/scripts/change_version.sh b/scripts/change_version.sh deleted file mode 100755 index bda87fd863..0000000000 --- a/scripts/change_version.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# Change the version across multiple files, prior to a release. Use `sed` to -# find/replace the exiting version with the new one. -# -# Takes two arguments: -# -# 1. Current version (e.g., `0.2.6`) -# 2. New version (e.g., `0.2.7`) -# -# ## Example: -# -# `./change_version.sh 0.2.6 0.2.7` - -FROM=$1 -TO=$2 -VERSION_CRATE="../common/lighthouse_version/src/lib.rs" - -update_cargo_toml () { - echo $1 - sed -i -e "s/version = \"$FROM\"/version = \"$TO\"/g" $1 -} - -echo "Changing version from $FROM to $TO" - -update_cargo_toml ../account_manager/Cargo.toml -update_cargo_toml ../beacon_node/Cargo.toml -update_cargo_toml ../boot_node/Cargo.toml -update_cargo_toml ../lcli/Cargo.toml -update_cargo_toml ../lighthouse/Cargo.toml -update_cargo_toml ../validator_client/Cargo.toml - -echo $VERSION_CRATE -sed -i -e "s/$FROM/$TO/g" $VERSION_CRATE diff --git a/scripts/ci/check-success-job.sh b/scripts/ci/check-success-job.sh index dfa5c03257..2eee35f69e 100755 --- a/scripts/ci/check-success-job.sh +++ b/scripts/ci/check-success-job.sh @@ -5,8 +5,13 @@ set -euf -o pipefail YAML=$1 SUCCESS_JOB=$2 +EXCLUDE_JOBS_REGEX=${3:-} + +yq '... comments="" | .jobs | map(. | key) | .[]' < "$YAML" | + grep -v "$SUCCESS_JOB" | + { [ -n "$EXCLUDE_JOBS_REGEX" ] && grep -Ev "$EXCLUDE_JOBS_REGEX" || cat; } | + sort > all_jobs.txt -yq '... comments="" | .jobs | map(. | key) | .[]' < "$YAML" | grep -v "$SUCCESS_JOB" | sort > all_jobs.txt yq "... comments=\"\" | .jobs.$SUCCESS_JOB.needs[]" < "$YAML" | grep -v "$SUCCESS_JOB" | sort > dep_jobs.txt diff all_jobs.txt dep_jobs.txt || (echo "COMPLETENESS CHECK FAILED" && exit 1) rm all_jobs.txt dep_jobs.txt diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index 9d9844c4c4..6260f91019 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -21,7 +21,7 @@ cd ./scripts/local_testnet ``` It will build a Lighthouse docker image from the root of the directory and will take an approximately 12 minutes to complete. Once built, the testing will be started automatically. You will see a list of services running and "Started!" at the end. -You can also select your own Lighthouse docker image to use by specifying it in `network_params.yml` under the `cl_image` key. +You can also select your own Lighthouse docker image to use by specifying it in `network_params.yaml` under the `cl_image` key. Full configuration reference for Kurtosis is specified [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). To view all running services: diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index e671340afb..a048674e63 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -1,18 +1,37 @@ # Full configuration reference [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). participants: - - el_type: geth - el_image: ethereum/client-go:latest - cl_type: lighthouse + - cl_type: lighthouse cl_image: lighthouse:local + el_type: geth + el_image: ethereum/client-go:latest + supernode: true cl_extra_params: - --target-peers=3 - count: 4 + count: 2 + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethereum/client-go:latest + supernode: false + cl_extra_params: + - --target-peers=3 + count: 2 network_params: - deneb_fork_epoch: 0 - seconds_per_slot: 3 -global_log_level: debug + fulu_fork_epoch: 0 + seconds_per_slot: 6 snooper_enabled: false +global_log_level: debug additional_services: - dora - spamoor - prometheus_grafana + - tempo +spamoor_params: + image: ethpandaops/spamoor:master + spammers: + - scenario: eoatx + config: + throughput: 200 + - scenario: blobs + config: + throughput: 20 \ No newline at end of file diff --git a/scripts/local_testnet/network_params_das.yaml b/scripts/local_testnet/network_params_das.yaml deleted file mode 100644 index 628b2696a5..0000000000 --- a/scripts/local_testnet/network_params_das.yaml +++ /dev/null @@ -1,37 +0,0 @@ -participants: - - cl_type: lighthouse - cl_image: lighthouse:local - el_image: ethpandaops/geth:marius-engine-getblobs-v2 - cl_extra_params: - - --subscribe-all-data-column-subnets - - --subscribe-all-subnets - # Note: useful for testing range sync (only produce block if the node is in sync to prevent forking) - - --sync-tolerance-epochs=0 - - --target-peers=3 - count: 2 - - cl_type: lighthouse - cl_image: lighthouse:local - el_image: ethpandaops/geth:marius-engine-getblobs-v2 - cl_extra_params: - # Note: useful for testing range sync (only produce block if the node is in sync to prevent forking) - - --sync-tolerance-epochs=0 - - --target-peers=3 - count: 2 -network_params: - electra_fork_epoch: 0 - fulu_fork_epoch: 1 - seconds_per_slot: 6 -snooper_enabled: false -global_log_level: debug -additional_services: - - dora - - spamoor - - prometheus_grafana -spamoor_params: - spammers: - - scenario: eoatx - config: - throughput: 200 - - scenario: blobs - config: - throughput: 20 \ No newline at end of file diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 1f15688693..8d8b33526d 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -13,10 +13,12 @@ BUILD_IMAGE=true BUILDER_PROPOSALS=false CI=false KEEP_ENCLAVE=false +RUN_ASSERTOOR_TESTS=false # Get options -while getopts "e:b:n:phck" flag; do +while getopts "e:b:n:phcak" flag; do case "${flag}" in + a) RUN_ASSERTOOR_TESTS=true;; e) ENCLAVE_NAME=${OPTARG};; b) BUILD_IMAGE=${OPTARG};; n) NETWORK_PARAMS_FILE=${OPTARG};; @@ -34,6 +36,7 @@ while getopts "e:b:n:phck" flag; do echo " -n: kurtosis network params file path default: $NETWORK_PARAMS_FILE" echo " -p: enable builder proposals" echo " -c: CI mode, run without other additional services like Grafana and Dora explorer" + echo " -a: run Assertoor tests" echo " -k: keeping enclave to allow starting the testnet without destroying the existing one" echo " -h: this help" exit @@ -63,17 +66,16 @@ if [ "$BUILDER_PROPOSALS" = true ]; then fi if [ "$CI" = true ]; then - # TODO: run assertoor tests yq eval '.additional_services = []' -i $NETWORK_PARAMS_FILE echo "Running without additional services (CI mode)." fi -if [ "$BUILD_IMAGE" = true ]; then - echo "Building Lighthouse Docker image." - ROOT_DIR="$SCRIPT_DIR/../.." - docker build --build-arg FEATURES=portable -f $ROOT_DIR/Dockerfile -t $LH_IMAGE_NAME $ROOT_DIR -else - echo "Not rebuilding Lighthouse Docker image." +if [ "$RUN_ASSERTOOR_TESTS" = true ]; then + yq eval '.additional_services += ["assertoor"] | .additional_services |= unique' -i $NETWORK_PARAMS_FILE + # The available tests can be found in the `assertoor_params` section: + # https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration + yq eval '.assertoor_params = {"run_stability_check": true, "run_block_proposal_check": true, "run_transaction_test": true, "run_blob_transaction_test": true}' -i $NETWORK_PARAMS_FILE + echo "Assertoor has been added to $NETWORK_PARAMS_FILE." fi if [ "$KEEP_ENCLAVE" = false ]; then @@ -81,6 +83,14 @@ if [ "$KEEP_ENCLAVE" = false ]; then kurtosis enclave rm -f $ENCLAVE_NAME 2>/dev/null || true fi +if [ "$BUILD_IMAGE" = true ]; then + echo "Building Lighthouse Docker image." + ROOT_DIR="$SCRIPT_DIR/../.." + docker build --build-arg FEATURES=portable,spec-minimal -f $ROOT_DIR/Dockerfile -t $LH_IMAGE_NAME $ROOT_DIR +else + echo "Not rebuilding Lighthouse Docker image." +fi + kurtosis run --enclave $ENCLAVE_NAME github.com/ethpandaops/ethereum-package@$ETHEREUM_PKG_VERSION --args-file $NETWORK_PARAMS_FILE echo "Started!" diff --git a/scripts/local_testnet/stop_local_testnet.sh b/scripts/local_testnet/stop_local_testnet.sh index 6af1989e9f..b90a891154 100755 --- a/scripts/local_testnet/stop_local_testnet.sh +++ b/scripts/local_testnet/stop_local_testnet.sh @@ -6,10 +6,21 @@ ENCLAVE_NAME=${1:-local-testnet} LOGS_PATH=$SCRIPT_DIR/logs LOGS_SUBDIR=$LOGS_PATH/$ENCLAVE_NAME +# Extract the service names of Lighthouse beacon nodes that start with "cl-". +services=$(kurtosis enclave inspect "$ENCLAVE_NAME" | awk '/^=+ User Services =+$/ { in_section=1; next } + /^=+/ { in_section=0 } + in_section && /^[0-9a-f]{12}/ { print $2 }' | grep '^cl-') + +# Store logs (including dependency logs) to Kurtosis Files Artifacts. These are downloaded locally by `kurtosis enclave dump`. +for service in $services; do + kurtosis files storeservice --name "$service-logs" "$ENCLAVE_NAME" "$service" /data/lighthouse/beacon-data/beacon/logs/ +done + # Delete existing logs directory and make sure parent directory exists. rm -rf $LOGS_SUBDIR && mkdir -p $LOGS_PATH kurtosis enclave dump $ENCLAVE_NAME $LOGS_SUBDIR echo "Local testnet logs stored to $LOGS_SUBDIR." +echo "The lighthouse beacon nodes' logs (including dependency logs) can be found in $LOGS_SUBDIR/files/cl-*-lighthouse-geth-logs." kurtosis enclave rm -f $ENCLAVE_NAME kurtosis engine stop diff --git a/scripts/print_release_diffs.py b/scripts/print_release_diffs.py new file mode 100644 index 0000000000..d910b1be5b --- /dev/null +++ b/scripts/print_release_diffs.py @@ -0,0 +1,72 @@ +""" +Summarise pull requests between two Lighthouse releases. + +Usage: + export GITHUB_TOKEN=your_token + python -m pip install requests==2.32.4 + python print_release_diffs.py --base v7.0.1 --head release-v7.1.0 + +Shows commit SHA, PR number, 'backwards-incompat' label status, and PR title. +""" + +import requests +import re +import argparse +import os + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +if not GITHUB_TOKEN: + raise SystemExit("Error: Please set the GITHUB_TOKEN environment variable.") + +parser = argparse.ArgumentParser(description="Summarise PRs between two Lighthouse versions.") +parser.add_argument("--base", required=True, help="Base tag or branch (older release)") +parser.add_argument("--head", required=True, help="Head tag or branch (newer release)") +args = parser.parse_args() + +BASE = args.base +HEAD = args.head +OWNER = 'sigp' +REPO = 'lighthouse' + +HEADERS = { + 'Authorization': f'token {GITHUB_TOKEN}', + 'Accept': 'application/vnd.github+json' +} + +def get_commits_between(base, head): + url = f'https://api.github.com/repos/{OWNER}/{REPO}/compare/{base}...{head}' + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + return response.json()['commits'] + +def has_backwards_incompat_label(pr_number): + url = f'https://api.github.com/repos/{OWNER}/{REPO}/issues/{pr_number}' + response = requests.get(url, headers=HEADERS) + if response.status_code != 200: + raise Exception(f"Failed to fetch PR #{pr_number}") + labels = response.json().get('labels', []) + return any(label['name'] == 'backwards-incompat' for label in labels) + +def main(): + commits = get_commits_between(BASE, HEAD) + print(" # Commit SHA PR Number Has backwards-incompat Label PR Title") + print("--- ------------ ----------- ------------------------------ --------------------------------------------") + + for i, commit in enumerate(commits, 1): + sha = commit['sha'][:12] + message = commit['commit']['message'] + pr_match = re.search(r"\(#(\d+)\)", message) + + if not pr_match: + print(f"{i:<3} {sha} {'-':<11} {'-':<30} [NO PR MATCH]: {message.splitlines()[0]}") + continue + + pr_number = int(pr_match.group(1)) + try: + has_label = has_backwards_incompat_label(pr_number) + print(f"{i:<3} {sha} {pr_number:<11} {str(has_label):<30} {message.splitlines()[0]}") + except Exception as e: + print(f"{i:<3} {sha} {pr_number:<11} {'ERROR':<30} [ERROR FETCHING PR]: {e}") + +if __name__ == '__main__': + main() diff --git a/scripts/tests/checkpoint-sync-config-sepolia.yaml b/scripts/tests/checkpoint-sync-config-sepolia.yaml new file mode 100644 index 0000000000..289dee7869 --- /dev/null +++ b/scripts/tests/checkpoint-sync-config-sepolia.yaml @@ -0,0 +1,20 @@ +# Kurtosis config file to checkpoint sync to a live network (Sepolia). +participants: + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethereum/client-go:latest + supernode: true + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethereum/client-go:latest + supernode: false + +checkpoint_sync_enabled: true +checkpoint_sync_url: "https://checkpoint-sync.sepolia.ethpandaops.io" + +global_log_level: debug + +network_params: + network: sepolia diff --git a/scripts/tests/checkpoint-sync.sh b/scripts/tests/checkpoint-sync.sh new file mode 100755 index 0000000000..605dc504f5 --- /dev/null +++ b/scripts/tests/checkpoint-sync.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# Checkpoint sync to a live network. +# +# Start with checkpoint sync and let the node(s) sync to head and perform backfill for a specified number of slots. +# This test ensures we cover all sync components (range, lookup, backfill) and measures sync speed +# to detect any performance regressions. +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +ENCLAVE_NAME=${1:-sync-testnet} +CONFIG=${2:-$SCRIPT_DIR/checkpoint-sync-config-sepolia.yaml} + +# Test configuration +# ------------------------------------------------------ +# Interval for polling the /lighthouse/syncing endpoint for sync status +POLL_INTERVAL_SECS=5 +# Target number of slots to backfill to complete this test. +TARGET_BACKFILL_SLOTS=256 +# Timeout for this test, if the node(s) fail to backfill `TARGET_BACKFILL_SLOTS` slots, fail the test. +TIMEOUT_MINS=10 +TIMEOUT_SECS=$((TIMEOUT_MINS * 60)) +# ------------------------------------------------------ + +# Polls a single node's sync status +poll_node() { + local node_type=$1 + local url=${node_urls[$node_type]} + + response=$(curl -s "${url}/lighthouse/syncing") + + if [ -z "$response" ] || [ "$response" = "null" ]; then + echo "${node_type} status: No response or null response" + return + fi + + # Print syncing status + sync_state=$(echo "$response" | jq -r 'if (.data | type) == "object" then "object" else "string" end' 2>/dev/null) + + if [ "$sync_state" = "object" ]; then + status=$(echo "$response" | jq -r '.data | keys[0] // "Unknown"') + fields=$(echo "$response" | jq -r ".data.${status} | to_entries | map(\"\(.key): \(.value)\") | join(\", \")") + echo "${node_type} status: ${status}, ${fields}" + else + status=$(echo "$response" | jq -r '.data' 2>/dev/null) + echo "${node_type} status: ${status:-Unknown}" + fi + + # Check for completion criteria + if [ "$status" = "BackFillSyncing" ]; then + completed=$(echo "$response" | jq -r ".data.${status}.completed // 0") + if [ "$completed" -ge "$TARGET_BACKFILL_SLOTS" ]; then + mark_node_complete "$node_type" + fi + fi + # For other states (Synced, SyncingFinalized, SyncingHead, SyncTransition, Stalled, Unknown), + # we continue polling + # NOTE: there is a bug where Lighthouse briefly switch to "Synced" before completing backfilling. We ignore this state + # as it's unlikely a node is fully synced without going through backfilling `TARGET_BACKFILL_SLOTS` slots (only + # possible on a new network). +} + +# Marks a node as complete and record time +mark_node_complete() { + local node_type=$1 + if [ "${node_completed[$node_type]}" = false ]; then + node_completed[$node_type]=true + node_complete_time[$node_type]=$(date +%s) + echo "${node_type} completed backfill in $((node_complete_time[$node_type] - start_time)) seconds" + fi +} + +exit_and_dump_logs() { + local exit_code=$1 + echo "Shutting down..." + $SCRIPT_DIR/../local_testnet/stop_local_testnet.sh $ENCLAVE_NAME + echo "Test completed with exit code $exit_code." + exit $exit_code +} + +# Start the nodes +$SCRIPT_DIR/../local_testnet/start_local_testnet.sh -e $ENCLAVE_NAME -b false -n $CONFIG +if [ $? -ne 0 ]; then + echo "Failed to start local testnet" + exit_and_dump_logs 1 +fi + +start_time=$(date +%s) + +# Get all beacon API URLs +supernode_url=$(kurtosis port print $ENCLAVE_NAME cl-1-lighthouse-geth http) +fullnode_url=$(kurtosis port print $ENCLAVE_NAME cl-2-lighthouse-geth http) + +# Initialize statuses +declare -A node_completed +declare -A node_complete_time +declare -A node_urls + +node_urls["supernode"]="$supernode_url" +node_urls["fullnode"]="$fullnode_url" +node_completed["supernode"]=false +node_completed["fullnode"]=false + +echo "Polling sync status until backfill reaches ${TARGET_BACKFILL_SLOTS} slots or timeout of ${TIMEOUT_MINS} mins" + +# while [ "${node_completed[supernode]}" = false ] || [ "${node_completed[fullnode]}" = false ]; do +while [ "${node_completed[fullnode]}" = false ]; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + + if [ "$elapsed" -ge "$TIMEOUT_SECS" ]; then + echo "ERROR: Nodes timed out syncing after ${TIMEOUT_MINS} minutes. Exiting." + exit_and_dump_logs 1 + fi + + # Poll each node that hasn't completed yet + # for node in "supernode" "fullnode"; do + for node in "fullnode"; do + if [ "${node_completed[$node]}" = false ]; then + poll_node "$node" + fi + done + + sleep $POLL_INTERVAL_SECS +done + +echo "Sync test complete! Fullnode has synced to HEAD and backfilled ${TARGET_BACKFILL_SLOTS} slots." +# echo "Supernode time: $((node_complete_time[supernode] - start_time)) seconds" +echo "Fullnode time: $((node_complete_time[fullnode] - start_time)) seconds" +exit_and_dump_logs 0 \ No newline at end of file diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 86c9705ee4..9009d49d58 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -60,7 +60,7 @@ DELAY=$(( $SECONDS_PER_SLOT * 32 + $GENESIS_DELAY + $MIN_GENESIS_TIME - $CURRENT sleep $DELAY # Use BN2 for the next validator client -bn_2_url=$(kurtosis service inspect $ENCLAVE_NAME cl-2-lighthouse-geth | grep 'enr-address' | cut -d'=' -f2) +bn_2_url=$(kurtosis service inspect $ENCLAVE_NAME cl-2-lighthouse-geth | grep -oP '(?<=--enr-address=)[^ ]+') bn_2_port=4000 if [[ "$BEHAVIOR" == "failure" ]]; then diff --git a/scripts/tests/genesis-sync-config-electra.yaml b/scripts/tests/genesis-sync-config-electra.yaml new file mode 100644 index 0000000000..1d1ed4d315 --- /dev/null +++ b/scripts/tests/genesis-sync-config-electra.yaml @@ -0,0 +1,21 @@ +# Kurtosis config file for testing sync on a local devnet. +participants: + - cl_type: lighthouse + cl_image: lighthouse:local + count: 2 + # nodes without validators, used for testing sync. + - cl_type: lighthouse + cl_image: lighthouse:local + validator_count: 0 + - cl_type: lighthouse + cl_image: lighthouse:local + validator_count: 0 +network_params: + seconds_per_slot: 6 + electra_fork_epoch: 0 + fulu_fork_epoch: 100000 # a really big number so this test stays in electra + preset: "minimal" +additional_services: + - tx_fuzz + - spamoor +global_log_level: debug diff --git a/scripts/tests/genesis-sync-config-fulu.yaml b/scripts/tests/genesis-sync-config-fulu.yaml new file mode 100644 index 0000000000..6d2c2647a9 --- /dev/null +++ b/scripts/tests/genesis-sync-config-fulu.yaml @@ -0,0 +1,29 @@ +# Kurtosis config file for testing sync on a local devnet. +participants: + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethpandaops/geth:master + supernode: true + count: 2 + # nodes without validators, used for testing sync. + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethpandaops/geth:master + supernode: true + validator_count: 0 + - cl_type: lighthouse + cl_image: lighthouse:local + el_type: geth + el_image: ethpandaops/geth:master + supernode: false + validator_count: 0 +network_params: + seconds_per_slot: 6 + fulu_fork_epoch: 0 + preset: "minimal" +additional_services: + - tx_fuzz + - spamoor +global_log_level: debug diff --git a/scripts/tests/genesis-sync.sh b/scripts/tests/genesis-sync.sh new file mode 100755 index 0000000000..39628c9e73 --- /dev/null +++ b/scripts/tests/genesis-sync.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# +# Genesis sync test on a local network. +# +# Start a local testnet, shut down non-validator nodes for a period, then restart them +# and monitor their sync progress from genesis to head. +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +ENCLAVE_NAME=${1:-genesis-sync-testnet} +CONFIG=${2:-$SCRIPT_DIR/genesis-sync-config-electra.yaml} +FORK_TYPE=${3:-electra} # electra or fulu +OFFLINE_DURATION_SECS=${4:-120} # stopped duration of non validating nodes + +# Test configuration +# ------------------------------------------------------ +# Interval for polling the /lighthouse/syncing endpoint for sync status +# Reduce the polling time so that some progress can be seen +POLL_INTERVAL_SECS=0.5 +# Timeout for this test, if the nodes fail to sync, fail the test. +TIMEOUT_MINS=5 +TIMEOUT_SECS=$((TIMEOUT_MINS * 60)) +# ------------------------------------------------------ + +echo "Starting genesis sync test with:" +echo " Fork: $FORK_TYPE" +echo " Offline duration: ${OFFLINE_DURATION_SECS}s" + +# Polls a node's sync status +poll_node() { + local node_type=$1 + local url=${node_urls[$node_type]} + + response=$(curl -s "${url}/lighthouse/syncing" 2>/dev/null) + + if [ -z "$response" ] || [ "$response" = "null" ]; then + echo "${node_type} status: No response or null response" + return + fi + + # Print syncing status + sync_state=$(echo "$response" | jq -r 'if (.data | type) == "object" then "object" else "string" end' 2>/dev/null) + + if [ "$sync_state" = "object" ]; then + status=$(echo "$response" | jq -r '.data | keys[0] // "Unknown"') + fields=$(echo "$response" | jq -r ".data.${status} | to_entries | map(\"\(.key): \(.value)\") | join(\", \")") + echo "${node_type} status: ${status}, ${fields}" + else + status=$(echo "$response" | jq -r '.data' 2>/dev/null) + echo "${node_type} status: ${status:-Unknown}" + + # The test is complete when the node is synced + if [ "$status" = "Synced" ]; then + mark_node_complete "$node_type" + fi + fi +} + +# Marks a node as complete and record time +mark_node_complete() { + local node_type=$1 + if [ "${node_completed[$node_type]}" = false ]; then + node_completed[$node_type]=true + node_complete_time[$node_type]=$(date +%s) + echo "${node_type} completed sync in $((node_complete_time[$node_type] - sync_start_time)) seconds" + fi +} + +exit_and_dump_logs() { + local exit_code=$1 + echo "Shutting down..." + $SCRIPT_DIR/../local_testnet/stop_local_testnet.sh $ENCLAVE_NAME + echo "Test completed with exit code $exit_code." + exit $exit_code +} + +# Start the nodes +$SCRIPT_DIR/../local_testnet/start_local_testnet.sh -e $ENCLAVE_NAME -b false -n $CONFIG +if [ $? -ne 0 ]; then + echo "Failed to start local testnet" + exit_and_dump_logs 1 +fi + +# Wait for 10s before stopping non-validating nodes +sleep 10 + +# These are non validating nodes +supernode="cl-3-lighthouse-geth" +fullnode="cl-4-lighthouse-geth" + +# Stop the non-validator nodes +kurtosis service stop $ENCLAVE_NAME $supernode +kurtosis service stop $ENCLAVE_NAME $fullnode + +echo "Non-validator nodes stopped. Waiting ${OFFLINE_DURATION_SECS} seconds..." + +# Display the time every 10s when the nodes are stopped +remaining_time=$OFFLINE_DURATION_SECS +while [ $remaining_time -gt 0 ]; do + sleep 10 + remaining_time=$((remaining_time - 10)) + echo "Nodes are stopped for $((OFFLINE_DURATION_SECS - remaining_time))s, ${remaining_time}s remains..." +done + +echo "Resuming non-validator nodes..." + +# Resume the non validating nodes +kurtosis service start $ENCLAVE_NAME $supernode +kurtosis service start $ENCLAVE_NAME $fullnode + +# The time at which syncing starts after the node was stopped +sync_start_time=$(date +%s) + +# Get beacon API URLs for non validating nodes for query +supernode_url=$(kurtosis port print $ENCLAVE_NAME $supernode http) +fullnode_url=$(kurtosis port print $ENCLAVE_NAME $fullnode http) + +# Initialize statuses +declare -A node_completed +declare -A node_complete_time +declare -A node_urls + +node_urls["supernode"]="$supernode_url" +node_urls["fullnode"]="$fullnode_url" +node_completed["supernode"]=false +node_completed["fullnode"]=false + +echo "Polling sync status until nodes are synced or timeout of ${TIMEOUT_MINS} mins" + +while [ "${node_completed[supernode]}" = false ] || [ "${node_completed[fullnode]}" = false ]; do + current_time=$(date +%s) + elapsed=$((current_time - sync_start_time)) + + if [ "$elapsed" -ge "$TIMEOUT_SECS" ]; then + echo "ERROR: Nodes timed out syncing after ${TIMEOUT_MINS} minutes. Exiting." + exit_and_dump_logs 1 + fi + + # Poll each node that hasn't completed yet + for node in "supernode" "fullnode"; do + if [ "${node_completed[$node]}" = false ]; then + poll_node "$node" + fi + done + + sleep $POLL_INTERVAL_SECS +done + +echo "Genesis sync test complete! Both supernode and fullnode have synced successfully." +echo "Supernode time: $((node_complete_time[supernode] - sync_start_time)) seconds" +echo "Fullnode time: $((node_complete_time[fullnode] - sync_start_time)) seconds" +exit_and_dump_logs 0 \ No newline at end of file diff --git a/scripts/tests/network_params.yaml b/scripts/tests/network_params.yaml index 21114df0e8..35916ac1e4 100644 --- a/scripts/tests/network_params.yaml +++ b/scripts/tests/network_params.yaml @@ -6,9 +6,10 @@ participants: cl_image: lighthouse:local cl_extra_params: - --target-peers=3 + supernode: true count: 4 network_params: - deneb_fork_epoch: 0 + fulu_fork_epoch: 0 seconds_per_slot: 3 num_validator_keys_per_node: 20 global_log_level: debug diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index b2f6eca9c3..a068b2e885 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -3,6 +3,7 @@ name = "slasher" version = "0.1.0" authors = ["Michael Sproul "] edition = { workspace = true } +autotests = false [features] default = ["lmdb"] @@ -13,11 +14,13 @@ portable = ["types/portable"] [dependencies] bincode = { workspace = true } +bls = { workspace = true } byteorder = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } filesystem = { workspace = true } +fixed_bytes = { workspace = true } flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } @@ -37,9 +40,14 @@ strum = { workspace = true } tracing = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } +typenum = { workspace = true } types = { workspace = true } [dev-dependencies] maplit = { workspace = true } rayon = { workspace = true } tempfile = { workspace = true } + +[[test]] +name = "slasher_tests" +path = "tests/main.rs" diff --git a/slasher/service/src/service.rs b/slasher/service/src/service.rs index 2409a24c78..c0e6a8a0cd 100644 --- a/slasher/service/src/service.rs +++ b/slasher/service/src/service.rs @@ -1,26 +1,26 @@ use beacon_chain::{ - observed_operations::ObservationOutcome, BeaconChain, BeaconChainError, BeaconChainTypes, + BeaconChain, BeaconChainError, BeaconChainTypes, observed_operations::ObservationOutcome, }; use directory::size_of_dir; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use slasher::{ - metrics::{self, SLASHER_DATABASE_SIZE, SLASHER_RUN_TIME}, Slasher, + metrics::{self, SLASHER_DATABASE_SIZE, SLASHER_RUN_TIME}, }; use slot_clock::SlotClock; use state_processing::{ + VerifyOperation, per_block_processing::errors::{ AttesterSlashingInvalid, BlockOperationError, ProposerSlashingInvalid, }, - VerifyOperation, }; -use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError}; use std::sync::Arc; +use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel}; use task_executor::TaskExecutor; use tokio::sync::mpsc::UnboundedSender; -use tokio::time::{interval_at, Duration, Instant}; -use tracing::{debug, error, info, info_span, trace, warn, Instrument}; +use tokio::time::{Duration, Instant, interval_at}; +use tracing::{debug, error, info, trace, warn}; use types::{AttesterSlashing, Epoch, EthSpec, ProposerSlashing}; pub struct SlasherService { @@ -64,15 +64,12 @@ impl SlasherService { update_period, slot_offset, notif_sender, - ) - .instrument(tracing::info_span!("slasher", service = "slasher")), + ), "slasher_server_notifier", ); executor.spawn_blocking( || { - let span = info_span!("slasher", service = "slasher"); - let _ = span.enter(); Self::run_processor(beacon_chain, slasher, notif_receiver, network_sender); }, "slasher_server_processor", @@ -214,15 +211,14 @@ impl SlasherService { beacon_chain.import_attester_slashing(verified_slashing); // Publish to the network if broadcast is enabled. - if slasher.config().broadcast { - if let Err(e) = + if slasher.config().broadcast + && let Err(e) = Self::publish_attester_slashing(beacon_chain, network_sender, slashing) - { - debug!( - error = ?e, - "Unable to publish attester slashing" - ); - } + { + debug!( + error = ?e, + "Unable to publish attester slashing" + ); } } } @@ -263,15 +259,14 @@ impl SlasherService { }; beacon_chain.import_proposer_slashing(verified_slashing); - if slasher.config().broadcast { - if let Err(e) = + if slasher.config().broadcast + && let Err(e) = Self::publish_proposer_slashing(beacon_chain, network_sender, slashing) - { - debug!( - error = ?e, - "Unable to publish proposer slashing" - ); - } + { + debug!( + error = ?e, + "Unable to publish proposer slashing" + ); } } } diff --git a/slasher/src/array.rs b/slasher/src/array.rs index 77ddceb85f..e375da4a71 100644 --- a/slasher/src/array.rs +++ b/slasher/src/array.rs @@ -6,7 +6,7 @@ use crate::{ use flate2::bufread::{ZlibDecoder, ZlibEncoder}; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; -use std::collections::{btree_map::Entry, BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashSet, btree_map::Entry}; use std::io::Read; use std::sync::Arc; use types::{AttesterSlashing, Epoch, EthSpec, IndexedAttestation}; @@ -147,7 +147,7 @@ pub trait TargetArrayChunk: Sized + serde::Serialize + serde::de::DeserializeOwn fn next_start_epoch(start_epoch: Epoch, config: &Config) -> Epoch; - fn select_db(db: &SlasherDB) -> &Database; + fn select_db(db: &SlasherDB) -> &Database<'_>; fn load( db: &SlasherDB, @@ -290,7 +290,7 @@ impl TargetArrayChunk for MinTargetChunk { start_epoch / chunk_size * chunk_size - 1 } - fn select_db(db: &SlasherDB) -> &Database { + fn select_db(db: &SlasherDB) -> &Database<'_> { &db.databases.min_targets_db } } @@ -389,7 +389,7 @@ impl TargetArrayChunk for MaxTargetChunk { (start_epoch / chunk_size + 1) * chunk_size } - fn select_db(db: &SlasherDB) -> &Database { + fn select_db(db: &SlasherDB) -> &Database<'_> { &db.databases.max_targets_db } } diff --git a/slasher/src/attester_record.rs b/slasher/src/attester_record.rs index 1cd4ba7d4e..db326a9d80 100644 --- a/slasher/src/attester_record.rs +++ b/slasher/src/attester_record.rs @@ -1,13 +1,15 @@ -use crate::{database::IndexedAttestationId, Error}; +use crate::{Error, database::IndexedAttestationId}; +use bls::AggregateSignature; use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; use std::borrow::Cow; use std::sync::{ - atomic::{AtomicU64, Ordering}, Arc, + atomic::{AtomicU64, Ordering}, }; use tree_hash::TreeHash as _; use tree_hash_derive::TreeHash; -use types::{AggregateSignature, EthSpec, Hash256, IndexedAttestation, VariableList}; +use types::{EthSpec, Hash256, IndexedAttestation}; #[derive(Debug, Clone, Copy)] pub struct AttesterRecord { diff --git a/slasher/src/config.rs b/slasher/src/config.rs index 33d68fa0e5..144016efd2 100644 --- a/slasher/src/config.rs +++ b/slasher/src/config.rs @@ -2,7 +2,7 @@ use crate::Error; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; use std::path::PathBuf; -use strum::{Display, EnumString, EnumVariantNames}; +use strum::{Display, EnumString, VariantNames}; use types::non_zero_usize::new_non_zero_usize; use types::{Epoch, EthSpec, IndexedAttestation}; @@ -59,7 +59,7 @@ pub struct DiskConfig { } #[derive( - Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Display, EnumString, EnumVariantNames, + Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Display, EnumString, VariantNames, )] #[strum(serialize_all = "lowercase")] pub enum DatabaseBackend { @@ -104,7 +104,7 @@ impl Config { Err(Error::ConfigInvalidZeroParameter { config: self.clone(), }) - } else if self.history_length % self.chunk_size != 0 { + } else if !self.history_length.is_multiple_of(self.chunk_size) { Err(Error::ConfigInvalidChunkSize { chunk_size: self.chunk_size, history_length: self.history_length, diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 071109e00c..80d073a81c 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -4,9 +4,10 @@ mod mdbx_impl; mod redb_impl; use crate::{ - metrics, AttesterRecord, AttesterSlashingStatus, CompactAttesterRecord, Config, Database, - Error, ProposerSlashingStatus, + AttesterRecord, AttesterSlashingStatus, CompactAttesterRecord, Config, Database, Error, + ProposerSlashingStatus, metrics, }; +use bls::AggregateSignature; use byteorder::{BigEndian, ByteOrder}; use interface::{Environment, OpenDatabases, RwTransaction}; use lru::LruCache; @@ -14,15 +15,16 @@ use parking_lot::Mutex; use serde::de::DeserializeOwned; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; use std::borrow::{Borrow, Cow}; use std::marker::PhantomData; use std::sync::Arc; use tracing::info; use tree_hash::TreeHash; use types::{ - AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, + AttestationData, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, ProposerSlashing, SignedBeaconBlockHeader, - Slot, VariableList, + Slot, }; /// Current database schema version, to check compatibility of on-disk DB with software. @@ -331,7 +333,7 @@ impl SlasherDB { Ok(db) } - pub fn begin_rw_txn(&self) -> Result { + pub fn begin_rw_txn(&self) -> Result, Error> { self.env.begin_rw_txn() } @@ -860,7 +862,8 @@ impl SlasherDB { #[cfg(test)] mod test { use super::*; - use types::{Checkpoint, ForkName, MainnetEthSpec, Unsigned}; + use typenum::Unsigned; + use types::{Checkpoint, ForkName, MainnetEthSpec}; type E = MainnetEthSpec; diff --git a/slasher/src/database/interface.rs b/slasher/src/database/interface.rs index af72006caa..dcbb82fe93 100644 --- a/slasher/src/database/interface.rs +++ b/slasher/src/database/interface.rs @@ -83,7 +83,7 @@ impl Environment { } } - pub fn create_databases(&self) -> Result { + pub fn create_databases(&self) -> Result, Error> { match self { #[cfg(feature = "mdbx")] Self::Mdbx(env) => env.create_databases(), @@ -95,7 +95,7 @@ impl Environment { } } - pub fn begin_rw_txn(&self) -> Result { + pub fn begin_rw_txn(&self) -> Result, Error> { match self { #[cfg(feature = "mdbx")] Self::Mdbx(env) => env.begin_rw_txn().map(RwTransaction::Mdbx), @@ -194,7 +194,7 @@ impl<'env> RwTransaction<'env> { impl Cursor<'_> { /// Return the first key in the current database while advancing the cursor's position. - pub fn first_key(&mut self) -> Result, Error> { + pub fn first_key(&mut self) -> Result>, Error> { match self { #[cfg(feature = "mdbx")] Cursor::Mdbx(cursor) => cursor.first_key(), @@ -207,7 +207,7 @@ impl Cursor<'_> { } /// Return the last key in the current database while advancing the cursor's position. - pub fn last_key(&mut self) -> Result, Error> { + pub fn last_key(&mut self) -> Result>, Error> { match self { #[cfg(feature = "mdbx")] Cursor::Mdbx(cursor) => cursor.last_key(), @@ -219,7 +219,7 @@ impl Cursor<'_> { } } - pub fn next_key(&mut self) -> Result, Error> { + pub fn next_key(&mut self) -> Result>, Error> { match self { #[cfg(feature = "mdbx")] Cursor::Mdbx(cursor) => cursor.next_key(), diff --git a/slasher/src/database/lmdb_impl.rs b/slasher/src/database/lmdb_impl.rs index 74342968cf..a2ef298830 100644 --- a/slasher/src/database/lmdb_impl.rs +++ b/slasher/src/database/lmdb_impl.rs @@ -41,7 +41,7 @@ impl Environment { Ok(Environment { env }) } - pub fn create_databases(&self) -> Result { + pub fn create_databases(&self) -> Result, Error> { let indexed_attestation_db = self .env .create_db(Some(INDEXED_ATTESTATION_DB), Self::db_flags())?; @@ -80,7 +80,7 @@ impl Environment { }) } - pub fn begin_rw_txn(&self) -> Result { + pub fn begin_rw_txn(&self) -> Result, Error> { let txn = self.env.begin_rw_txn()?; Ok(RwTransaction { txn }) } @@ -137,7 +137,7 @@ impl<'env> RwTransaction<'env> { } impl<'env> Cursor<'env> { - pub fn first_key(&mut self) -> Result, Error> { + pub fn first_key(&mut self) -> Result>, Error> { let opt_key = self .cursor .get(None, None, MDB_FIRST) diff --git a/slasher/src/database/mdbx_impl.rs b/slasher/src/database/mdbx_impl.rs index e249de963f..ede7249f04 100644 --- a/slasher/src/database/mdbx_impl.rs +++ b/slasher/src/database/mdbx_impl.rs @@ -1,12 +1,12 @@ #![cfg(feature = "mdbx")] use crate::{ + Config, Error, config::MEGABYTE, database::{ interface::{Key, OpenDatabases, Value}, *, }, - Config, Error, }; use mdbx::{DatabaseFlags, Geometry, WriteFlags}; use std::borrow::Cow; @@ -44,7 +44,7 @@ impl Environment { Ok(Environment { env }) } - pub fn create_databases(&self) -> Result { + pub fn create_databases(&self) -> Result, Error> { let txn = self.begin_rw_txn()?; txn.create_db(INDEXED_ATTESTATION_DB)?; txn.create_db(INDEXED_ATTESTATION_ID_DB)?; @@ -77,7 +77,7 @@ impl Environment { }) } - pub fn begin_rw_txn(&self) -> Result { + pub fn begin_rw_txn(&self) -> Result, Error> { let txn = self.env.begin_rw_txn()?; Ok(RwTransaction { txn }) } @@ -106,7 +106,7 @@ impl<'env> RwTransaction<'env> { Ok(()) } - pub fn open_db(&self, name: &'static str) -> Result { + pub fn open_db(&self, name: &'static str) -> Result, Error> { let db = self.txn.open_db(Some(name))?; Ok(Database { db }) } diff --git a/slasher/src/database/redb_impl.rs b/slasher/src/database/redb_impl.rs index 12bef71148..570d7df131 100644 --- a/slasher/src/database/redb_impl.rs +++ b/slasher/src/database/redb_impl.rs @@ -1,13 +1,13 @@ #![cfg(feature = "redb")] use crate::{ + Config, Error, config::REDB_DATA_FILENAME, database::{ interface::{Key, OpenDatabases, Value}, *, }, - Config, Error, }; -use derivative::Derivative; +use educe::Educe; use redb::{ReadableTable, TableDefinition}; use std::{borrow::Cow, path::PathBuf}; @@ -23,18 +23,18 @@ pub struct Database<'env> { _phantom: PhantomData<&'env ()>, } -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Educe)] +#[educe(Debug)] pub struct RwTransaction<'env> { - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] txn: redb::WriteTransaction, _phantom: PhantomData<&'env ()>, } -#[derive(Derivative)] -#[derivative(Debug)] +#[derive(Educe)] +#[educe(Debug)] pub struct Cursor<'env> { - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] txn: &'env redb::WriteTransaction, db: &'env Database<'env>, current_key: Option>, @@ -51,7 +51,7 @@ impl Environment { }) } - pub fn create_databases(&self) -> Result { + pub fn create_databases(&self) -> Result, Error> { let indexed_attestation_db = self.create_table(INDEXED_ATTESTATION_DB)?; let indexed_attestation_id_db = self.create_table(INDEXED_ATTESTATION_ID_DB)?; let attesters_db = self.create_table(ATTESTERS_DB)?; @@ -94,7 +94,7 @@ impl Environment { vec![config.database_path.join(REDB_DATA_FILENAME)] } - pub fn begin_rw_txn(&self) -> Result { + pub fn begin_rw_txn(&self) -> Result, Error> { let mut txn = self.db.begin_write()?; txn.set_durability(redb::Durability::Eventual); Ok(RwTransaction { @@ -160,7 +160,7 @@ impl<'env> RwTransaction<'env> { } impl<'env> Cursor<'env> { - pub fn first_key(&mut self) -> Result, Error> { + pub fn first_key(&mut self) -> Result>, Error> { let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(&self.db.table_name); let table = self.txn.open_table(table_definition)?; diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index d3a26337d6..b41aa23f7f 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -23,8 +23,8 @@ pub use attester_record::{AttesterRecord, CompactAttesterRecord, IndexedAttester pub use block_queue::BlockQueue; pub use config::{Config, DatabaseBackend, DatabaseBackendOverride}; pub use database::{ - interface::{Database, Environment, RwTransaction}, IndexedAttestationId, SlasherDB, + interface::{Database, Environment, RwTransaction}, }; pub use error::Error; diff --git a/slasher/src/migrate.rs b/slasher/src/migrate.rs index 674ab9c132..ec552a19d0 100644 --- a/slasher/src/migrate.rs +++ b/slasher/src/migrate.rs @@ -1,4 +1,4 @@ -use crate::{database::CURRENT_SCHEMA_VERSION, Error, SlasherDB}; +use crate::{Error, SlasherDB, database::CURRENT_SCHEMA_VERSION}; use types::EthSpec; impl SlasherDB { diff --git a/slasher/src/slasher.rs b/slasher/src/slasher.rs index 12f35e657e..5d26c5a6da 100644 --- a/slasher/src/slasher.rs +++ b/slasher/src/slasher.rs @@ -5,8 +5,8 @@ use crate::metrics::{ SLASHER_NUM_BLOCKS_PROCESSED, }; use crate::{ - array, AttestationBatch, AttestationQueue, AttesterRecord, BlockQueue, Config, Error, - IndexedAttestationId, ProposerSlashingStatus, RwTransaction, SimpleBatch, SlasherDB, + AttestationBatch, AttestationQueue, AttesterRecord, BlockQueue, Config, Error, + IndexedAttestationId, ProposerSlashingStatus, RwTransaction, SimpleBatch, SlasherDB, array, }; use parking_lot::Mutex; use std::collections::HashSet; diff --git a/slasher/src/test_utils.rs b/slasher/src/test_utils.rs index 8054c0ad59..20d1ee9217 100644 --- a/slasher/src/test_utils.rs +++ b/slasher/src/test_utils.rs @@ -1,11 +1,12 @@ +use bls::{AggregateSignature, Signature}; +use fixed_bytes::FixedBytesExtended; use std::collections::HashSet; use std::sync::Arc; use types::{ + AttestationData, AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, + BeaconBlockHeader, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, IndexedAttestation, + MainnetEthSpec, SignedBeaconBlockHeader, Slot, indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectra}, - AggregateSignature, AttestationData, AttesterSlashing, AttesterSlashingBase, - AttesterSlashingElectra, BeaconBlockHeader, ChainSpec, Checkpoint, Epoch, EthSpec, - FixedBytesExtended, Hash256, IndexedAttestation, MainnetEthSpec, Signature, - SignedBeaconBlockHeader, Slot, }; pub type E = MainnetEthSpec; @@ -17,7 +18,7 @@ pub fn indexed_att_electra( target_root: u64, ) -> IndexedAttestation { IndexedAttestation::Electra(IndexedAttestationElectra { - attesting_indices: attesting_indices.as_ref().to_vec().into(), + attesting_indices: attesting_indices.as_ref().to_vec().try_into().unwrap(), data: AttestationData { slot: Slot::new(0), index: 0, @@ -42,7 +43,7 @@ pub fn indexed_att( target_root: u64, ) -> IndexedAttestation { IndexedAttestation::Base(IndexedAttestationBase { - attesting_indices: attesting_indices.as_ref().to_vec().into(), + attesting_indices: attesting_indices.as_ref().to_vec().try_into().unwrap(), data: AttestationData { slot: Slot::new(0), index: 0, diff --git a/slasher/tests/attester_slashings.rs b/slasher/tests/attester_slashings.rs index 22c9cfc128..9a8e1e27a4 100644 --- a/slasher/tests/attester_slashings.rs +++ b/slasher/tests/attester_slashings.rs @@ -3,12 +3,12 @@ use maplit::hashset; use rayon::prelude::*; use slasher::{ + Config, Slasher, config::DEFAULT_CHUNK_SIZE, test_utils::{ - att_slashing, chain_spec, indexed_att, indexed_att_electra, - slashed_validators_from_slashings, E, + E, att_slashing, chain_spec, indexed_att, indexed_att_electra, + slashed_validators_from_slashings, }, - Config, Slasher, }; use std::collections::HashSet; use tempfile::tempdir; diff --git a/slasher/tests/backend.rs b/slasher/tests/backend.rs index fd1a6ae14f..ca32b7bd6b 100644 --- a/slasher/tests/backend.rs +++ b/slasher/tests/backend.rs @@ -1,6 +1,6 @@ #![cfg(feature = "lmdb")] -use slasher::{config::MDBX_DATA_FILENAME, Config, DatabaseBackend, DatabaseBackendOverride}; +use slasher::{Config, DatabaseBackend, DatabaseBackendOverride, config::MDBX_DATA_FILENAME}; use std::fs::File; use tempfile::tempdir; diff --git a/slasher/tests/main.rs b/slasher/tests/main.rs new file mode 100644 index 0000000000..fb78dcb917 --- /dev/null +++ b/slasher/tests/main.rs @@ -0,0 +1,5 @@ +mod attester_slashings; +mod backend; +mod proposer_slashings; +mod random; +mod wrap_around; diff --git a/slasher/tests/proposer_slashings.rs b/slasher/tests/proposer_slashings.rs index ef525c6f3f..4e363fbaa1 100644 --- a/slasher/tests/proposer_slashings.rs +++ b/slasher/tests/proposer_slashings.rs @@ -1,8 +1,8 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] use slasher::{ - test_utils::{block as test_block, chain_spec, E}, Config, Slasher, + test_utils::{E, block as test_block, chain_spec}, }; use tempfile::tempdir; use types::{Epoch, EthSpec}; @@ -56,10 +56,8 @@ fn block_pruning() { (config.history_length - 1) * slots_per_epoch as usize + 1 ); // Check epochs of all slashings are from within range. - assert!(proposer_slashings.iter().all(|slashing| slashing - .signed_header_1 - .message - .slot - .epoch(slots_per_epoch) - > current_epoch - config.history_length as u64)); + assert!(proposer_slashings.iter().all(|slashing| { + slashing.signed_header_1.message.slot.epoch(slots_per_epoch) + > current_epoch - config.history_length as u64 + })); } diff --git a/slasher/tests/random.rs b/slasher/tests/random.rs index 3270700d88..5d1b2c9a74 100644 --- a/slasher/tests/random.rs +++ b/slasher/tests/random.rs @@ -1,16 +1,16 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] -use rand::prelude::*; +use rand::{prelude::*, rng}; use slasher::{ - test_utils::{ - block, chain_spec, indexed_att, slashed_validators_from_attestations, - slashed_validators_from_slashings, E, - }, Config, Slasher, SlasherDB, + test_utils::{ + E, block, chain_spec, indexed_att, slashed_validators_from_attestations, + slashed_validators_from_slashings, + }, }; use std::cmp::max; use std::sync::Arc; -use tempfile::{tempdir, TempDir}; +use tempfile::{TempDir, tempdir}; use types::{Epoch, EthSpec}; #[derive(Debug)] @@ -49,11 +49,11 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas let mut rng = StdRng::seed_from_u64(seed); let mut config = Config::new(db.get_config().database_path.clone()); - config.validator_chunk_size = 1 << rng.gen_range(1..4); + config.validator_chunk_size = 1 << rng.random_range(1..4); - let chunk_size_exponent = rng.gen_range(1..4); + let chunk_size_exponent = rng.random_range(1..4); config.chunk_size = 1 << chunk_size_exponent; - config.history_length = 1 << rng.gen_range(chunk_size_exponent..chunk_size_exponent + 3); + config.history_length = 1 << rng.random_range(chunk_size_exponent..chunk_size_exponent + 3); let config = Arc::new(config); db.update_config(config.clone()); @@ -62,13 +62,13 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas let validators = (0..num_validators as u64).collect::>(); - let num_attestations = rng.gen_range(2..max_attestations + 1); + let num_attestations = rng.random_range(2..max_attestations + 1); let mut current_epoch = Epoch::new(0); let mut attestations = vec![]; for _ in 0..num_attestations { - let num_attesters = rng.gen_range(1..num_validators); + let num_attesters = rng.random_range(1..num_validators); let mut attesting_indices = validators .choose_multiple(&mut rng, num_attesters) .copied() @@ -77,20 +77,20 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas // If checking slashings, generate valid attestations in range. let (source, target) = if check_slashings { - let source = rng.gen_range( + let source = rng.random_range( current_epoch .as_u64() .saturating_sub(config.history_length as u64 - 1) ..current_epoch.as_u64() + 1, ); - let target = rng.gen_range(source..current_epoch.as_u64() + 1); + let target = rng.random_range(source..current_epoch.as_u64() + 1); (source, target) } else { - let source = rng.gen_range(0..max(3 * current_epoch.as_u64(), 1)); - let target = rng.gen_range(source..max(3 * current_epoch.as_u64(), source + 1)); + let source = rng.random_range(0..max(3 * current_epoch.as_u64(), 1)); + let target = rng.random_range(source..max(3 * current_epoch.as_u64(), source + 1)); (source, target) }; - let target_root = rng.gen_range(0..3); + let target_root = rng.random_range(0..3); let attestation = indexed_att(&attesting_indices, source, target, target_root); if check_slashings { @@ -101,25 +101,26 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas slasher.accept_attestation(attestation); // Maybe add a random block too - if test_config.add_blocks && rng.gen_bool(0.1) { - let slot = rng.gen_range(0..1 + 3 * current_epoch.as_u64() * E::slots_per_epoch() / 2); - let proposer = rng.gen_range(0..num_validators as u64); - let block_root = rng.gen_range(0..2); + if test_config.add_blocks && rng.random_bool(0.1) { + let slot = + rng.random_range(0..1 + 3 * current_epoch.as_u64() * E::slots_per_epoch() / 2); + let proposer = rng.random_range(0..num_validators as u64); + let block_root = rng.random_range(0..2); slasher.accept_block_header(block(slot, proposer, block_root)); } // Maybe process - if rng.gen_bool(0.1) { + if rng.random_bool(0.1) { slasher.process_queued(current_epoch).unwrap(); // Maybe prune - if rng.gen_bool(0.1) { + if rng.random_bool(0.1) { slasher.prune_database(current_epoch).unwrap(); } } // Maybe advance to the next epoch - if rng.gen_bool(0.5) { + if rng.random_bool(0.5) { if check_slashings { slasher.process_queued(current_epoch).unwrap(); } @@ -147,10 +148,10 @@ fn random_test(seed: u64, mut db: SlasherDB, test_config: TestConfig) -> Slas #[test] #[ignore] fn no_crash() { - let mut rng = thread_rng(); + let mut rng = rng(); let (_tempdir, mut db) = make_db(); loop { - db = random_test(rng.gen(), db, TestConfig::default()); + db = random_test(rng.random(), db, TestConfig::default()); } } @@ -158,11 +159,11 @@ fn no_crash() { #[test] #[ignore] fn no_crash_with_blocks() { - let mut rng = thread_rng(); + let mut rng = rng(); let (_tempdir, mut db) = make_db(); loop { db = random_test( - rng.gen(), + rng.random(), db, TestConfig { add_blocks: true, @@ -176,11 +177,11 @@ fn no_crash_with_blocks() { #[test] #[ignore] fn check_slashings() { - let mut rng = thread_rng(); + let mut rng = rng(); let (_tempdir, mut db) = make_db(); loop { db = random_test( - rng.gen(), + rng.random(), db, TestConfig { check_slashings: true, diff --git a/slasher/tests/wrap_around.rs b/slasher/tests/wrap_around.rs index e34d0f2233..5257bae099 100644 --- a/slasher/tests/wrap_around.rs +++ b/slasher/tests/wrap_around.rs @@ -1,8 +1,8 @@ #![cfg(any(feature = "mdbx", feature = "lmdb", feature = "redb"))] use slasher::{ - test_utils::{chain_spec, indexed_att}, Config, Slasher, + test_utils::{chain_spec, indexed_att}, }; use tempfile::tempdir; use types::Epoch; diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index d93f3a5578..cef201ee91 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } [features] # `ef_tests` feature must be enabled to actually run the tests ef_tests = [] +disable_rayon = [] fake_crypto = ["bls/fake_crypto"] portable = ["beacon_chain/portable"] @@ -15,8 +16,8 @@ alloy-primitives = { workspace = true } beacon_chain = { workspace = true } bls = { workspace = true } compare_fields = { workspace = true } -compare_fields_derive = { workspace = true } -derivative = { workspace = true } +context_deserialize = { workspace = true } +educe = { workspace = true } eth2_network_config = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } @@ -26,14 +27,17 @@ fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } logging = { workspace = true } +milhouse = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } serde_yaml = { workspace = true } snap = { workspace = true } +ssz_types = { workspace = true } state_processing = { workspace = true } swap_or_not_shuffle = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } +typenum = { workspace = true } types = { workspace = true } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index c3a56ec11a..0ead9d0047 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,44 +1,33 @@ -TESTS_TAG := v1.5.0-beta.4 -TESTS = general minimal mainnet -TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) - +# To download/extract nightly tests, run: +# CONSENSUS_SPECS_TEST_VERSION=nightly make +CONSENSUS_SPECS_TEST_VERSION ?= v1.6.0-beta.1 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) -BASE_URL := https://github.com/ethereum/$(REPO_NAME)/releases/download/$(TESTS_TAG) BLS_TEST_REPO_NAME := bls12-381-tests -BLS_TEST_TAG := v0.1.1 +BLS_TEST_VERSION := v0.1.1 BLS_TEST = bls_tests_yaml -BLS_TARBALL = $(patsubst %,%-$(BLS_TEST_TAG).tar.gz,$(BLS_TEST)) BLS_OUTPUT_DIR := $(OUTPUT_DIR)/$(BLS_TEST_REPO_NAME) -BLS_BASE_URL := https://github.com/ethereum/$(BLS_TEST_REPO_NAME)/releases/download/$(BLS_TEST_TAG) +BLS_BASE_URL := https://github.com/ethereum/$(BLS_TEST_REPO_NAME)/releases/download/$(BLS_TEST_VERSION) -CURL := $(if $(LIGHTHOUSE_GITHUB_TOKEN),curl -L --header "Authorization: $(LIGHTHOUSE_GITHUB_TOKEN)",curl -L) +.PHONY: all clean -all: - make $(OUTPUT_DIR) - make $(BLS_OUTPUT_DIR) +all: clean $(OUTPUT_DIR) $(BLS_OUTPUT_DIR) -$(OUTPUT_DIR): $(TARBALLS) - mkdir $(OUTPUT_DIR) - for test_tarball in $^; do \ - tar -xzf $$test_tarball -C $(OUTPUT_DIR);\ +clean: + rm -rf *.tar.gz $(OUTPUT_DIR) $(BLS_OUTPUT_DIR) + +$(OUTPUT_DIR): + mkdir -p $(OUTPUT_DIR) + ./download_test_vectors.sh $(CONSENSUS_SPECS_TEST_VERSION) + for test_tarball in *.tar.gz; do \ + tar -xzf $$test_tarball -C $(OUTPUT_DIR); \ + rm -f $$test_tarball; \ done $(BLS_OUTPUT_DIR): - mkdir $(BLS_OUTPUT_DIR) - $(CURL) $(BLS_BASE_URL)/$(BLS_TEST).tar.gz -o $(BLS_TARBALL) - tar -xzf $(BLS_TARBALL) -C $(BLS_OUTPUT_DIR) - -%-$(TESTS_TAG).tar.gz: - $(CURL) $(BASE_URL)/$*.tar.gz -o $@ - -clean-test-files: - rm -rf $(OUTPUT_DIR) $(BLS_OUTPUT_DIR) - -clean-archives: - rm -f $(TARBALLS) $(BLS_TARBALL) - -clean: clean-test-files clean-archives - -.PHONY: clean clean-archives clean-test-files + mkdir -p $(BLS_OUTPUT_DIR) + curl --progress-bar --location --remote-name --show-error --retry 3 --retry-all-errors --fail \ + $(BLS_BASE_URL)/$(BLS_TEST).tar.gz + tar -xzf *.tar.gz -C $(BLS_OUTPUT_DIR) + rm -f *.tar.gz diff --git a/testing/ef_tests/README.md b/testing/ef_tests/README.md index 5ffd453d99..b04cd25dc7 100644 --- a/testing/ef_tests/README.md +++ b/testing/ef_tests/README.md @@ -28,6 +28,16 @@ $ cargo test --features ef_tests The tests won't run without the `ef_tests` feature enabled (this is to ensure that a top-level `cargo test --all` won't fail on missing files). +The following is sometimes necessary to avoid stack overflow issues when running on MacOS: +``` +$ export RUST_MIN_STACK=8388608 +``` + +When debugging failing tests, it's often useful to disable parallization and output suppression: +``` +$ cargo test --features ef_tests,disable_rayon -- --nocapture +``` + ## Saving Space When you download the tests, the downloaded archives will be kept in addition to the extracted diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 3aeff8ce06..1f70881a88 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -45,13 +45,25 @@ excluded_paths = [ "bls12-381-tests/deserialization_G1", "bls12-381-tests/deserialization_G2", "bls12-381-tests/hash_to_G2", - "tests/.*/eip6110", - "tests/.*/whisk", - # TODO(das): Fulu tests are ignored for now - "tests/.*/fulu", - "tests/.*/fulu/ssz_static/MatrixEntry", - "tests/.*/eip7441", "tests/.*/eip7732", + "tests/.*/eip7805", + # Ignore MatrixEntry SSZ tests for now. + "tests/.*/fulu/ssz_static/MatrixEntry/.*", + # EIP-7916 is still in draft and hasn't been implemented yet https://eips.ethereum.org/EIPS/eip-7916 + "tests/general/phase0/ssz_generic/progressive_bitlist", + "tests/general/phase0/ssz_generic/basic_progressive_list", + "tests/general/phase0/ssz_generic/containers/.*/ProgressiveBitsStruct.*", + "tests/general/phase0/ssz_generic/containers/.*/ProgressiveTestStruct.*", + "tests/general/phase0/ssz_generic/progressive_containers/.*", + "tests/general/phase0/ssz_generic/compatible_unions/.*", + # Ignore full epoch tests for now (just test the sub-transitions). + "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", + "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", + # Ignore gloas tests for now + "tests/.*/gloas/.*", + # Ignore KZG tests that target internal kzg library functions + "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", + "tests/.*/compute_challenge/.*", ] diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh new file mode 100755 index 0000000000..21f74e817f --- /dev/null +++ b/testing/ef_tests/download_test_vectors.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +TESTS=("general" "minimal" "mainnet") + +version=${1} +if [[ "$version" == "nightly" ]]; then + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "Error GITHUB_TOKEN is not set" + exit 1 + fi + + for cmd in unzip jq; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error ${cmd} is not installed" + exit 1 + fi + done + + repo="ethereum/consensus-specs" + api="https://api.github.com" + auth_header="Authorization: token ${GITHUB_TOKEN}" + + run_id=$(curl -s -H "${auth_header}" \ + "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | + jq -r '.workflow_runs[0].id') + + if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then + echo "No successful nightly workflow run found" + exit 1 + fi + + echo "Downloading nightly test vectors for run: ${run_id}" + curl -s -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | + jq -c '.artifacts[] | {name, url: .archive_download_url}' | + while read -r artifact; do + name=$(echo "${artifact}" | jq -r .name) + url=$(echo "${artifact}" | jq -r .url) + + if [[ "$name" == "consensustestgen.log" ]]; then + continue + fi + + echo "Downloading artifact: ${name}" + curl --progress-bar --location --show-error --retry 3 --retry-all-errors --fail \ + -H "${auth_header}" -H "Accept: application/vnd.github+json" \ + --output "${name}.zip" "${url}" || { + echo "Failed to download ${name}" + exit 1 + } + + unzip -qo "${name}.zip" + rm -f "${name}.zip" + done +else + for test in "${TESTS[@]}"; do + if [[ ! -e "${test}.tar.gz" ]]; then + echo "Downloading: ${version}/${test}.tar.gz" + curl --progress-bar --location --remote-name --show-error --retry 3 --retry-all-errors --fail \ + "https://github.com/ethereum/consensus-specs/releases/download/${version}/${test}.tar.gz" \ + || { + echo "Curl failed. Aborting" + rm -f "${test}.tar.gz" + exit 1 + } + fi + done +fi diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index 31662e831a..b2e0276353 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -22,6 +22,7 @@ mod genesis_validity; mod get_custody_groups; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; +mod kzg_compute_cells; mod kzg_compute_cells_and_kzg_proofs; mod kzg_compute_kzg_proof; mod kzg_recover_cells_and_kzg_proofs; @@ -58,6 +59,7 @@ pub use genesis_validity::*; pub use get_custody_groups::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; +pub use kzg_compute_cells::*; pub use kzg_compute_cells_and_kzg_proofs::*; pub use kzg_compute_kzg_proof::*; pub use kzg_recover_cells_and_kzg_proofs::*; @@ -91,29 +93,29 @@ pub use transition::TransitionTest; /// to return `true` for the feature in order for the feature test vector to be tested. #[derive(Debug, PartialEq, Clone, Copy)] pub enum FeatureName { - // TODO(fulu): to be removed once we start using Fulu types for test vectors. - // Existing SSZ types for PeerDAS (Fulu) are the same as Electra, so the test vectors get - // loaded as Electra types (default serde behaviour for untagged enums). - Fulu, + // Placeholder for future feature-gated forks + // Add new feature-gated forks here before they are incorporated into a main fork + #[doc(hidden)] + __Placeholder, } impl FeatureName { pub fn list_all() -> Vec { - vec![FeatureName::Fulu] + vec![] } /// `ForkName` to use when running the feature tests. pub fn fork_name(&self) -> ForkName { match self { - FeatureName::Fulu => ForkName::Electra, + FeatureName::__Placeholder => unreachable!("Placeholder variant should never be used"), } } } impl Display for FeatureName { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { match self { - FeatureName::Fulu => f.write_str("fulu"), + FeatureName::__Placeholder => unreachable!("Placeholder variant should never be used"), } } } @@ -165,17 +167,13 @@ impl Cases { self.test_cases .into_par_iter() .enumerate() - .map(|(i, (ref path, ref tc))| { - CaseResult::new(i, path, tc, tc.result(i, fork_name)) - }) + .map(|(i, (path, tc))| CaseResult::new(i, path, tc, tc.result(i, fork_name))) .collect() } else { self.test_cases .iter() .enumerate() - .map(|(i, (ref path, ref tc))| { - CaseResult::new(i, path, tc, tc.result(i, fork_name)) - }) + .map(|(i, (path, tc))| CaseResult::new(i, path, tc, tc.result(i, fork_name))) .collect() } } diff --git a/testing/ef_tests/src/cases/bls_batch_verify.rs b/testing/ef_tests/src/cases/bls_batch_verify.rs index 703444c987..f1349b06e6 100644 --- a/testing/ef_tests/src/cases/bls_batch_verify.rs +++ b/testing/ef_tests/src/cases/bls_batch_verify.rs @@ -1,7 +1,7 @@ use super::*; use crate::case_result::compare_result; use crate::impl_bls_load_case; -use bls::{verify_signature_sets, BlsWrappedSignature, PublicKeyBytes, Signature, SignatureSet}; +use bls::{BlsWrappedSignature, PublicKeyBytes, Signature, SignatureSet, verify_signature_sets}; use serde::Deserialize; use std::borrow::Cow; use std::str::FromStr; diff --git a/testing/ef_tests/src/cases/common.rs b/testing/ef_tests/src/cases/common.rs index 9364a35ba4..d58f6dbb10 100644 --- a/testing/ef_tests/src/cases/common.rs +++ b/testing/ef_tests/src/cases/common.rs @@ -1,8 +1,8 @@ -use serde::Deserialize; +use context_deserialize::ContextDeserialize; +use serde::{Deserialize, Deserializer}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use std::fmt::Debug; -use types::ForkName; /// Macro to wrap U128 and U256 so they deserialize correctly. macro_rules! uint_wrapper { @@ -40,6 +40,15 @@ macro_rules! uint_wrapper { self.x.tree_hash_root() } } + + impl<'de, T> ContextDeserialize<'de, T> for $wrapper_name { + fn context_deserialize(deserializer: D, _context: T) -> Result + where + D: Deserializer<'de>, + { + <$wrapper_name>::deserialize(deserializer) + } + } }; } @@ -47,29 +56,9 @@ uint_wrapper!(DecimalU128, alloy_primitives::U128); uint_wrapper!(DecimalU256, alloy_primitives::U256); /// Trait for types that can be used in SSZ static tests. -pub trait SszStaticType: - serde::de::DeserializeOwned + Encode + Clone + PartialEq + Debug + Sync -{ -} +pub trait SszStaticType: Encode + Clone + PartialEq + Debug + Sync {} -impl SszStaticType for T where - T: serde::de::DeserializeOwned + Encode + Clone + PartialEq + Debug + Sync -{ -} - -/// Return the fork immediately prior to a fork. -pub fn previous_fork(fork_name: ForkName) -> ForkName { - match fork_name { - ForkName::Base => ForkName::Base, - ForkName::Altair => ForkName::Base, - ForkName::Bellatrix => ForkName::Altair, - ForkName::Capella => ForkName::Bellatrix, - ForkName::Deneb => ForkName::Capella, - ForkName::Electra => ForkName::Deneb, - ForkName::Eip7805 => ForkName::Electra, - ForkName::Fulu => ForkName::Eip7805, - } -} +impl SszStaticType for T where T: Encode + Clone + PartialEq + Debug + Sync {} #[macro_export] macro_rules! impl_bls_load_case { diff --git a/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs b/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs index 8a6330d399..16d3eaf7af 100644 --- a/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs +++ b/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs @@ -1,7 +1,7 @@ use super::*; use serde::Deserialize; use std::marker::PhantomData; -use types::data_column_custody_group::{compute_columns_for_custody_group, CustodyIndex}; +use types::data_column_custody_group::{CustodyIndex, compute_columns_for_custody_group}; #[derive(Debug, Clone, Deserialize)] #[serde(bound = "E: EthSpec", deny_unknown_fields)] @@ -27,7 +27,7 @@ impl Case for ComputeColumnsForCustodyGroups { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let spec = E::default_spec(); - let computed_columns = compute_columns_for_custody_group(self.custody_group, &spec) + let computed_columns = compute_columns_for_custody_group::(self.custody_group, &spec) .expect("should compute custody columns from group") .collect::>(); diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index e05225c171..f143643ec3 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -4,6 +4,7 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_state, yaml_decode_file}; use crate::type_name; use serde::Deserialize; +use state_processing::EpochProcessingError; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_epoch_processing::capella::process_historical_summaries_update; @@ -11,7 +12,7 @@ use state_processing::per_epoch_processing::effective_balance_updates::{ process_effective_balance_updates, process_effective_balance_updates_slow, }; use state_processing::per_epoch_processing::single_pass::{ - process_epoch_single_pass, SinglePassConfig, + SinglePassConfig, process_epoch_single_pass, process_proposer_lookahead, }; use state_processing::per_epoch_processing::{ altair, base, @@ -20,7 +21,6 @@ use state_processing::per_epoch_processing::{ process_slashings_slow, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, }; -use state_processing::EpochProcessingError; use std::marker::PhantomData; use types::BeaconState; @@ -77,6 +77,8 @@ pub struct SyncCommitteeUpdates; pub struct InactivityUpdates; #[derive(Debug)] pub struct ParticipationFlagUpdates; +#[derive(Debug)] +pub struct ProposerLookahead; type_name!( JustificationAndFinalization, @@ -97,6 +99,7 @@ type_name!(ParticipationRecordUpdates, "participation_record_updates"); type_name!(SyncCommitteeUpdates, "sync_committee_updates"); type_name!(InactivityUpdates, "inactivity_updates"); type_name!(ParticipationFlagUpdates, "participation_flag_updates"); +type_name!(ProposerLookahead, "proposer_lookahead"); impl EpochTransition for JustificationAndFinalization { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { @@ -280,6 +283,16 @@ impl EpochTransition for ParticipationFlagUpdates { } } +impl EpochTransition for ProposerLookahead { + fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { + if state.fork_name_unchecked().fulu_enabled() { + process_proposer_lookahead(state, spec) + } else { + Ok(()) + } + } +} + impl> LoadCase for EpochProcessing { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { let spec = &testing_spec::(fork_name); @@ -338,6 +351,11 @@ impl> Case for EpochProcessing { { return false; } + + if !fork_name.fulu_enabled() && T::name() == "proposer_lookahead" { + return false; + } + true } diff --git a/testing/ef_tests/src/cases/fork.rs b/testing/ef_tests/src/cases/fork.rs index d40d57af3b..45d976f7c9 100644 --- a/testing/ef_tests/src/cases/fork.rs +++ b/testing/ef_tests/src/cases/fork.rs @@ -1,11 +1,10 @@ use super::*; use crate::case_result::compare_beacon_state_results_without_caches; -use crate::cases::common::previous_fork; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_eip7805, upgrade_to_electra, upgrade_to_fulu, + upgrade_to_eip7805, upgrade_to_electra, upgrade_to_fulu, upgrade_to_gloas, }; use types::BeaconState; @@ -33,7 +32,10 @@ impl LoadCase for ForkTest { assert_eq!(metadata.fork_name(), fork_name); // Decode pre-state with previous fork. - let pre_spec = &previous_fork(fork_name).make_genesis_spec(E::default_spec()); + let pre_spec = &fork_name + .previous_fork() + .unwrap_or(ForkName::Base) + .make_genesis_spec(E::default_spec()); let pre = ssz_decode_state(&path.join("pre.ssz_snappy"), pre_spec)?; // Decode post-state with target fork. @@ -58,7 +60,7 @@ impl Case for ForkTest { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let mut result_state = self.pre.clone(); let mut expected = Some(self.post.clone()); - let spec = &E::default_spec(); + let spec = &fork_name.make_genesis_spec(E::default_spec()); let mut result = match fork_name { ForkName::Base => panic!("phase0 not supported"), @@ -71,6 +73,7 @@ impl Case for ForkTest { ForkName::Electra => upgrade_to_electra(&mut result_state, spec).map(|_| result_state), ForkName::Eip7805 => upgrade_to_eip7805(&mut result_state, spec).map(|_| result_state), ForkName::Fulu => upgrade_to_fulu(&mut result_state, spec).map(|_| result_state), + ForkName::Gloas => upgrade_to_gloas(&mut result_state, spec).map(|_| result_state), }; compare_beacon_state_results_without_caches(&mut result, &mut expected) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index b507383190..8e9d438a24 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -5,19 +5,21 @@ use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::chain_config::{ - DisallowedReOrgOffsets, DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, + DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, + DEFAULT_RE_ORG_PARENT_THRESHOLD, DisallowedReOrgOffsets, }; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::slot_clock::SlotClock; use beacon_chain::{ + AvailabilityProcessingStatus, BeaconChainTypes, CachedHead, ChainConfig, NotifyExecutionLayer, attestation_verification::{ - obtain_indexed_attestation_and_committees_per_slot, VerifiedAttestation, + VerifiedAttestation, obtain_indexed_attestation_and_committees_per_slot, }, blob_verification::GossipVerifiedBlob, + custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - AvailabilityProcessingStatus, BeaconChainTypes, CachedHead, ChainConfig, NotifyExecutionLayer, }; -use execution_layer::{json_structures::JsonPayloadStatusV1Status, PayloadStatusV1}; +use execution_layer::{PayloadStatusV1, json_structures::JsonPayloadStatusV1Status}; use serde::Deserialize; use ssz_derive::Decode; use state_processing::state_advance::complete_state_advance; @@ -26,8 +28,9 @@ use std::sync::Arc; use std::time::Duration; use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, - BlobSidecar, BlobsList, BlockImportSource, Checkpoint, ExecutionBlockHash, Hash256, - IndexedAttestation, KzgProof, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecarList, + DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, KzgProof, + ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -91,14 +94,14 @@ impl From for PayloadStatusV1 { #[derive(Debug, Clone, Deserialize)] #[serde(untagged, deny_unknown_fields)] -pub enum Step { +pub enum Step { Tick { tick: u64, }, ValidBlock { block: TBlock, }, - MaybeValidBlock { + MaybeValidBlockAndBlobs { block: TBlock, blobs: Option, proofs: Option>, @@ -120,6 +123,11 @@ pub enum Step { Checks { checks: Box, }, + MaybeValidBlockAndColumns { + block: TBlock, + columns: Option, + valid: bool, + }, } #[derive(Debug, Clone, Deserialize)] @@ -136,7 +144,14 @@ pub struct ForkChoiceTest { pub anchor_block: BeaconBlock, #[allow(clippy::type_complexity)] pub steps: Vec< - Step, BlobsList, Attestation, AttesterSlashing, PowBlock>, + Step< + SignedBeaconBlock, + BlobsList, + DataColumnSidecarList, + Attestation, + AttesterSlashing, + PowBlock, + >, >, } @@ -150,7 +165,7 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec> = + let steps: Vec, String, String, String>> = yaml_decode_file(&path.join("steps.yaml"))?; // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps @@ -163,7 +178,7 @@ impl LoadCase for ForkChoiceTest { }) .map(|block| Step::ValidBlock { block }) } - Step::MaybeValidBlock { + Step::MaybeValidBlockAndBlobs { block, blobs, proofs, @@ -176,7 +191,7 @@ impl LoadCase for ForkChoiceTest { let blobs = blobs .map(|blobs| ssz_decode_file(&path.join(format!("{blobs}.ssz_snappy")))) .transpose()?; - Ok(Step::MaybeValidBlock { + Ok(Step::MaybeValidBlockAndBlobs { block, blobs, proofs, @@ -223,6 +238,31 @@ impl LoadCase for ForkChoiceTest { payload_status, }), Step::Checks { checks } => Ok(Step::Checks { checks }), + Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + } => { + let block = + ssz_decode_file_with(&path.join(format!("{block}.ssz_snappy")), |bytes| { + SignedBeaconBlock::from_ssz_bytes(bytes, spec) + })?; + let columns = columns + .map(|columns_vec| { + columns_vec + .into_iter() + .map(|column| { + ssz_decode_file(&path.join(format!("{column}.ssz_snappy"))) + }) + .collect::, _>>() + }) + .transpose()?; + Ok(Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + }) + } }) .collect::>()?; let anchor_state = ssz_decode_state(&path.join("anchor_state.ssz_snappy"), spec)?; @@ -263,14 +303,19 @@ impl Case for ForkChoiceTest { match step { Step::Tick { tick } => tester.set_tick(*tick), Step::ValidBlock { block } => { - tester.process_block(block.clone(), None, None, true)? + tester.process_block_and_blobs(block.clone(), None, None, true)? } - Step::MaybeValidBlock { + Step::MaybeValidBlockAndBlobs { block, blobs, proofs, valid, - } => tester.process_block(block.clone(), blobs.clone(), proofs.clone(), *valid)?, + } => tester.process_block_and_blobs( + block.clone(), + blobs.clone(), + proofs.clone(), + *valid, + )?, Step::Attestation { attestation } => tester.process_attestation(attestation)?, Step::AttesterSlashing { attester_slashing } => { tester.process_attester_slashing(attester_slashing.to_ref()) @@ -344,6 +389,14 @@ impl Case for ForkChoiceTest { tester.check_expected_proposer_head(*expected_proposer_head)?; } } + + Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + } => { + tester.process_block_and_columns(block.clone(), columns.clone(), *valid)?; + } } } @@ -384,6 +437,7 @@ impl Tester { .genesis_state_ephemeral_store(case.anchor_state.clone()) .mock_execution_layer() .recalculate_fork_times_with_genesis(0) + .node_custody_type(NodeCustodyType::Supernode) .mock_execution_layer_all_payloads_valid() .build(); @@ -454,7 +508,67 @@ impl Tester { .unwrap(); } - pub fn process_block( + pub fn process_block_and_columns( + &self, + block: SignedBeaconBlock, + columns: Option>, + valid: bool, + ) -> Result<(), Error> { + let block_root = block.canonical_root(); + let mut data_column_success = true; + + if let Some(columns) = columns.clone() { + let gossip_verified_data_columns = columns + .into_iter() + .map(|column| { + let subnet_id = DataColumnSubnetId::from_column_index(column.index, &self.spec); + GossipVerifiedDataColumn::new(column.clone(), subnet_id, &self.harness.chain) + .unwrap_or_else(|_| { + data_column_success = false; + GossipVerifiedDataColumn::__new_for_testing(column) + }) + }) + .collect(); + + let result = self.block_on_dangerous( + self.harness + .chain + .process_gossip_data_columns(gossip_verified_data_columns, || Ok(())), + )?; + if valid { + assert!(result.is_ok()); + } + }; + + let block = Arc::new(block); + let result: Result, _> = self + .block_on_dangerous(self.harness.chain.process_block( + block_root, + RpcBlock::new_without_blobs(Some(block_root), block.clone()), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ))? + .map(|avail: AvailabilityProcessingStatus| avail.try_into()); + let success = data_column_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); + if success != valid { + return Err(Error::DidntFail(format!( + "block with root {} was valid={} whilst test expects valid={}. result: {:?}", + block_root, + result.is_ok(), + valid, + result + ))); + } + + if !valid && columns.is_none() { + self.apply_invalid_block(&block)?; + } + + Ok(()) + } + + pub fn process_block_and_blobs( &self, block: SignedBeaconBlock, blobs: Option>, @@ -520,7 +634,7 @@ impl Tester { let result: Result, _> = self .block_on_dangerous(self.harness.chain.process_block( block_root, - RpcBlock::new_without_blobs(Some(block_root), block.clone(), 0), + RpcBlock::new_without_blobs(Some(block_root), block.clone()), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -537,66 +651,73 @@ impl Tester { ))); } - // Apply invalid blocks directly against the fork choice `on_block` function. This ensures - // that the block is being rejected by `on_block`, not just some upstream block processing - // function. When blobs exist, we don't do this. if !valid && blobs.is_none() { - // A missing parent block whilst `valid == false` means the test should pass. - if let Some(parent_block) = self + self.apply_invalid_block(&block)?; + } + + Ok(()) + } + + // Apply invalid blocks directly against the fork choice `on_block` function. This ensures + // that the block is being rejected by `on_block`, not just some upstream block processing + // function. When data columns or blobs exist, we don't do this. + fn apply_invalid_block(&self, block: &Arc>) -> Result<(), Error> { + let block_root = block.canonical_root(); + // A missing parent block whilst `valid == false` means the test should pass. + if let Some(parent_block) = self + .harness + .chain + .get_blinded_block(&block.parent_root()) + .unwrap() + { + let parent_state_root = parent_block.state_root(); + + let mut state = self .harness .chain - .get_blinded_block(&block.parent_root()) - .unwrap() - { - let parent_state_root = parent_block.state_root(); - - let mut state = self - .harness - .chain - .get_state( - &parent_state_root, - Some(parent_block.slot()), - CACHE_STATE_IN_TESTS, - ) - .unwrap() - .unwrap(); - - complete_state_advance( - &mut state, - Some(parent_state_root), - block.slot(), - &self.harness.chain.spec, + .get_state( + &parent_state_root, + Some(parent_block.slot()), + CACHE_STATE_IN_TESTS, ) + .unwrap() .unwrap(); - let block_delay = self - .harness - .chain - .slot_clock - .seconds_from_current_slot_start() - .unwrap(); + complete_state_advance( + &mut state, + Some(parent_state_root), + block.slot(), + &self.harness.chain.spec, + ) + .unwrap(); - let result = self - .harness - .chain - .canonical_head - .fork_choice_write_lock() - .on_block( - self.harness.chain.slot().unwrap(), - block.message(), - block_root, - block_delay, - &state, - PayloadVerificationStatus::Irrelevant, - &self.harness.chain.spec, - ); + let block_delay = self + .harness + .chain + .slot_clock + .seconds_from_current_slot_start() + .unwrap(); - if result.is_ok() { - return Err(Error::DidntFail(format!( - "block with root {} should fail on_block", - block_root, - ))); - } + let result = self + .harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_block( + self.harness.chain.slot().unwrap(), + block.message(), + block_root, + block_delay, + &state, + PayloadVerificationStatus::Irrelevant, + &self.harness.chain.spec, + ); + + if result.is_ok() { + return Err(Error::DidntFail(format!( + "block with root {} should fail on_block", + block_root, + ))); } } @@ -799,7 +920,7 @@ impl Tester { let cached_head = self.harness.chain.canonical_head.cached_head(); let next_slot = cached_head.snapshot.beacon_block.slot() + 1; let next_slot_epoch = next_slot.epoch(E::slots_per_epoch()); - let (proposer_indices, decision_root, _, fork) = + let (proposer_indices, decision_root, _, _, fork) = compute_proposer_duties_from_head(next_slot_epoch, &self.harness.chain).unwrap(); let proposer_index = proposer_indices[next_slot.as_usize() % E::slots_per_epoch() as usize]; @@ -883,7 +1004,7 @@ pub struct ManuallyVerifiedAttestation<'a, T: BeaconChainTypes> { } impl VerifiedAttestation for ManuallyVerifiedAttestation<'_, T> { - fn attestation(&self) -> AttestationRef { + fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.attestation.to_ref() } diff --git a/testing/ef_tests/src/cases/kzg_compute_cells.rs b/testing/ef_tests/src/cases/kzg_compute_cells.rs new file mode 100644 index 0000000000..bd7f3649d6 --- /dev/null +++ b/testing/ef_tests/src/cases/kzg_compute_cells.rs @@ -0,0 +1,54 @@ +use super::*; +use crate::case_result::compare_result; +use kzg::Cell; +use serde::Deserialize; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KZGComputeCellsInput { + pub blob: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct KZGComputeCells { + pub input: KZGComputeCellsInput, + pub output: Option>, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for KZGComputeCells { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("data.yaml").as_path()) + } +} + +impl Case for KZGComputeCells { + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.fulu_enabled() + } + + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let cells = parse_blob::(&self.input.blob) + .and_then(|blob| { + let blob = blob.as_ref().try_into().map_err(|e| { + Error::InternalError(format!("Failed to convert blob to kzg blob: {e:?}")) + })?; + let kzg = get_kzg(); + kzg.compute_cells(blob).map_err(|e| { + Error::InternalError(format!("Failed to compute cells and kzg proofs: {e:?}")) + }) + }) + .map(|cells| cells.to_vec()); + + let expected = self.output.as_ref().map(|cells| { + parse_cells_and_proofs(cells, &[]) + .map(|(cells, _)| cells) + .expect("Valid cells") + }); + + compare_result::, _>(&cells, &expected) + } +} diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index 66f50d534b..2f0a058402 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -2,7 +2,7 @@ use super::*; use crate::case_result::compare_result; use beacon_chain::kzg_utils::validate_blob; use kzg::trusted_setup::get_trusted_setup; -use kzg::{Cell, Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup}; +use kzg::{Cell, Error as KzgError, Kzg, KzgCommitment, KzgProof}; use serde::Deserialize; use std::marker::PhantomData; use std::sync::Arc; @@ -10,10 +10,7 @@ use std::sync::LazyLock; use types::Blob; static KZG: LazyLock> = LazyLock::new(|| { - let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) - .map_err(|e| Error::InternalError(format!("Failed to initialize trusted setup: {:?}", e))) - .expect("failed to initialize trusted setup"); - let kzg = Kzg::new_from_trusted_setup_das_enabled(trusted_setup) + let kzg = Kzg::new_from_trusted_setup(&get_trusted_setup()) .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e))) .expect("failed to initialize kzg"); Arc::new(kzg) diff --git a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs index e3edc0df0a..7973af861f 100644 --- a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs @@ -53,7 +53,7 @@ impl Case for KZGVerifyCellKZGProofBatch { let kzg = get_kzg(); match kzg.verify_cell_proof_batch(&cells, &proofs, cell_indices, &commitments) { Ok(_) => Ok(true), - Err(KzgError::KzgVerificationFailed) => Ok(false), + Err((_, KzgError::KzgVerificationFailed)) => Ok(false), Err(e) => Err(Error::InternalError(format!( "Failed to validate cells: {:?}", e diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index 14779c7e0d..9ea6327a86 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -1,11 +1,13 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; +use ssz_types::FixedVector; use tree_hash::Hash256; +use typenum::Unsigned; use types::{ - light_client_update, BeaconBlockBody, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, - BeaconBlockBodyEip7805, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, FixedVector, - FullPayload, Unsigned, + BeaconBlockBody, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyEip7805, + BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconBlockBodyGloas, BeaconState, FullPayload, + light_client_update, }; #[derive(Debug, Clone, Deserialize)] @@ -161,7 +163,7 @@ impl LoadCase for KzgInclusionMerkleProofValidity { return Err(Error::InternalError(format!( "KZG inclusion merkle proof validity test skipped for {:?}", fork_name - ))) + ))); } ForkName::Deneb => { ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() @@ -177,6 +179,9 @@ impl LoadCase for KzgInclusionMerkleProofValidity { ForkName::Fulu => { ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() } + ForkName::Gloas => { + ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() + } }; let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?; // Metadata does not exist in these tests but it is left like this just in case. @@ -279,7 +284,7 @@ impl LoadCase for BeaconBlockBodyMerkleProofValidity { return Err(Error::InternalError(format!( "Beacon block body merkle proof validity test skipped for {:?}", fork_name - ))) + ))); } ForkName::Capella => { ssz_decode_file::>(&path.join("object.ssz_snappy"))? @@ -299,6 +304,9 @@ impl LoadCase for BeaconBlockBodyMerkleProofValidity { ForkName::Fulu => { ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() } + ForkName::Gloas => { + ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() + } }; let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?; // Metadata does not exist in these tests but it is left like this just in case. diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 7178edb151..a53bce927c 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -10,22 +10,23 @@ use state_processing::per_block_processing::process_operations::{ process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, }; use state_processing::{ + ConsensusContext, per_block_processing::{ + VerifyBlockRoot, VerifySignatures, errors::BlockProcessingError, process_block_header, process_execution_payload, process_operations::{ altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, process_proposer_slashings, }, - process_sync_aggregate, process_withdrawals, VerifyBlockRoot, VerifySignatures, + process_sync_aggregate, process_withdrawals, }, - ConsensusContext, }; use std::fmt::Debug; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, - BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, - BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, + BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, + BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; @@ -171,7 +172,7 @@ impl Operation for Deposit { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_deposits(state, &[self.clone()], spec) + process_deposits(state, std::slice::from_ref(self), spec) } } @@ -194,7 +195,7 @@ impl Operation for ProposerSlashing { initialize_progressive_balances_cache(state, spec)?; process_proposer_slashings( state, - &[self.clone()], + std::slice::from_ref(self), VerifySignatures::True, &mut ctxt, spec, @@ -217,7 +218,12 @@ impl Operation for SignedVoluntaryExit { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_exits(state, &[self.clone()], VerifySignatures::True, spec) + process_exits( + state, + std::slice::from_ref(self), + VerifySignatures::True, + spec, + ) } } @@ -301,6 +307,7 @@ impl Operation for BeaconBlockBody> { ForkName::Deneb => BeaconBlockBody::Deneb(<_>::from_ssz_bytes(bytes)?), ForkName::Electra => BeaconBlockBody::Electra(<_>::from_ssz_bytes(bytes)?), ForkName::Fulu => BeaconBlockBody::Fulu(<_>::from_ssz_bytes(bytes)?), + // TODO(EIP-7732): See if we need to handle Gloas here _ => panic!(), }) }) @@ -357,9 +364,10 @@ impl Operation for BeaconBlockBody> { BeaconBlockBody::Electra(inner.clone_as_blinded()) } ForkName::Fulu => { - let inner = >>::from_ssz_bytes(bytes)?; - BeaconBlockBody::Electra(inner.clone_as_blinded()) + let inner = >>::from_ssz_bytes(bytes)?; + BeaconBlockBody::Fulu(inner.clone_as_blinded()) } + // TODO(EIP-7732): See if we need to handle Gloas here _ => panic!(), }) }) @@ -411,6 +419,7 @@ impl Operation for WithdrawalsPayload { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { + // TODO(EIP-7732): implement separate gloas and non-gloas variants of process_withdrawals process_withdrawals::<_, FullPayload<_>>(state, self.payload.to_ref(), spec) } } @@ -438,7 +447,12 @@ impl Operation for SignedBlsToExecutionChange { spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { - process_bls_to_execution_changes(state, &[self.clone()], VerifySignatures::True, spec) + process_bls_to_execution_changes( + state, + std::slice::from_ref(self), + VerifySignatures::True, + spec, + ) } } @@ -462,7 +476,7 @@ impl Operation for WithdrawalRequest { _extra: &Operations, ) -> Result<(), BlockProcessingError> { state.update_pubkey_cache()?; - process_withdrawal_requests(state, &[self.clone()], spec) + process_withdrawal_requests(state, std::slice::from_ref(self), spec) } } @@ -485,7 +499,7 @@ impl Operation for DepositRequest { spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { - process_deposit_requests(state, &[self.clone()], spec) + process_deposit_requests(state, std::slice::from_ref(self), spec) } } @@ -509,7 +523,7 @@ impl Operation for ConsolidationRequest { _extra: &Operations, ) -> Result<(), BlockProcessingError> { state.update_pubkey_cache()?; - process_consolidation_requests(state, &[self.clone()], spec) + process_consolidation_requests(state, std::slice::from_ref(self), spec) } } @@ -587,10 +601,10 @@ impl> Case for Operations { let mut state = pre_state.clone(); let mut expected = self.post.clone(); - if O::handler_name() != "withdrawals" { - if let Some(post_state) = expected.as_mut() { - post_state.build_all_committee_caches(spec).unwrap(); - } + if O::handler_name() != "withdrawals" + && let Some(post_state) = expected.as_mut() + { + post_state.build_all_committee_caches(spec).unwrap(); } let mut result = self diff --git a/testing/ef_tests/src/cases/rewards.rs b/testing/ef_tests/src/cases/rewards.rs index c5879f5c9c..798014a6b0 100644 --- a/testing/ef_tests/src/cases/rewards.rs +++ b/testing/ef_tests/src/cases/rewards.rs @@ -1,18 +1,17 @@ use super::*; use crate::case_result::compare_result_detailed; use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; -use compare_fields_derive::CompareFields; +use compare_fields::CompareFields; use serde::Deserialize; use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use state_processing::per_epoch_processing::base::rewards_and_penalties::ProposerRewardCalculation; use state_processing::{ - per_epoch_processing::{ - altair, - base::{self, rewards_and_penalties::AttestationDelta, ValidatorStatuses}, - Delta, - }, EpochProcessingError, + per_epoch_processing::{ + Delta, altair, + base::{self, ValidatorStatuses, rewards_and_penalties::AttestationDelta}, + }, }; use types::BeaconState; diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index 91bb995cc4..538783eaa9 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -4,8 +4,8 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::{ - per_block_processing, per_slot_processing, BlockProcessingError, BlockSignatureStrategy, - ConsensusContext, VerifyBlockRoot, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + per_block_processing, per_slot_processing, }; use types::{BeaconState, RelativeEpoch, SignedBeaconBlock}; diff --git a/testing/ef_tests/src/cases/ssz_generic.rs b/testing/ef_tests/src/cases/ssz_generic.rs index 3dc2f17968..1dd37a22ee 100644 --- a/testing/ef_tests/src/cases/ssz_generic.rs +++ b/testing/ef_tests/src/cases/ssz_generic.rs @@ -3,15 +3,19 @@ use super::*; use crate::cases::common::{DecimalU128, DecimalU256, SszStaticType}; use crate::cases::ssz_static::{check_serialization, check_tree_hash}; -use crate::decode::{log_file_access, snappy_decode_file, yaml_decode_file}; -use serde::{de::Error as SerdeError, Deserialize, Deserializer}; +use crate::decode::{context_yaml_decode_file, log_file_access, snappy_decode_file}; +use context_deserialize::{ContextDeserialize, context_deserialize}; +use milhouse::Vector; +use serde::{Deserialize, Deserializer, de::Error as SerdeError}; use ssz_derive::{Decode, Encode}; +use ssz_types::{BitList, BitVector, FixedVector, VariableList}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -use types::typenum::*; -use types::{BitList, BitVector, FixedVector, ForkName, VariableList, Vector}; +use typenum::*; +use types::ForkName; #[derive(Debug, Clone, Deserialize)] +#[context_deserialize(ForkName)] struct Metadata { root: String, #[serde(rename(deserialize = "signing_root"))] @@ -78,12 +82,16 @@ macro_rules! type_dispatch { "7" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U7>, $($rest)*), "8" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U8>, $($rest)*), "9" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U9>, $($rest)*), + "15" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U15>, $($rest)*), "16" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U16>, $($rest)*), + "17" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U17>, $($rest)*), "31" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U31>, $($rest)*), "32" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U32>, $($rest)*), + "33" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U33>, $($rest)*), "64" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U64>, $($rest)*), "128" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U128>, $($rest)*), "256" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U256>, $($rest)*), + "511" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U511>, $($rest)*), "512" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U512>, $($rest)*), "513" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U513>, $($rest)*), "1024" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* U1024>, $($rest)*), @@ -105,6 +113,8 @@ macro_rules! type_dispatch { "VarTestStruct" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* VarTestStruct>, $($rest)*), "ComplexTestStruct" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* ComplexTestStruct>, $($rest)*), "BitsStruct" => type_dispatch!($function, ($($arg),*), $base_ty, <$($param_ty,)* BitsStruct>, $($rest)*), + // EIP-7916 is still in draft and hasn't been implemented yet https://eips.ethereum.org/EIPS/eip-7916 + "ProgressiveTestStruct" | "ProgressiveBitsStruct" => Err(Error::SkippedKnownFailure), _ => Err(Error::FailedToParseTest(format!("unsupported: {}", $value))), } }; @@ -118,7 +128,7 @@ macro_rules! type_dispatch { } impl Case for SszGeneric { - fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let parts = self.case_name.split('_').collect::>(); match self.handler_name.as_str() { @@ -134,7 +144,7 @@ impl Case for SszGeneric { type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), Vector, <>, [elem_ty => primitive_type] @@ -142,7 +152,7 @@ impl Case for SszGeneric { )?; type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), FixedVector, <>, [elem_ty => primitive_type] @@ -159,7 +169,7 @@ impl Case for SszGeneric { type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), BitList, <>, [limit => typenum] @@ -170,21 +180,21 @@ impl Case for SszGeneric { type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), BitVector, <>, [length => typenum] )?; } "boolean" => { - ssz_generic_test::(&self.path)?; + ssz_generic_test::(&self.path, fork_name)?; } "uints" => { let type_name = "uint".to_owned() + parts[1]; type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), _, <>, [type_name.as_str() => primitive_type] @@ -195,7 +205,7 @@ impl Case for SszGeneric { type_dispatch!( ssz_generic_test, - (&self.path), + (&self.path, fork_name), _, <>, [type_name => test_container] @@ -207,10 +217,15 @@ impl Case for SszGeneric { } } -fn ssz_generic_test(path: &Path) -> Result<(), Error> { +fn ssz_generic_test< + T: SszStaticType + for<'de> ContextDeserialize<'de, ForkName> + TreeHash + ssz::Decode, +>( + path: &Path, + fork_name: ForkName, +) -> Result<(), Error> { let meta_path = path.join("meta.yaml"); let meta: Option = if meta_path.is_file() { - Some(yaml_decode_file(&meta_path)?) + Some(context_yaml_decode_file(&meta_path, fork_name)?) } else { None }; @@ -220,7 +235,7 @@ fn ssz_generic_test(path: &Path) -> R let value_path = path.join("value.yaml"); let value: Option = if value_path.is_file() { - Some(yaml_decode_file(&value_path)?) + Some(context_yaml_decode_file(&value_path, fork_name)?) } else { None }; @@ -246,17 +261,20 @@ fn ssz_generic_test(path: &Path) -> R // Containers for SSZ generic tests #[derive(Debug, Clone, Default, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct SingleFieldTestStruct { A: u8, } #[derive(Debug, Clone, Default, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct SmallTestStruct { A: u16, B: u16, } #[derive(Debug, Clone, Default, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct FixedTestStruct { A: u8, B: u64, @@ -264,6 +282,7 @@ struct FixedTestStruct { } #[derive(Debug, Clone, Default, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct VarTestStruct { A: u16, B: VariableList, @@ -271,6 +290,7 @@ struct VarTestStruct { } #[derive(Debug, Clone, Default, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct ComplexTestStruct { A: u16, B: VariableList, @@ -283,6 +303,7 @@ struct ComplexTestStruct { } #[derive(Debug, Clone, PartialEq, Decode, Encode, TreeHash, Deserialize)] +#[context_deserialize(ForkName)] struct BitsStruct { A: BitList, B: BitVector, @@ -299,14 +320,13 @@ where { let s: String = serde::de::Deserialize::deserialize(deserializer)?; let decoded: Vec = hex::decode(&s.as_str()[2..]).map_err(D::Error::custom)?; + let decoded_len = decoded.len(); - if decoded.len() > N::to_usize() { - Err(D::Error::custom(format!( + decoded.try_into().map_err(|_| { + D::Error::custom(format!( "Too many values for list, got: {}, limit: {}", - decoded.len(), + decoded_len, N::to_usize() - ))) - } else { - Ok(decoded.into()) - } + )) + }) } diff --git a/testing/ef_tests/src/cases/ssz_static.rs b/testing/ef_tests/src/cases/ssz_static.rs index c80977a8ac..3f066c2fe3 100644 --- a/testing/ef_tests/src/cases/ssz_static.rs +++ b/testing/ef_tests/src/cases/ssz_static.rs @@ -1,10 +1,11 @@ use super::*; use crate::case_result::compare_result; -use crate::decode::{snappy_decode_file, yaml_decode_file}; +use crate::decode::{context_yaml_decode_file, snappy_decode_file, yaml_decode_file}; +use context_deserialize::ContextDeserialize; use serde::Deserialize; use ssz::Decode; use tree_hash::TreeHash; -use types::{BeaconBlock, BeaconState, Hash256, SignedBeaconBlock}; +use types::{BeaconBlock, BeaconState, DataColumnsByRootIdentifier, Hash256, SignedBeaconBlock}; #[derive(Debug, Clone, Deserialize)] struct SszStaticRoots { @@ -37,18 +38,28 @@ pub struct SszStaticWithSpec { value: T, } -fn load_from_dir(path: &Path) -> Result<(SszStaticRoots, Vec, T), Error> { +fn load_from_dir ContextDeserialize<'de, ForkName>>( + path: &Path, + fork_name: ForkName, +) -> Result<(SszStaticRoots, Vec, T), Error> { + load_from_dir_with_context(path, fork_name) +} + +fn load_from_dir_with_context ContextDeserialize<'de, C>, C>( + path: &Path, + context: C, +) -> Result<(SszStaticRoots, Vec, T), Error> { let roots = yaml_decode_file(&path.join("roots.yaml"))?; let serialized = snappy_decode_file(&path.join("serialized.ssz_snappy")) .expect("serialized.ssz_snappy exists"); - let value = yaml_decode_file(&path.join("value.yaml"))?; + let value = context_yaml_decode_file(&path.join("value.yaml"), context)?; Ok((roots, serialized, value)) } -impl LoadCase for SszStatic { - fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { - load_from_dir(path).map(|(roots, serialized, value)| Self { +impl ContextDeserialize<'de, ForkName>> LoadCase for SszStatic { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + load_from_dir(path, fork_name).map(|(roots, serialized, value)| Self { roots, serialized, value, @@ -56,19 +67,9 @@ impl LoadCase for SszStatic { } } -impl LoadCase for SszStaticTHC { - fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { - load_from_dir(path).map(|(roots, serialized, value)| Self { - roots, - serialized, - value, - }) - } -} - -impl LoadCase for SszStaticWithSpec { - fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { - load_from_dir(path).map(|(roots, serialized, value)| Self { +impl ContextDeserialize<'de, ForkName>> LoadCase for SszStaticTHC { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + load_from_dir(path, fork_name).map(|(roots, serialized, value)| Self { roots, serialized, value, @@ -124,6 +125,16 @@ impl Case for SszStaticTHC> { } } +impl LoadCase for SszStaticWithSpec> { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + load_from_dir(path, fork_name).map(|(roots, serialized, value)| Self { + roots, + serialized, + value, + }) + } +} + impl Case for SszStaticWithSpec> { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); @@ -135,6 +146,16 @@ impl Case for SszStaticWithSpec> { } } +impl LoadCase for SszStaticWithSpec> { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + load_from_dir(path, fork_name).map(|(roots, serialized, value)| Self { + roots, + serialized, + value, + }) + } +} + impl Case for SszStaticWithSpec> { fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); @@ -145,3 +166,23 @@ impl Case for SszStaticWithSpec> { Ok(()) } } + +impl LoadCase for SszStaticWithSpec> { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + load_from_dir(path, fork_name).map(|(roots, serialized, value)| Self { + roots, + serialized, + value, + }) + } +} + +impl Case for SszStaticWithSpec> { + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + check_serialization(&self.value, &self.serialized, |bytes| { + DataColumnsByRootIdentifier::from_ssz_bytes(bytes) + })?; + check_tree_hash(&self.roots.root, self.value.tree_hash_root().as_slice())?; + Ok(()) + } +} diff --git a/testing/ef_tests/src/cases/transition.rs b/testing/ef_tests/src/cases/transition.rs index 0a21fe3fbc..4ca838f77b 100644 --- a/testing/ef_tests/src/cases/transition.rs +++ b/testing/ef_tests/src/cases/transition.rs @@ -3,8 +3,8 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::{ - per_block_processing, state_advance::complete_state_advance, BlockSignatureStrategy, - ConsensusContext, VerifyBlockRoot, + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing, + state_advance::complete_state_advance, }; use std::str::FromStr; use types::{BeaconState, Epoch, SignedBeaconBlock}; @@ -76,6 +76,15 @@ impl LoadCase for TransitionTest { spec.electra_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(metadata.fork_epoch); } + ForkName::Gloas => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(metadata.fork_epoch); + } } // Load blocks diff --git a/testing/ef_tests/src/decode.rs b/testing/ef_tests/src/decode.rs index eb88ac6af1..2074ffce23 100644 --- a/testing/ef_tests/src/decode.rs +++ b/testing/ef_tests/src/decode.rs @@ -1,4 +1,5 @@ use super::*; +use context_deserialize::ContextDeserialize; use fs2::FileExt; use snap::raw::Decoder; use std::fs::{self}; @@ -35,6 +36,27 @@ pub fn yaml_decode(string: &str) -> Result(string: &'de str, context: C) -> Result +where + T: ContextDeserialize<'de, C>, +{ + let deserializer = serde_yaml::Deserializer::from_str(string); + T::context_deserialize(deserializer, context) + .map_err(|e| Error::FailedToParseTest(format!("{:?}", e))) +} + +pub fn context_yaml_decode_file(path: &Path, context: C) -> Result +where + T: for<'de> ContextDeserialize<'de, C>, +{ + log_file_access(path); + fs::read_to_string(path) + .map_err(|e| { + Error::FailedToParseTest(format!("Unable to load {}: {:?}", path.display(), e)) + }) + .and_then(|s| context_yaml_decode(&s, context)) +} + pub fn yaml_decode_file(path: &Path) -> Result { log_file_access(path); fs::read_to_string(path) diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index a375498239..a5b2ffada3 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1,7 +1,8 @@ use crate::cases::{self, Case, Cases, EpochTransition, LoadCase, Operation}; use crate::type_name::TypeName; -use crate::{type_name, FeatureName}; -use derivative::Derivative; +use crate::{FeatureName, type_name}; +use context_deserialize::ContextDeserialize; +use educe::Educe; use std::fs::{self, DirEntry}; use std::marker::PhantomData; use std::path::PathBuf; @@ -21,7 +22,7 @@ pub trait Handler { // Add forks here to exclude them from EF spec testing. Helpful for adding future or // unspecified forks. fn disabled_forks(&self) -> Vec { - vec![ForkName::Fulu] + vec![ForkName::Gloas] } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { @@ -50,6 +51,19 @@ pub trait Handler { } } + // Do NOT override this function. + // TODO: use default keyword when stable. + fn rayon_enabled() -> bool { + #[cfg(feature = "disable_rayon")] + { + false + } + #[cfg(not(feature = "disable_rayon"))] + { + Self::use_rayon() + } + } + fn use_rayon() -> bool { true } @@ -79,13 +93,12 @@ pub trait Handler { .filter_map(as_directory) .map(|test_case_dir| { let path = test_case_dir.path(); - let case = Self::Case::load_from_dir(&path, fork_name).expect("test should load"); (path, case) }) .collect(); - let results = Cases { test_cases }.test_results(fork_name, Self::use_rayon()); + let results = Cases { test_cases }.test_results(fork_name, Self::rayon_enabled()); let name = format!( "{}/{}/{}", @@ -127,7 +140,7 @@ pub trait Handler { }) .collect(); - let results = Cases { test_cases }.test_results(fork_name, Self::use_rayon()); + let results = Cases { test_cases }.test_results(fork_name, Self::rayon_enabled()); let name = format!( "{}/{}/{}", @@ -141,8 +154,8 @@ pub trait Handler { macro_rules! bls_eth_handler { ($runner_name: ident, $case_name:ident, $handler_name:expr) => { - #[derive(Derivative)] - #[derivative(Default(bound = ""))] + #[derive(Educe)] + #[educe(Default)] pub struct $runner_name; impl Handler for $runner_name { @@ -161,8 +174,8 @@ macro_rules! bls_eth_handler { macro_rules! bls_handler { ($runner_name: ident, $case_name:ident, $handler_name:expr) => { - #[derive(Derivative)] - #[derivative(Default(bound = ""))] + #[derive(Educe)] + #[educe(Default)] pub struct $runner_name; impl Handler for $runner_name { @@ -205,7 +218,7 @@ macro_rules! bls_handler { }) .collect(); - let results = Cases { test_cases }.test_results(fork_name, Self::use_rayon()); + let results = Cases { test_cases }.test_results(fork_name, Self::rayon_enabled()); let name = format!( "{}/{}/{}", @@ -322,18 +335,42 @@ impl SszStaticHandler { } /// Handler for SSZ types that implement `CachedTreeHash`. -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct SszStaticTHCHandler(PhantomData<(T, E)>); /// Handler for SSZ types that don't implement `ssz::Decode`. -#[derive(Derivative)] -#[derivative(Default(bound = ""))] -pub struct SszStaticWithSpecHandler(PhantomData<(T, E)>); +pub struct SszStaticWithSpecHandler { + supported_forks: Vec, + _phantom: PhantomData<(T, E)>, +} + +impl Default for SszStaticWithSpecHandler { + fn default() -> Self { + Self::for_forks(ForkName::list_all()) + } +} + +impl SszStaticWithSpecHandler { + pub fn for_forks(supported_forks: Vec) -> Self { + SszStaticWithSpecHandler { + supported_forks, + _phantom: PhantomData, + } + } + + pub fn fulu_and_later() -> Self { + Self::for_forks(ForkName::list_all()[6..].to_vec()) + } +} impl Handler for SszStaticHandler where - T: cases::SszStaticType + tree_hash::TreeHash + ssz::Decode + TypeName, + T: cases::SszStaticType + + for<'de> ContextDeserialize<'de, ForkName> + + tree_hash::TreeHash + + ssz::Decode + + TypeName, E: TypeName, { type Case = cases::SszStatic; @@ -353,25 +390,6 @@ where fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - // TODO(fulu): to be removed once Fulu types start differing from Electra. We currently run Fulu tests as a - // "feature" - this means we use Electra types for Fulu SSZ tests (except for PeerDAS types, e.g. `DataColumnSidecar`). - // - // This ensures we only run the tests **once** for `Fulu`, using the types matching the - // correct fork, e.g. `Fulu` uses SSZ types from `Electra` as of spec test version - // `v1.5.0-beta.0`, therefore the `Fulu` tests should get included when testing Deneb types. - // - // e.g. Fulu test vectors are executed in the 2nd line below, but excluded in the 1st - // line when testing the type `AttestationElectra`: - // - // ``` - // SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); - // SszStaticHandler::, MainnetEthSpec>::electra_only().run(); - // ``` - feature_name == FeatureName::Fulu - && self.supported_forks.contains(&feature_name.fork_name()) - } } impl Handler for SszStaticTHCHandler, E> @@ -391,10 +409,6 @@ where fn handler_name(&self) -> String { BeaconState::::name().into() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } impl Handler for SszStaticWithSpecHandler @@ -417,13 +431,13 @@ where T::name().into() } - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + self.supported_forks.contains(&fork_name) } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct ShufflingHandler(PhantomData); impl Handler for ShufflingHandler { @@ -446,8 +460,8 @@ impl Handler for ShufflingHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct SanityBlocksHandler(PhantomData); impl Handler for SanityBlocksHandler { @@ -472,8 +486,8 @@ impl Handler for SanityBlocksHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct SanitySlotsHandler(PhantomData); impl Handler for SanitySlotsHandler { @@ -497,8 +511,8 @@ impl Handler for SanitySlotsHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct RandomHandler(PhantomData); impl Handler for RandomHandler { @@ -517,8 +531,8 @@ impl Handler for RandomHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct EpochProcessingHandler(PhantomData<(E, T)>); impl> Handler for EpochProcessingHandler { @@ -567,8 +581,8 @@ impl Handler for RewardsHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct ForkHandler(PhantomData); impl Handler for ForkHandler { @@ -587,8 +601,8 @@ impl Handler for ForkHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct TransitionHandler(PhantomData); impl Handler for TransitionHandler { @@ -607,8 +621,8 @@ impl Handler for TransitionHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct FinalityHandler(PhantomData); impl Handler for FinalityHandler { @@ -691,8 +705,8 @@ impl Handler for ForkChoiceHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct OptimisticSyncHandler(PhantomData); impl Handler for OptimisticSyncHandler { @@ -720,8 +734,8 @@ impl Handler for OptimisticSyncHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct GenesisValidityHandler(PhantomData); impl Handler for GenesisValidityHandler { @@ -740,8 +754,8 @@ impl Handler for GenesisValidityHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct GenesisInitializationHandler(PhantomData); impl Handler for GenesisInitializationHandler { @@ -760,8 +774,8 @@ impl Handler for GenesisInitializationHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGBlobToKZGCommitmentHandler(PhantomData); impl Handler for KZGBlobToKZGCommitmentHandler { @@ -780,8 +794,8 @@ impl Handler for KZGBlobToKZGCommitmentHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGComputeBlobKZGProofHandler(PhantomData); impl Handler for KZGComputeBlobKZGProofHandler { @@ -800,8 +814,8 @@ impl Handler for KZGComputeBlobKZGProofHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGComputeKZGProofHandler(PhantomData); impl Handler for KZGComputeKZGProofHandler { @@ -820,8 +834,8 @@ impl Handler for KZGComputeKZGProofHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGVerifyBlobKZGProofHandler(PhantomData); impl Handler for KZGVerifyBlobKZGProofHandler { @@ -840,8 +854,8 @@ impl Handler for KZGVerifyBlobKZGProofHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGVerifyBlobKZGProofBatchHandler(PhantomData); impl Handler for KZGVerifyBlobKZGProofBatchHandler { @@ -860,8 +874,8 @@ impl Handler for KZGVerifyBlobKZGProofBatchHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGVerifyKZGProofHandler(PhantomData); impl Handler for KZGVerifyKZGProofHandler { @@ -880,8 +894,8 @@ impl Handler for KZGVerifyKZGProofHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct GetCustodyGroupsHandler(PhantomData); impl Handler for GetCustodyGroupsHandler { @@ -898,14 +912,10 @@ impl Handler for GetCustodyGroupsHandler { fn handler_name(&self) -> String { "get_custody_groups".into() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct ComputeColumnsForCustodyGroupHandler(PhantomData); impl Handler for ComputeColumnsForCustodyGroupHandler { @@ -922,14 +932,30 @@ impl Handler for ComputeColumnsForCustodyGroupHandler fn handler_name(&self) -> String { "compute_columns_for_custody_group".into() } +} - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu +#[derive(Educe)] +#[educe(Default)] +pub struct KZGComputeCellsHandler(PhantomData); + +impl Handler for KZGComputeCellsHandler { + type Case = cases::KZGComputeCells; + + fn config_name() -> &'static str { + "general" + } + + fn runner_name() -> &'static str { + "kzg" + } + + fn handler_name(&self) -> String { + "compute_cells".into() } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGComputeCellsAndKZGProofHandler(PhantomData); impl Handler for KZGComputeCellsAndKZGProofHandler { @@ -946,14 +972,10 @@ impl Handler for KZGComputeCellsAndKZGProofHandler { fn handler_name(&self) -> String { "compute_cells_and_kzg_proofs".into() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGVerifyCellKZGProofBatchHandler(PhantomData); impl Handler for KZGVerifyCellKZGProofBatchHandler { @@ -970,14 +992,10 @@ impl Handler for KZGVerifyCellKZGProofBatchHandler { fn handler_name(&self) -> String { "verify_cell_kzg_proof_batch".into() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KZGRecoverCellsAndKZGProofHandler(PhantomData); impl Handler for KZGRecoverCellsAndKZGProofHandler { @@ -994,14 +1012,10 @@ impl Handler for KZGRecoverCellsAndKZGProofHandler { fn handler_name(&self) -> String { "recover_cells_and_kzg_proofs".into() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct KzgInclusionMerkleProofValidityHandler(PhantomData); impl Handler for KzgInclusionMerkleProofValidityHandler { @@ -1022,14 +1036,10 @@ impl Handler for KzgInclusionMerkleProofValidityHandler bool { fork_name.deneb_enabled() } - - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Fulu - } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct MerkleProofValidityHandler(PhantomData); impl Handler for MerkleProofValidityHandler { @@ -1052,8 +1062,8 @@ impl Handler for MerkleProofValidityHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct LightClientUpdateHandler(PhantomData); impl Handler for LightClientUpdateHandler { @@ -1077,8 +1087,8 @@ impl Handler for LightClientUpdateHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct OperationsHandler(PhantomData<(E, O)>); impl> Handler for OperationsHandler { @@ -1097,8 +1107,8 @@ impl> Handler for OperationsHandler } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] +#[derive(Educe)] +#[educe(Default)] pub struct SszGenericHandler(PhantomData); impl Handler for SszGenericHandler { diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index e7367719d7..8ec4860cab 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -4,8 +4,8 @@ pub use cases::{ Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, - PendingConsolidations, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, - SlashingsReset, SyncCommitteeUpdates, + PendingConsolidations, ProposerLookahead, RandaoMixesReset, RegistryUpdates, + RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, }; pub use decode::log_file_access; pub use error::Error; diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 387e77310d..f57170d219 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -58,7 +58,7 @@ type_name_generic!(BeaconBlockBodyFulu, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); -type_name!(DataColumnsByRootIdentifier); +type_name_generic!(DataColumnsByRootIdentifier, "DataColumnsByRootIdentifier"); type_name_generic!(BlobSidecar); type_name_generic!(DataColumnSidecar); type_name!(Checkpoint); @@ -76,6 +76,7 @@ type_name_generic!(ExecutionPayloadCapella, "ExecutionPayload"); type_name_generic!(ExecutionPayloadDeneb, "ExecutionPayload"); type_name_generic!(ExecutionPayloadElectra, "ExecutionPayload"); type_name_generic!(ExecutionPayloadFulu, "ExecutionPayload"); +type_name_generic!(ExecutionPayloadEip7805, "ExecutionPayload"); type_name_generic!(FullPayload, "ExecutionPayload"); type_name_generic!(ExecutionPayloadHeader); type_name_generic!(ExecutionPayloadHeaderBellatrix, "ExecutionPayloadHeader"); @@ -83,6 +84,7 @@ type_name_generic!(ExecutionPayloadHeaderCapella, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderDeneb, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderElectra, "ExecutionPayloadHeader"); type_name_generic!(ExecutionPayloadHeaderFulu, "ExecutionPayloadHeader"); +type_name_generic!(ExecutionPayloadHeaderEip7805, "ExecutionPayloadHeader"); type_name_generic!(ExecutionRequests); type_name_generic!(BlindedPayload, "ExecutionPayloadHeader"); type_name!(Fork); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index d333cdbb11..0cec69c97e 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1,6 +1,7 @@ #![cfg(feature = "ef_tests")] use ef_tests::*; +use typenum::Unsigned; use types::*; // Check that the hand-computed multiplications on EthSpec are correctly computed. @@ -237,9 +238,7 @@ macro_rules! ssz_static_test_no_run { #[cfg(feature = "fake_crypto")] mod ssz_static { - use ef_tests::{ - FeatureName, Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler, - }; + use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; use types::historical_summary::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, @@ -660,20 +659,24 @@ mod ssz_static { #[test] fn data_column_sidecar() { - SszStaticHandler::, MinimalEthSpec>::default() - .run_for_feature(FeatureName::Fulu); - SszStaticHandler::, MainnetEthSpec>::default() - .run_for_feature(FeatureName::Fulu); + SszStaticHandler::, MinimalEthSpec>::fulu_and_later() + .run(); + SszStaticHandler::, MainnetEthSpec>::fulu_and_later() + .run(); } #[test] - #[ignore] - // TODO(das): enable once EF tests are updated to latest release. fn data_column_by_root_identifier() { - // SszStaticHandler::::default() - // .run_for_feature(FeatureName::Fulu); - // SszStaticHandler::::default() - // .run_for_feature(FeatureName::Fulu); + SszStaticWithSpecHandler::< + DataColumnsByRootIdentifier, + MinimalEthSpec, + >::fulu_and_later() + .run(); + SszStaticWithSpecHandler::< + DataColumnsByRootIdentifier, + MainnetEthSpec, + >::fulu_and_later() + .run(); } #[test] @@ -828,6 +831,12 @@ fn epoch_processing_participation_flag_updates() { EpochProcessingHandler::::default().run(); } +#[test] +fn epoch_processing_proposer_lookahead() { + EpochProcessingHandler::::default().run(); + EpochProcessingHandler::::default().run(); +} + #[test] fn fork_upgrade() { ForkHandler::::default().run(); @@ -941,6 +950,11 @@ fn kzg_verify_kzg_proof() { KZGVerifyKZGProofHandler::::default().run(); } +#[test] +fn kzg_compute_cells() { + KZGComputeCellsHandler::::default().run(); +} + #[test] fn kzg_compute_cells_and_proofs() { KZGComputeCellsAndKZGProofHandler::::default().run(); diff --git a/testing/eth1_test_rig/.gitignore b/testing/eth1_test_rig/.gitignore deleted file mode 100644 index 81b46ff033..0000000000 --- a/testing/eth1_test_rig/.gitignore +++ /dev/null @@ -1 +0,0 @@ -contract/ diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml deleted file mode 100644 index 9b0ac5ec9b..0000000000 --- a/testing/eth1_test_rig/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "eth1_test_rig" -version = "0.2.0" -authors = ["Paul Hauner "] -edition = { workspace = true } - -[dependencies] -deposit_contract = { workspace = true } -ethers-contract = "1.0.2" -ethers-core = { workspace = true } -ethers-providers = { workspace = true } -hex = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -types = { workspace = true } -unused_port = { workspace = true } diff --git a/testing/eth1_test_rig/src/anvil.rs b/testing/eth1_test_rig/src/anvil.rs deleted file mode 100644 index c6c37ae4a7..0000000000 --- a/testing/eth1_test_rig/src/anvil.rs +++ /dev/null @@ -1,100 +0,0 @@ -use ethers_core::utils::{Anvil, AnvilInstance}; -use ethers_providers::{Http, Middleware, Provider}; -use serde_json::json; -use unused_port::unused_tcp4_port; - -/// Provides a dedicated `anvil` instance. -/// -/// Requires that `anvil` is installed and available on `PATH`. -pub struct AnvilCliInstance { - pub port: u16, - pub anvil: AnvilInstance, - pub client: Provider, - chain_id: u64, -} - -impl AnvilCliInstance { - fn new_from_child(anvil_instance: Anvil, chain_id: u64, port: u16) -> Result { - let client = Provider::::try_from(&endpoint(port)) - .map_err(|e| format!("Failed to start HTTP transport connected to anvil: {:?}", e))?; - Ok(Self { - port, - anvil: anvil_instance.spawn(), - client, - chain_id, - }) - } - pub fn new(chain_id: u64) -> Result { - let port = unused_tcp4_port()?; - - let anvil = Anvil::new() - .port(port) - .mnemonic("vast thought differ pull jewel broom cook wrist tribe word before omit") - .arg("--balance") - .arg("1000000000") - .arg("--gas-limit") - .arg("1000000000") - .arg("--accounts") - .arg("10") - .arg("--chain-id") - .arg(format!("{}", chain_id)); - - Self::new_from_child(anvil, chain_id, port) - } - - pub fn fork(&self) -> Result { - let port = unused_tcp4_port()?; - - let anvil = Anvil::new() - .port(port) - .arg("--chain-id") - .arg(format!("{}", self.chain_id())) - .fork(self.endpoint()); - - Self::new_from_child(anvil, self.chain_id, port) - } - - /// Returns the endpoint that this instance is listening on. - pub fn endpoint(&self) -> String { - endpoint(self.port) - } - - /// Returns the chain id of the anvil instance - pub fn chain_id(&self) -> u64 { - self.chain_id - } - - /// Increase the timestamp on future blocks by `increase_by` seconds. - pub async fn increase_time(&self, increase_by: u64) -> Result<(), String> { - self.client - .request("evm_increaseTime", vec![json!(increase_by)]) - .await - .map(|_json_value: u64| ()) - .map_err(|e| format!("Failed to increase time on EVM (is this anvil?): {:?}", e)) - } - - /// Returns the current block number, as u64 - pub async fn block_number(&self) -> Result { - self.client - .get_block_number() - .await - .map(|v| v.as_u64()) - .map_err(|e| format!("Failed to get block number: {:?}", e)) - } - - /// Mines a single block. - pub async fn evm_mine(&self) -> Result<(), String> { - self.client - .request("evm_mine", ()) - .await - .map(|_: String| ()) - .map_err(|_| { - "utils should mine new block with evm_mine (only works with anvil/ganache!)" - .to_string() - }) - } -} - -fn endpoint(port: u16) -> String { - format!("http://127.0.0.1:{}", port) -} diff --git a/testing/eth1_test_rig/src/lib.rs b/testing/eth1_test_rig/src/lib.rs deleted file mode 100644 index 3cba908261..0000000000 --- a/testing/eth1_test_rig/src/lib.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Provides utilities for deploying and manipulating the eth2 deposit contract on the eth1 chain. -//! -//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) to simulate -//! the deposit contract for testing beacon node eth1 integration. -//! -//! Not tested to work with actual clients (e.g., geth). It should work fine, however there may be -//! some initial issues. -mod anvil; - -use anvil::AnvilCliInstance; -use deposit_contract::{ - encode_eth1_tx_data, testnet, ABI, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS, -}; -use ethers_contract::Contract; -use ethers_core::{ - abi::Abi, - types::{transaction::eip2718::TypedTransaction, Address, Bytes, TransactionRequest, U256}, -}; -pub use ethers_providers::{Http, Middleware, Provider}; -use std::time::Duration; -use tokio::time::sleep; -use types::{test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, Signature}; -use types::{DepositData, FixedBytesExtended}; - -pub const DEPLOYER_ACCOUNTS_INDEX: usize = 0; -pub const DEPOSIT_ACCOUNTS_INDEX: usize = 0; - -/// Provides a dedicated anvil instance with the deposit contract already deployed. -pub struct AnvilEth1Instance { - pub anvil: AnvilCliInstance, - pub deposit_contract: DepositContract, -} - -impl AnvilEth1Instance { - pub async fn new(chain_id: u64) -> Result { - let anvil = AnvilCliInstance::new(chain_id)?; - DepositContract::deploy(anvil.client.clone(), 0, None) - .await - .map(|deposit_contract| Self { - anvil, - deposit_contract, - }) - } - - pub fn endpoint(&self) -> String { - self.anvil.endpoint() - } - - pub fn json_rpc_client(&self) -> Provider { - self.anvil.client.clone() - } -} - -/// Deploys and provides functions for the eth2 deposit contract, deployed on the eth1 chain. -#[derive(Clone, Debug)] -pub struct DepositContract { - client: Provider, - contract: Contract>, -} - -impl DepositContract { - pub async fn deploy( - client: Provider, - confirmations: usize, - password: Option, - ) -> Result { - Self::deploy_bytecode(client, confirmations, BYTECODE, ABI, password).await - } - - pub async fn deploy_testnet( - client: Provider, - confirmations: usize, - password: Option, - ) -> Result { - Self::deploy_bytecode( - client, - confirmations, - testnet::BYTECODE, - testnet::ABI, - password, - ) - .await - } - - async fn deploy_bytecode( - client: Provider, - confirmations: usize, - bytecode: &[u8], - abi: &[u8], - password: Option, - ) -> Result { - let abi = Abi::load(abi).map_err(|e| format!("Invalid deposit contract abi: {:?}", e))?; - let address = - deploy_deposit_contract(client.clone(), confirmations, bytecode.to_vec(), password) - .await - .map_err(|e| { - format!( - "Failed to deploy contract: {}. Is the RPC server running?.", - e - ) - })?; - - let contract = Contract::new(address, abi, client.clone()); - Ok(Self { client, contract }) - } - - /// The deposit contract's address in `0x00ab...` format. - pub fn address(&self) -> String { - format!("0x{:x}", self.contract.address()) - } - - /// A helper to return a fully-formed `DepositData`. Does not submit the deposit data to the - /// smart contact. - pub fn deposit_helper( - &self, - keypair: Keypair, - withdrawal_credentials: Hash256, - amount: u64, - ) -> DepositData { - let mut deposit = DepositData { - pubkey: keypair.pk.into(), - withdrawal_credentials, - amount, - signature: Signature::empty().into(), - }; - - deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); - - deposit - } - - /// Creates a random, valid deposit and submits it to the deposit contract. - /// - /// The keypairs are created randomly and destroyed. - pub async fn deposit_random(&self) -> Result<(), String> { - let keypair = Keypair::random(); - - let mut deposit = DepositData { - pubkey: keypair.pk.into(), - withdrawal_credentials: Hash256::zero(), - amount: 32_000_000_000, - signature: Signature::empty().into(), - }; - - deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); - - self.deposit(deposit).await - } - - /// Perfoms a blocking deposit. - pub async fn deposit(&self, deposit_data: DepositData) -> Result<(), String> { - self.deposit_async(deposit_data) - .await - .map_err(|e| format!("Deposit failed: {:?}", e)) - } - - pub async fn deposit_deterministic_async( - &self, - keypair_index: usize, - amount: u64, - ) -> Result<(), String> { - let keypair = generate_deterministic_keypair(keypair_index); - - let mut deposit = DepositData { - pubkey: keypair.pk.into(), - withdrawal_credentials: Hash256::zero(), - amount, - signature: Signature::empty().into(), - }; - - deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); - - self.deposit_async(deposit).await - } - - /// Performs a non-blocking deposit. - pub async fn deposit_async(&self, deposit_data: DepositData) -> Result<(), String> { - let from = self - .client - .get_accounts() - .await - .map_err(|e| format!("Failed to get accounts: {:?}", e)) - .and_then(|accounts| { - accounts - .get(DEPOSIT_ACCOUNTS_INDEX) - .cloned() - .ok_or_else(|| "Insufficient accounts for deposit".to_string()) - })?; - // Note: the reason we use this `TransactionRequest` instead of just using the - // function in `self.contract` is so that the `eth1_tx_data` function gets used - // during testing. - // - // It's important that `eth1_tx_data` stays correct and does not suffer from - // code-rot. - let tx_request = TransactionRequest::new() - .from(from) - .to(self.contract.address()) - .gas(DEPOSIT_GAS) - .value(from_gwei(deposit_data.amount)) - .data(Bytes::from(encode_eth1_tx_data(&deposit_data).map_err( - |e| format!("Failed to encode deposit data: {:?}", e), - )?)); - - let pending_tx = self - .client - .send_transaction(tx_request, None) - .await - .map_err(|e| format!("Failed to call deposit fn: {:?}", e))?; - - pending_tx - .interval(Duration::from_millis(10)) - .confirmations(0) - .await - .map_err(|e| format!("Transaction failed to resolve: {:?}", e))? - .ok_or_else(|| "Transaction dropped from mempool".to_string())?; - Ok(()) - } - - /// Peforms many deposits, each preceded by a delay. - pub async fn deposit_multiple(&self, deposits: Vec) -> Result<(), String> { - for deposit in deposits.into_iter() { - sleep(deposit.delay).await; - self.deposit_async(deposit.deposit).await?; - } - Ok(()) - } -} - -/// Describes a deposit and a delay that should should precede it's submission to the deposit -/// contract. -#[derive(Clone)] -pub struct DelayThenDeposit { - /// Wait this duration ... - pub delay: Duration, - /// ... then submit this deposit. - pub deposit: DepositData, -} - -fn from_gwei(gwei: u64) -> U256 { - U256::from(gwei) * U256::exp10(9) -} - -/// Deploys the deposit contract to the given web3 instance using the account with index -/// `DEPLOYER_ACCOUNTS_INDEX`. -async fn deploy_deposit_contract( - client: Provider, - confirmations: usize, - bytecode: Vec, - password_opt: Option, -) -> Result { - let from_address = client - .get_accounts() - .await - .map_err(|e| format!("Failed to get accounts: {:?}", e)) - .and_then(|accounts| { - accounts - .get(DEPLOYER_ACCOUNTS_INDEX) - .cloned() - .ok_or_else(|| "Insufficient accounts for deployer".to_string()) - })?; - - let deploy_address = if let Some(password) = password_opt { - let result = client - .request( - "personal_unlockAccount", - vec![from_address.to_string(), password], - ) - .await; - - match result { - Ok(true) => from_address, - Ok(false) => return Err("Eth1 node refused to unlock account".to_string()), - Err(e) => return Err(format!("Eth1 unlock request failed: {:?}", e)), - } - } else { - from_address - }; - - let mut bytecode = String::from_utf8(bytecode).unwrap(); - bytecode.retain(|c| c.is_ascii_hexdigit()); - let bytecode = hex::decode(&bytecode[1..]).unwrap(); - - let deploy_tx: TypedTransaction = TransactionRequest::new() - .from(deploy_address) - .data(Bytes::from(bytecode)) - .gas(CONTRACT_DEPLOY_GAS) - .into(); - - let pending_tx = client - .send_transaction(deploy_tx, None) - .await - .map_err(|e| format!("Failed to send tx: {:?}", e))?; - - let tx = pending_tx - .interval(Duration::from_millis(500)) - .confirmations(confirmations) - .await - .map_err(|e| format!("Failed to fetch tx receipt: {:?}", e))?; - tx.and_then(|tx| tx.contract_address) - .ok_or_else(|| "Deposit contract not deployed successfully".to_string()) -} diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 55c42eb9d3..034b6c5c8a 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -3,26 +3,30 @@ name = "execution_engine_integration" version = "0.1.0" edition = { workspace = true } +[features] +portable = ["types/portable"] + [dependencies] +alloy-network = { workspace = true } +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true } +alloy-rpc-types-eth = { workspace = true } +alloy-signer-local = { workspace = true } async-channel = { workspace = true } +bls = { workspace = true } deposit_contract = { workspace = true } -ethers-core = { workspace = true } -ethers-middleware = { workspace = true } -ethers-providers = { workspace = true } -ethers-signers = { workspace = true } execution_layer = { workspace = true } +fixed_bytes = { workspace = true } fork_choice = { workspace = true } futures = { workspace = true } hex = { workspace = true } logging = { workspace = true } +network_utils = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } task_executor = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } +typenum = { workspace = true } types = { workspace = true } -unused_port = { workspace = true } - -[features] -portable = ["types/portable"] diff --git a/testing/execution_engine_integration/src/execution_engine.rs b/testing/execution_engine_integration/src/execution_engine.rs index 61a50b0405..3bb8585e44 100644 --- a/testing/execution_engine_integration/src/execution_engine.rs +++ b/testing/execution_engine_integration/src/execution_engine.rs @@ -1,10 +1,11 @@ -use ethers_providers::{Http, Provider}; +use alloy_provider::ProviderBuilder; use execution_layer::DEFAULT_JWT_FILE; +use network_utils::unused_port::unused_tcp4_port; +use reqwest::Url; use sensitive_url::SensitiveUrl; use std::path::PathBuf; use std::process::Child; use tempfile::TempDir; -use unused_port::unused_tcp4_port; pub const KEYSTORE_PASSWORD: &str = "testpwd"; pub const ACCOUNT1: &str = "7b8C3a386C0eea54693fFB0DA17373ffC9228139"; @@ -34,7 +35,7 @@ pub struct ExecutionEngine { http_port: u16, http_auth_port: u16, child: Child, - pub provider: Provider, + pub provider: Box, } impl Drop for ExecutionEngine { @@ -53,8 +54,9 @@ impl ExecutionEngine { let http_port = unused_tcp4_port().unwrap(); let http_auth_port = unused_tcp4_port().unwrap(); let child = E::start_client(&datadir, http_port, http_auth_port, jwt_secret_path); - let provider = Provider::::try_from(format!("http://localhost:{}", http_port)) - .expect("failed to instantiate ethers provider"); + let provider = Box::new(ProviderBuilder::new().connect_http( + Url::parse(&format!("http://localhost:{}", http_port)).expect("failed to parse URL"), + )); Self { engine, datadir, diff --git a/testing/execution_engine_integration/src/genesis_json.rs b/testing/execution_engine_integration/src/genesis_json.rs index 991118478d..e5bf90d41f 100644 --- a/testing/execution_engine_integration/src/genesis_json.rs +++ b/testing/execution_engine_integration/src/genesis_json.rs @@ -1,4 +1,4 @@ -use serde_json::{json, Value}; +use serde_json::{Value, json}; /// Sourced from: /// diff --git a/testing/execution_engine_integration/src/geth.rs b/testing/execution_engine_integration/src/geth.rs index 8c39fda4e3..4b62e68e94 100644 --- a/testing/execution_engine_integration/src/geth.rs +++ b/testing/execution_engine_integration/src/geth.rs @@ -1,11 +1,11 @@ use crate::build_utils; use crate::execution_engine::GenericExecutionEngine; use crate::genesis_json::geth_genesis_json; +use network_utils::unused_port::unused_tcp4_port; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output}; use std::{env, fs}; use tempfile::TempDir; -use unused_port::unused_tcp4_port; const GETH_BRANCH: &str = "master"; const GETH_REPO_URL: &str = "https://github.com/ethereum/go-ethereum"; @@ -14,6 +14,10 @@ pub fn build_result(repo_dir: &Path) -> Output { Command::new("make") .arg("geth") .current_dir(repo_dir) + // Geth now uses the commit hash from a GitHub runner environment variable if it detects a CI environment. + // We need to override this to successfully build Geth in Lighthouse workflows. + // See: https://github.com/ethereum/go-ethereum/blob/668c3a7278af399c0e776e92f1c721b5158388f2/internal/build/env.go#L95-L121 + .env("CI", "false") .output() .expect("failed to make geth") } diff --git a/testing/execution_engine_integration/src/nethermind.rs b/testing/execution_engine_integration/src/nethermind.rs index c3b8651789..6a336161bd 100644 --- a/testing/execution_engine_integration/src/nethermind.rs +++ b/testing/execution_engine_integration/src/nethermind.rs @@ -1,12 +1,12 @@ use crate::build_utils; use crate::execution_engine::GenericExecutionEngine; use crate::genesis_json::nethermind_genesis_json; +use network_utils::unused_port::unused_tcp4_port; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Output}; use tempfile::TempDir; -use unused_port::unused_tcp4_port; /// We've pinned the Nethermind version since our method of using the `master` branch to /// find the latest tag isn't working. It appears Nethermind don't always tag on `master`. diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index b0d115960c..8413da4c5e 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -1,19 +1,22 @@ use crate::execution_engine::{ - ExecutionEngine, GenericExecutionEngine, ACCOUNT1, ACCOUNT2, KEYSTORE_PASSWORD, PRIVATE_KEYS, + ACCOUNT1, ACCOUNT2, ExecutionEngine, GenericExecutionEngine, KEYSTORE_PASSWORD, PRIVATE_KEYS, }; use crate::transactions::transactions; -use ethers_middleware::SignerMiddleware; -use ethers_providers::Middleware; -use ethers_signers::LocalWallet; +use alloy_network::{EthereumWallet, TransactionBuilder}; +use alloy_primitives::Address as AlloyAddress; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_signer_local::PrivateKeySigner; +use bls::PublicKeyBytes; use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, PayloadParameters, PayloadStatus, }; +use fixed_bytes::FixedBytesExtended; use fork_choice::ForkchoiceUpdateParameters; -use reqwest::{header::CONTENT_TYPE, Client}; +use reqwest::{Client, header::CONTENT_TYPE}; use sensitive_url::SensitiveUrl; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; @@ -21,8 +24,9 @@ use tokio::time::sleep; use types::payload::BlockProductionVersion; use types::{ Address, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, - FixedBytesExtended, ForkName, Hash256, MainnetEthSpec, PublicKeyBytes, Slot, Uint256, + ForkName, Hash256, MainnetEthSpec, Slot, Uint256, }; + const EXECUTION_ENGINE_START_TIMEOUT: Duration = Duration::from_secs(60); const TEST_FORK: ForkName = ForkName::Capella; @@ -64,7 +68,7 @@ async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: let client = Client::builder().build().unwrap(); let request = client - .post(http_url.full.clone()) + .post(http_url.expose_full().clone()) .header(CONTENT_TYPE, "application/json") .json(&body); @@ -90,7 +94,7 @@ async fn import_and_unlock(http_url: SensitiveUrl, priv_keys: &[&str], password: ); let request = client - .post(http_url.full.clone()) + .post(http_url.expose_full().clone()) .header(CONTENT_TYPE, "application/json") .json(&body); @@ -202,12 +206,13 @@ impl TestRig { self.wait_until_synced().await; // Create a local signer in case we need to sign transactions locally - let wallet1: LocalWallet = PRIVATE_KEYS[0].parse().expect("Invalid private key"); - let signer = SignerMiddleware::new(&self.ee_a.execution_engine.provider, wallet1); + let private_key_signer: PrivateKeySigner = + PRIVATE_KEYS[0].parse().expect("Invalid private key"); + let wallet = EthereumWallet::from(private_key_signer); // We hardcode the accounts here since some EEs start with a default unlocked account - let account1 = ethers_core::types::Address::from_slice(&hex::decode(ACCOUNT1).unwrap()); - let account2 = ethers_core::types::Address::from_slice(&hex::decode(ACCOUNT2).unwrap()); + let account1 = AlloyAddress::from_slice(&hex::decode(ACCOUNT1).unwrap()); + let account2 = AlloyAddress::from_slice(&hex::decode(ACCOUNT2).unwrap()); /* * Read the terminal block hash from both pairs, check it's equal. @@ -237,11 +242,18 @@ impl TestRig { if self.use_local_signing { // Sign locally with the Signer middleware - for (i, tx) in txs.clone().into_iter().enumerate() { + for (i, mut tx) in txs.clone().into_iter().enumerate() { // The local signer uses eth_sendRawTransaction, so we need to manually set the nonce - let mut tx = tx.clone(); - tx.set_nonce(i as u64); - let pending_tx = signer.send_transaction(tx, None).await.unwrap(); + tx = tx.with_nonce(i as u64); + let wallet_provider = ProviderBuilder::new().wallet(wallet.clone()).connect_http( + self.ee_a + .execution_engine + .http_url() + .to_string() + .parse() + .unwrap(), + ); + let pending_tx = wallet_provider.send_transaction(tx).await.unwrap(); pending_txs.push(pending_tx); } } else { @@ -261,7 +273,7 @@ impl TestRig { .ee_a .execution_engine .provider - .send_transaction(tx, None) + .send_transaction(tx) .await .unwrap(); pending_txs.push(pending_tx); @@ -446,11 +458,10 @@ impl TestRig { // Verify that all submitted txs were successful for pending_tx in pending_txs { - let tx_receipt = pending_tx.await.unwrap().unwrap(); - assert_eq!( - tx_receipt.status, - Some(1.into()), - "Tx index {} has invalid status ", + let tx_receipt = pending_tx.get_receipt().await.unwrap(); + assert!( + tx_receipt.status(), + "Tx index {:?} has invalid status ", tx_receipt.transaction_index ); } diff --git a/testing/execution_engine_integration/src/transactions.rs b/testing/execution_engine_integration/src/transactions.rs index fd458ad205..8cd63ce307 100644 --- a/testing/execution_engine_integration/src/transactions.rs +++ b/testing/execution_engine_integration/src/transactions.rs @@ -1,9 +1,10 @@ -use deposit_contract::{encode_eth1_tx_data, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS}; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip2930::AccessList}, - Address, Bytes, Eip1559TransactionRequest, TransactionRequest, U256, -}; -use types::{DepositData, EthSpec, FixedBytesExtended, Hash256, Keypair, Signature}; +use alloy_network::TransactionBuilder; +use alloy_primitives::{Address, U256}; +use alloy_rpc_types_eth::{AccessList, TransactionRequest}; +use bls::{Keypair, Signature}; +use deposit_contract::{BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS, encode_eth1_tx_data}; +use fixed_bytes::FixedBytesExtended; +use types::{DepositData, EthSpec, Hash256}; /// Hardcoded deposit contract address based on sender address and nonce pub const DEPOSIT_CONTRACT_ADDRESS: &str = "64f43BEc7F86526686C931d65362bB8698872F90"; @@ -21,7 +22,7 @@ pub enum Transaction { } /// Get a list of transactions to publish to the execution layer. -pub fn transactions(account1: Address, account2: Address) -> Vec { +pub fn transactions(account1: Address, account2: Address) -> Vec { vec![ Transaction::Transfer(account1, account2).transaction::(), Transaction::TransferLegacy(account1, account2).transaction::(), @@ -29,7 +30,7 @@ pub fn transactions(account1: Address, account2: Address) -> Vec(), Transaction::DepositDepositContract { sender: account1, - deposit_contract_address: ethers_core::types::Address::from_slice( + deposit_contract_address: Address::from_slice( &hex::decode(DEPOSIT_CONTRACT_ADDRESS).unwrap(), ), } @@ -38,33 +39,36 @@ pub fn transactions(account1: Address, account2: Address) -> Vec(&self) -> TypedTransaction { + pub fn transaction(&self) -> TransactionRequest { match &self { - Self::TransferLegacy(from, to) => TransactionRequest::new() + Self::TransferLegacy(from, to) => TransactionRequest::default() .from(*from) .to(*to) - .value(1) - .into(), - Self::Transfer(from, to) => Eip1559TransactionRequest::new() + .value(U256::from(1)) + .with_gas_price(1_000_000_000u128), // 1 gwei + Self::Transfer(from, to) => TransactionRequest::default() .from(*from) .to(*to) - .value(1) - .into(), - Self::TransferAccessList(from, to) => TransactionRequest::new() + .value(U256::from(1)) + .with_max_fee_per_gas(2_000_000_000u128) + .with_max_priority_fee_per_gas(1_000_000_000u128), + Self::TransferAccessList(from, to) => TransactionRequest::default() .from(*from) .to(*to) - .value(1) + .value(U256::from(1)) .with_access_list(AccessList::default()) - .into(), + .with_gas_price(1_000_000_000u128), // 1 gwei Self::DeployDepositContract(addr) => { let mut bytecode = String::from_utf8(BYTECODE.to_vec()).unwrap(); bytecode.retain(|c| c.is_ascii_hexdigit()); let bytecode = hex::decode(&bytecode[1..]).unwrap(); - TransactionRequest::new() + let mut req = TransactionRequest::default() .from(*addr) - .data(Bytes::from(bytecode)) - .gas(CONTRACT_DEPLOY_GAS) - .into() + .with_input(bytecode) + .with_gas_limit(CONTRACT_DEPLOY_GAS.try_into().unwrap()) + .with_gas_price(1_000_000_000u128); // 1 gwei + req.set_create(); + req } Self::DepositDepositContract { sender, @@ -80,13 +84,13 @@ impl Transaction { signature: Signature::empty().into(), }; deposit.signature = deposit.create_signature(&keypair.sk, &E::default_spec()); - TransactionRequest::new() + TransactionRequest::default() .from(*sender) .to(*deposit_contract_address) - .data(Bytes::from(encode_eth1_tx_data(&deposit).unwrap())) - .gas(DEPOSIT_GAS) - .value(U256::from(amount) * U256::exp10(9)) - .into() + .with_input(encode_eth1_tx_data(&deposit).unwrap()) + .with_gas_limit(DEPOSIT_GAS.try_into().unwrap()) + .value(U256::from(amount) * U256::from(10).pow(U256::from(9))) + .with_gas_price(1_000_000_000u128) // 1 gwei } } } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 4021a6d2c5..e49d11ee1e 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -4,7 +4,7 @@ use beacon_node::ProductionBeaconNode; use environment::RuntimeContext; -use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, Timeouts}; +use eth2::{BeaconNodeHttpClient, Timeouts, reqwest::ClientBuilder}; use sensitive_url::SensitiveUrl; use std::path::PathBuf; use std::time::Duration; @@ -248,14 +248,8 @@ impl LocalExecutionNode { if let Err(e) = std::fs::write(jwt_file_path, config.jwt_key.hex_string()) { panic!("Failed to write jwt file {}", e); } - let spec = context.eth2_config.spec.clone(); Self { - server: MockServer::new_with_config( - &context.executor.handle().unwrap(), - config, - spec, - None, - ), + server: MockServer::new_with_config(&context.executor.handle().unwrap(), config, None), datadir, } } diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index cf0d03c24f..a1b1b6f95d 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -7,9 +7,7 @@ edition = { workspace = true } [dependencies] clap = { workspace = true } -env_logger = { workspace = true } environment = { workspace = true } -eth2_network_config = { workspace = true } execution_layer = { workspace = true } futures = { workspace = true } kzg = { workspace = true } @@ -17,9 +15,10 @@ logging = { workspace = true } node_test_rig = { path = "../node_test_rig" } parking_lot = { workspace = true } rayon = { workspace = true } -sensitive_url = { path = "../../common/sensitive_url" } +sensitive_url = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +typenum = { workspace = true } types = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 1c27ca7792..23ec70ae5d 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -1,13 +1,14 @@ use crate::local_network::LocalNetworkParams; use crate::local_network::TERMINAL_BLOCK; -use crate::{checks, LocalNetwork}; +use crate::{LocalNetwork, checks}; use clap::ArgMatches; use crate::retry::with_retry; use futures::prelude::*; use node_test_rig::{ + ApiTopic, ValidatorFiles, environment::{EnvironmentBuilder, LoggerConfig}, - testing_validator_config, ApiTopic, ValidatorFiles, + testing_validator_config, }; use rayon::prelude::*; use std::cmp::max; @@ -17,7 +18,7 @@ use std::time::Duration; use environment::tracing_common; use tracing_subscriber::prelude::*; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use logging::build_workspace_filter; use tokio::time::sleep; @@ -25,12 +26,14 @@ use tracing::Level; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 32; +const GENESIS_DELAY: u64 = 38; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 0; const DENEB_FORK_EPOCH: u64 = 0; const ELECTRA_FORK_EPOCH: u64 = 2; +// const FULU_FORK_EPOCH: u64 = 3; +// const GLOAS_FORK_EPOCH: u64 = 4; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; diff --git a/testing/simulator/src/checks.rs b/testing/simulator/src/checks.rs index 1b2d4024d1..35200692c3 100644 --- a/testing/simulator/src/checks.rs +++ b/testing/simulator/src/checks.rs @@ -1,7 +1,8 @@ use crate::local_network::LocalNetwork; use node_test_rig::eth2::types::{BlockId, FinalityCheckpointsData, StateId}; use std::time::Duration; -use types::{Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Slot, Unsigned}; +use typenum::Unsigned; +use types::{Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Slot}; /// Checks that all of the validators have on-boarded by the start of the second eth1 voting /// period. @@ -303,7 +304,7 @@ pub(crate) async fn verify_light_client_updates( } // Verify light client optimistic update. `signature_slot_distance` should be 1 in the ideal scenario. - let signature_slot = *client + let signature_slot = client .get_beacon_light_client_optimistic_update::() .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? @@ -312,7 +313,9 @@ pub(crate) async fn verify_light_client_updates( .signature_slot(); let signature_slot_distance = slot - signature_slot; if signature_slot_distance > light_client_update_slot_tolerance { - return Err(format!("Existing optimistic update too old: signature slot {signature_slot}, current slot {slot:?}")); + return Err(format!( + "Existing optimistic update too old: signature slot {signature_slot}, current slot {slot:?}" + )); } // Verify light client finality update. `signature_slot_distance` should be 1 in the ideal scenario. @@ -332,7 +335,7 @@ pub(crate) async fn verify_light_client_updates( } continue; } - let signature_slot = *client + let signature_slot = client .get_beacon_light_client_finality_update::() .await .map_err(|e| format!("Error while getting light client updates: {:?}", e))? @@ -422,7 +425,7 @@ pub async fn verify_full_blob_production_up_to( // the `verify_full_block_production_up_to` function. if block.is_some() { remote_node - .get_blobs::(BlockId::Slot(Slot::new(slot)), None, &E::default_spec()) + .get_blobs::(BlockId::Slot(Slot::new(slot)), None) .await .map_err(|e| format!("Failed to get blobs at slot {slot:?}: {e:?}"))? .ok_or_else(|| format!("No blobs available at slot {slot:?}"))?; diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 707baf04a7..70c4680e92 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{crate_version, Arg, ArgAction, Command}; +use clap::{Arg, ArgAction, Command, crate_version}; pub fn cli_app() -> Command { Command::new("simulator") @@ -20,7 +20,7 @@ pub fn cli_app() -> Command { .short('n') .long("nodes") .action(ArgAction::Set) - .default_value("3") + .default_value("2") .help("Number of beacon nodes"), ) .arg( @@ -28,7 +28,7 @@ pub fn cli_app() -> Command { .short('p') .long("proposer-nodes") .action(ArgAction::Set) - .default_value("3") + .default_value("1") .help("Number of proposer-only beacon nodes"), ) .arg( diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 2d0cacd941..6e0db52d75 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -1,5 +1,5 @@ use crate::local_network::LocalNetworkParams; -use crate::{checks, LocalNetwork}; +use crate::{LocalNetwork, checks}; use clap::ArgMatches; use crate::retry::with_retry; @@ -7,8 +7,9 @@ use environment::tracing_common; use futures::prelude::*; use logging::build_workspace_filter; use node_test_rig::{ + ValidatorFiles, environment::{EnvironmentBuilder, LoggerConfig}, - testing_validator_config, ValidatorFiles, + testing_validator_config, }; use rayon::prelude::*; use std::cmp::max; @@ -18,16 +19,17 @@ use std::time::Duration; use tokio::time::sleep; use tracing::Level; use tracing_subscriber::prelude::*; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 32; +const GENESIS_DELAY: u64 = 38; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; const DENEB_FORK_EPOCH: u64 = 2; // const ELECTRA_FORK_EPOCH: u64 = 3; // const FULU_FORK_EPOCH: u64 = 4; +// const GLOAS_FORK_EPOCH: u64 = 5; // Since simulator tests are non-deterministic and there is a non-zero chance of missed // attestations, define an acceptable network-wide attestation performance. diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 3914d33f93..bd22a21511 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,10 +1,11 @@ use crate::checks::epoch_delay; use kzg::trusted_setup::get_trusted_setup; use node_test_rig::{ + ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, + MockExecutionConfig, MockServerConfig, ValidatorConfig, ValidatorFiles, environment::RuntimeContext, - eth2::{types::StateId, BeaconNodeHttpClient}, - testing_client_config, ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, - LocalValidatorClient, MockExecutionConfig, MockServerConfig, ValidatorConfig, ValidatorFiles, + eth2::{BeaconNodeHttpClient, types::StateId}, + testing_client_config, }; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; @@ -45,8 +46,7 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; - beacon_config.trusted_setup = serde_json::from_reader(get_trusted_setup().as_slice()) - .expect("Trusted setup bytes should be valid"); + beacon_config.trusted_setup = get_trusted_setup(); let el_config = execution_layer::Config { execution_endpoint: Some( diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index 1cc4a1779b..7bd6e546f7 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -18,16 +18,12 @@ mod local_network; mod retry; use cli::cli_app; -use env_logger::{Builder, Env}; use local_network::LocalNetwork; use types::MinimalEthSpec; pub type E = MinimalEthSpec; fn main() { - // Debugging output for libp2p and external crates. - Builder::from_env(Env::default()).init(); - let matches = cli_app().get_matches(); match matches.subcommand_name() { Some("basic-sim") => match basic_sim::run_basic_sim(&matches) { diff --git a/testing/simulator/src/retry.rs b/testing/simulator/src/retry.rs index ad85b74236..dea132c9da 100644 --- a/testing/simulator/src/retry.rs +++ b/testing/simulator/src/retry.rs @@ -32,11 +32,7 @@ mod tests { use std::collections::VecDeque; async fn my_async_func(is_ok: bool) -> Result<(), ()> { - if is_ok { - Ok(()) - } else { - Err(()) - } + if is_ok { Ok(()) } else { Err(()) } } #[tokio::test] diff --git a/testing/state_transition_vectors/Cargo.toml b/testing/state_transition_vectors/Cargo.toml index 7c29715346..437aa539f4 100644 --- a/testing/state_transition_vectors/Cargo.toml +++ b/testing/state_transition_vectors/Cargo.toml @@ -3,14 +3,16 @@ name = "state_transition_vectors" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } + +[features] +portable = ["beacon_chain/portable"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] beacon_chain = { workspace = true } +bls = { workspace = true } ethereum_ssz = { workspace = true } +fixed_bytes = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } types = { workspace = true } - -[features] -portable = ["beacon_chain/portable"] diff --git a/testing/state_transition_vectors/Makefile b/testing/state_transition_vectors/Makefile index 437aa50b00..c90810ad39 100644 --- a/testing/state_transition_vectors/Makefile +++ b/testing/state_transition_vectors/Makefile @@ -5,4 +5,4 @@ test: cargo test --release --features "$(TEST_FEATURES)" clean: - rm -r vectors/ + rm -rf vectors/ diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index 61cae6dbe1..f8ece0218f 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -1,7 +1,7 @@ use super::*; use state_processing::{ - per_block_processing, per_block_processing::errors::ExitInvalid, BlockProcessingError, - BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + per_block_processing, per_block_processing::errors::ExitInvalid, }; use types::{BeaconBlock, Epoch}; diff --git a/testing/state_transition_vectors/src/main.rs b/testing/state_transition_vectors/src/main.rs index 7f0f697d61..80c30489b7 100644 --- a/testing/state_transition_vectors/src/main.rs +++ b/testing/state_transition_vectors/src/main.rs @@ -3,6 +3,8 @@ mod macros; mod exit; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use bls::Keypair; +use fixed_bytes::FixedBytesExtended; use ssz::Encode; use std::env; use std::fs::{self, File}; @@ -10,10 +12,8 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::exit; use std::sync::LazyLock; -use types::{ - test_utils::generate_deterministic_keypairs, BeaconState, EthSpec, Keypair, SignedBeaconBlock, -}; -use types::{FixedBytesExtended, Hash256, MainnetEthSpec, Slot}; +use types::{BeaconState, EthSpec, SignedBeaconBlock, test_utils::generate_deterministic_keypairs}; +use types::{Hash256, MainnetEthSpec, Slot}; type E = MainnetEthSpec; diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 7a90270913..ff1e772d54 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -41,7 +41,7 @@ impl MockBeaconNode { pub fn mock_config_spec(&mut self, spec: &ChainSpec) { let path_pattern = Regex::new(r"^/eth/v1/config/spec$").unwrap(); - let config_and_preset = ConfigAndPreset::from_chain_spec::(spec, None); + let config_and_preset = ConfigAndPreset::from_chain_spec::(spec); let data = GenericResponse::from(config_and_preset); self.server .mock("GET", Matcher::Regex(path_pattern.to_string())) diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index b4637b4030..3ef2e0f7f7 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -9,10 +9,12 @@ edition = { workspace = true } [dev-dependencies] account_utils = { workspace = true } async-channel = { workspace = true } +bls = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } eth2_keystore = { workspace = true } eth2_network_config = { workspace = true } +fixed_bytes = { workspace = true } futures = { workspace = true } initialized_validators = { workspace = true } lighthouse_validator_store = { workspace = true } @@ -24,6 +26,7 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } +ssz_types = { workspace = true } task_executor = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/testing/web3signer_tests/src/get_web3signer.rs b/testing/web3signer_tests/src/get_web3signer.rs index 8c46a07a7d..0c3d9b02db 100644 --- a/testing/web3signer_tests/src/get_web3signer.rs +++ b/testing/web3signer_tests/src/get_web3signer.rs @@ -28,7 +28,10 @@ pub async fn download_binary(dest_dir: PathBuf) { // Download the release zip. let client = Client::builder().build().unwrap(); - let zip_url = format!("https://artifacts.consensys.net/public/web3signer/raw/names/web3signer.zip/versions/{}/web3signer-{}.zip", version, version); + let zip_url = format!( + "https://artifacts.consensys.net/public/web3signer/raw/names/web3signer.zip/versions/{}/web3signer-{}.zip", + version, version + ); let zip_response = client .get(zip_url) .send() diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 4bc0f62346..541f9b2b4a 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -20,18 +20,21 @@ mod tests { use account_utils::validator_definitions::{ SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, }; + use bls::{AggregateSignature, Keypair, PublicKeyBytes, SecretKey, Signature}; use eth2::types::FullBlockContents; use eth2_keystore::KeystoreBuilder; use eth2_network_config::Eth2NetworkConfig; + use fixed_bytes::FixedBytesExtended; use initialized_validators::{ - load_pem_certificate, load_pkcs12_identity, InitializedValidators, + InitializedValidators, load_pem_certificate, load_pkcs12_identity, }; use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::Mutex; use reqwest::Client; use serde::Serialize; - use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; + use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use slot_clock::{SlotClock, TestingSlotClock}; + use ssz_types::BitList; use std::env; use std::fmt::Debug; use std::fs::{self, File}; @@ -41,7 +44,7 @@ mod tests { use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant}; use task_executor::TaskExecutor; - use tempfile::{tempdir, TempDir}; + use tempfile::{TempDir, tempdir}; use tokio::sync::OnceCell; use tokio::time::sleep; use types::{attestation::AttestationBase, *}; diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index a8c8fd59f1..6990a2f61a 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "validator_client" -version = "0.3.5" +version = { workspace = true } authors = ["Sigma Prime "] edition = { workspace = true } diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml index 3bcb0d7034..481aece48b 100644 --- a/validator_client/beacon_node_fallback/Cargo.toml +++ b/validator_client/beacon_node_fallback/Cargo.toml @@ -9,10 +9,12 @@ name = "beacon_node_fallback" path = "src/lib.rs" [dependencies] +bls = { workspace = true } clap = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } itertools = { workspace = true } +sensitive_url = { workspace = true } serde = { workspace = true } slot_clock = { workspace = true } strum = { workspace = true } diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index e19da31e9a..f2e499f789 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -4,13 +4,14 @@ pub mod beacon_node_health; use beacon_node_health::{ - check_node_health, BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, - IsOptimistic, SyncDistanceTier, + BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, IsOptimistic, + SyncDistanceTier, check_node_health, }; use clap::ValueEnum; -use eth2::BeaconNodeHttpClient; +use eth2::{BeaconNodeHttpClient, Timeouts}; use futures::future; -use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer}; +use sensitive_url::SensitiveUrl; +use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; use slot_clock::SlotClock; use std::cmp::Ordering; use std::fmt; @@ -19,12 +20,12 @@ use std::future::Future; use std::sync::Arc; use std::time::{Duration, Instant}; use std::vec::Vec; -use strum::EnumVariantNames; +use strum::VariantNames; use task_executor::TaskExecutor; use tokio::{sync::RwLock, time::sleep}; use tracing::{debug, error, warn}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; -use validator_metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS}; +use validator_metrics::{ENDPOINT_ERRORS, ENDPOINT_REQUESTS, inc_counter_vec}; /// Message emitted when the VC detects the BN is using a different spec. const UPDATE_REQUIRED_LOG_HINT: &str = "this VC or the remote BN may need updating"; @@ -358,6 +359,13 @@ impl CandidateBeaconNode { hint = UPDATE_REQUIRED_LOG_HINT, "Beacon node has mismatched Fulu fork epoch" ); + } else if beacon_node_spec.gloas_fork_epoch != spec.gloas_fork_epoch { + warn!( + endpoint = %self.beacon_node, + endpoint_gloas_fork_epoch = ?beacon_node_spec.gloas_fork_epoch, + hint = UPDATE_REQUIRED_LOG_HINT, + "Beacon node has mismatched Gloas fork epoch" + ); } Ok(()) @@ -455,6 +463,39 @@ impl BeaconNodeFallback { (candidate_info, num_available, num_synced) } + /// Update the list of candidates with a new list. + /// Returns `Ok(new_list)` if the update was successful. + /// Returns `Err(some_err)` if the list is empty. + pub async fn update_candidates_list( + &self, + new_list: Vec, + use_long_timeouts: bool, + ) -> Result, String> { + if new_list.is_empty() { + return Err("list cannot be empty".to_string()); + } + + let timeouts: Timeouts = if new_list.len() == 1 || use_long_timeouts { + Timeouts::set_all(Duration::from_secs(self.spec.seconds_per_slot)) + } else { + Timeouts::use_optimized_timeouts(Duration::from_secs(self.spec.seconds_per_slot)) + }; + + let new_candidates: Vec = new_list + .clone() + .into_iter() + .enumerate() + .map(|(index, url)| { + CandidateBeaconNode::new(BeaconNodeHttpClient::new(url, timeouts.clone()), index) + }) + .collect(); + + let mut candidates = self.candidates.write().await; + *candidates = new_candidates; + + Ok(new_list) + } + /// Loop through ALL candidates in `self.candidates` and update their sync status. /// /// It is possible for a node to return an unsynced status while continuing to serve @@ -615,7 +656,7 @@ impl BeaconNodeFallback { R: Future>, Err: Debug, { - inc_counter_vec(&ENDPOINT_REQUESTS, &[candidate.as_ref()]); + inc_counter_vec(&ENDPOINT_REQUESTS, &[candidate.server().redacted()]); // There exists a race condition where `func` may be called when the candidate is // actually not ready. We deem this an acceptable inefficiency. @@ -627,7 +668,7 @@ impl BeaconNodeFallback { error = ?e, "Request to beacon node failed" ); - inc_counter_vec(&ENDPOINT_ERRORS, &[candidate.as_ref()]); + inc_counter_vec(&ENDPOINT_ERRORS, &[candidate.server().redacted()]); Err((candidate.to_string(), Error::RequestFailed(e))) } } @@ -711,7 +752,7 @@ async fn sort_nodes_by_health(nodes: &mut Vec) { } /// Serves as a cue for `BeaconNodeFallback` to tell which requests need to be broadcasted. -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, EnumVariantNames, ValueEnum)] +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, VariantNames, ValueEnum)] #[strum(serialize_all = "kebab-case")] pub enum ApiTopic { None, @@ -739,12 +780,13 @@ impl ApiTopic { mod tests { use super::*; use crate::beacon_node_health::BeaconNodeHealthTier; + use bls::Signature; use eth2::SensitiveUrl; use eth2::Timeouts; use slot_clock::TestingSlotClock; use strum::VariantNames; use types::{BeaconBlockDeneb, MainnetEthSpec, Slot}; - use types::{EmptyBlock, Signature, SignedBeaconBlockDeneb, SignedBlindedBeaconBlock}; + use types::{EmptyBlock, SignedBeaconBlockDeneb, SignedBlindedBeaconBlock}; use validator_test_rig::mock_beacon_node::MockBeaconNode; type E = MainnetEthSpec; @@ -756,10 +798,12 @@ mod tests { let mut variants = ApiTopic::VARIANTS.to_vec(); variants.retain(|s| *s != "none"); assert_eq!(all.len(), variants.len()); - assert!(variants - .iter() - .map(|topic| ApiTopic::from_str(topic, true).unwrap()) - .eq(all.into_iter())); + assert!( + variants + .iter() + .map(|topic| ApiTopic::from_str(topic, true).unwrap()) + .eq(all.into_iter()) + ); } #[tokio::test] diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml index e5b183570d..66b27eb39d 100644 --- a/validator_client/doppelganger_service/Cargo.toml +++ b/validator_client/doppelganger_service/Cargo.toml @@ -6,6 +6,7 @@ authors = ["Sigma Prime "] [dependencies] beacon_node_fallback = { workspace = true } +bls = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } logging = { workspace = true } diff --git a/validator_client/doppelganger_service/src/lib.rs b/validator_client/doppelganger_service/src/lib.rs index e3c7ce78b4..600ae82c54 100644 --- a/validator_client/doppelganger_service/src/lib.rs +++ b/validator_client/doppelganger_service/src/lib.rs @@ -30,6 +30,7 @@ //! Doppelganger protection is a best-effort, last-line-of-defence mitigation. Do not rely upon it. use beacon_node_fallback::BeaconNodeFallback; +use bls::PublicKeyBytes; use environment::RuntimeContext; use eth2::types::LivenessResponseData; use logging::crit; @@ -41,7 +42,7 @@ use std::sync::Arc; use task_executor::ShutdownReason; use tokio::time::sleep; use tracing::{error, info}; -use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; +use types::{Epoch, EthSpec, Slot}; use validator_store::{DoppelgangerStatus, ValidatorStore}; struct LivenessResponses { @@ -261,8 +262,8 @@ impl DoppelgangerService { continue; } - if let Some(slot) = slot_clock.now() { - if let Err(e) = service + if let Some(slot) = slot_clock.now() + && let Err(e) = service .detect_doppelgangers::( slot, &get_index, @@ -270,12 +271,11 @@ impl DoppelgangerService { &mut shutdown_func, ) .await - { - error!( - error = ?e, - "Error during doppelganger detection" - ); - } + { + error!( + error = ?e, + "Error during doppelganger detection" + ); } } }, @@ -603,8 +603,8 @@ mod test { use std::future; use std::time::Duration; use types::{ - test_utils::{SeedableRng, TestRandom, XorShiftRng}, MainnetEthSpec, + test_utils::{SeedableRng, TestRandom, XorShiftRng}, }; use validator_store::DoppelgangerStatus; diff --git a/validator_client/graffiti_file/src/lib.rs b/validator_client/graffiti_file/src/lib.rs index 86f582aa38..8e40ef907d 100644 --- a/validator_client/graffiti_file/src/lib.rs +++ b/validator_client/graffiti_file/src/lib.rs @@ -2,11 +2,11 @@ use bls::PublicKeyBytes; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; -use std::io::{prelude::*, BufReader}; +use std::io::{BufReader, prelude::*}; use std::path::PathBuf; use std::str::FromStr; use tracing::warn; -use types::{graffiti::GraffitiString, Graffiti}; +use types::{Graffiti, graffiti::GraffitiString}; #[derive(Debug)] #[allow(clippy::enum_variant_names)] @@ -154,7 +154,7 @@ mod tests { let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap(); let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap(); - let file_name = temp.into_path().join("graffiti.txt"); + let file_name = temp.keep().join("graffiti.txt"); let file = File::create(&file_name).unwrap(); let mut graffiti_file = LineWriter::new(file); diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 588aa2ca93..2bd57867ac 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -16,10 +16,11 @@ deposit_contract = { workspace = true } directory = { workspace = true } dirs = { workspace = true } doppelganger_service = { workspace = true } -eth2 = { workspace = true } +eth2 = { workspace = true, features = ["lighthouse"] } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } +fixed_bytes = { workspace = true } graffiti_file = { workspace = true } health_metrics = { workspace = true } initialized_validators = { workspace = true } @@ -41,6 +42,7 @@ tempfile = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } +typenum = { workspace = true } types = { workspace = true } url = { workspace = true } validator_dir = { workspace = true } @@ -54,3 +56,4 @@ zeroize = { workspace = true } futures = { workspace = true } itertools = { workspace = true } rand = { workspace = true, features = ["small_rng"] } +ssz_types = { workspace = true } diff --git a/validator_client/http_api/src/api_secret.rs b/validator_client/http_api/src/api_secret.rs index bac54dc8b2..2241d791bc 100644 --- a/validator_client/http_api/src/api_secret.rs +++ b/validator_client/http_api/src/api_secret.rs @@ -1,6 +1,6 @@ use filesystem::create_with_600_perms; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; +use rand::distr::Alphanumeric; +use rand::{Rng, rng}; use std::fs; use std::path::{Path, PathBuf}; use warp::Filter; @@ -58,7 +58,7 @@ impl ApiSecret { } let length = PK_LEN; - let pk: String = thread_rng() + let pk: String = rng() .sample_iter(&Alphanumeric) .take(length) .map(char::from) diff --git a/validator_client/http_api/src/create_validator.rs b/validator_client/http_api/src/create_validator.rs index 278274198d..e4aff34dbc 100644 --- a/validator_client/http_api/src/create_validator.rs +++ b/validator_client/http_api/src/create_validator.rs @@ -1,7 +1,7 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use account_utils::{ eth2_keystore::Keystore, - eth2_wallet::{bip39::Mnemonic, WalletBuilder}, + eth2_wallet::{WalletBuilder, bip39::Mnemonic}, random_mnemonic, random_password, }; use eth2::lighthouse_vc::types::{self as api_types}; @@ -9,7 +9,7 @@ use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::path::{Path, PathBuf}; use types::{ChainSpec, EthSpec}; -use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; +use validator_dir::{Builder as ValidatorDirBuilder, keystore_password_path}; use zeroize::Zeroizing; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in diff --git a/validator_client/http_api/src/graffiti.rs b/validator_client/http_api/src/graffiti.rs index 4372b14b04..3cc898435d 100644 --- a/validator_client/http_api/src/graffiti.rs +++ b/validator_client/http_api/src/graffiti.rs @@ -2,7 +2,7 @@ use bls::PublicKey; use lighthouse_validator_store::LighthouseValidatorStore; use slot_clock::SlotClock; use std::sync::Arc; -use types::{graffiti::GraffitiString, EthSpec, Graffiti}; +use types::{EthSpec, Graffiti, graffiti::GraffitiString}; pub fn get_graffiti( validator_pubkey: PublicKey, diff --git a/validator_client/http_api/src/keystores.rs b/validator_client/http_api/src/keystores.rs index 302b21d7d8..18accf0d5a 100644 --- a/validator_client/http_api/src/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -1,5 +1,6 @@ //! Implementation of the standard keystore management API. use account_utils::validator_definitions::PasswordStorage; +use bls::PublicKeyBytes; use eth2::lighthouse_vc::{ std_types::{ DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, @@ -18,8 +19,8 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; use tracing::{info, warn}; -use types::{EthSpec, PublicKeyBytes}; -use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; +use types::EthSpec; +use validator_dir::{Builder as ValidatorDirBuilder, keystore_password_path}; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; use zeroize::Zeroizing; @@ -79,7 +80,7 @@ pub fn import( let slashing_protection_status = if let Some(InterchangeJsonStr(slashing_protection)) = request.slashing_protection { // Warn for missing slashing protection. - for KeystoreJsonStr(ref keystore) in &request.keystores { + for KeystoreJsonStr(keystore) in &request.keystores { if let Some(public_key) = keystore.public_key() { let pubkey_bytes = public_key.compress(); if !slashing_protection diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index aebe179567..a35b4ec6c6 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -12,7 +12,7 @@ pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; use create_signed_voluntary_exit::create_signed_voluntary_exit; -use graffiti_file::{determine_graffiti, GraffitiFile}; +use graffiti_file::{GraffitiFile, determine_graffiti}; use lighthouse_validator_store::LighthouseValidatorStore; use validator_store::ValidatorStore; @@ -22,6 +22,8 @@ use account_utils::{ }; pub use api_secret::ApiSecret; use beacon_node_fallback::CandidateInfo; +use bls::{PublicKey, PublicKeyBytes}; +use core::convert::Infallible; use create_validator::{ create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, }; @@ -29,15 +31,16 @@ use directory::{DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_VALIDATOR_D use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{ - self as api_types, GenericResponse, GetGraffitiResponse, Graffiti, PublicKey, - PublicKeyBytes, SetGraffitiRequest, + self as api_types, GenericResponse, GetGraffitiResponse, Graffiti, SetGraffitiRequest, + UpdateCandidatesRequest, UpdateCandidatesResponse, }, }; use health_metrics::observe::Observe; use lighthouse_version::version_with_platform; -use logging::crit; use logging::SSELoggingComponents; +use logging::crit; use parking_lot::RwLock; +use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use std::collections::HashMap; @@ -48,12 +51,13 @@ use std::sync::Arc; use sysinfo::{System, SystemExt}; use system_health::observe_system_health_vc; use task_executor::TaskExecutor; -use tokio_stream::{wrappers::BroadcastStream, StreamExt}; +use tokio_stream::{StreamExt, wrappers::BroadcastStream}; use tracing::{info, warn}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; use validator_services::block_service::BlockService; -use warp::{sse::Event, Filter}; +use warp::{Filter, reply::Response, sse::Event}; +use warp_utils::reject::convert_rejection; use warp_utils::task::blocking_json_task; #[derive(Debug)] @@ -102,6 +106,7 @@ pub struct Config { pub allow_keystore_export: bool, pub store_passwords_in_secrets_dir: bool, pub http_token_path: PathBuf, + pub bn_long_timeouts: bool, } impl Default for Config { @@ -121,6 +126,7 @@ impl Default for Config { allow_keystore_export: false, store_passwords_in_secrets_dir: false, http_token_path, + bn_long_timeouts: false, } } } @@ -147,6 +153,7 @@ pub fn serve( let config = &ctx.config; let allow_keystore_export = config.allow_keystore_export; let store_passwords_in_secrets_dir = config.store_passwords_in_secrets_dir; + let use_long_timeouts = config.bn_long_timeouts; // Configure CORS. let cors_builder = { @@ -309,7 +316,7 @@ pub fn serve( .and(spec_filter.clone()) .then(|spec: Arc<_>| { blocking_json_task(move || { - let config = ConfigAndPreset::from_chain_spec::(&spec, None); + let config = ConfigAndPreset::from_chain_spec::(&spec); Ok(api_types::GenericResponse::from(config)) }) }); @@ -839,6 +846,59 @@ pub fn serve( }) }); + // POST /lighthouse/beacon/update + let post_lighthouse_beacon_update = warp::path("lighthouse") + .and(warp::path("beacon")) + .and(warp::path("update")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(block_service_filter.clone()) + .then( + move |request: UpdateCandidatesRequest, + block_service: BlockService, T>| async move { + async fn parse_urls(urls: &[String]) -> Result, Response> { + match urls + .iter() + .map(|url| SensitiveUrl::parse(url).map_err(|e| e.to_string())) + .collect() + { + Ok(sensitive_urls) => Ok(sensitive_urls), + Err(_) => Err(convert_rejection::(Err( + warp_utils::reject::custom_bad_request( + "one or more urls could not be parsed".to_string(), + ), + )) + .await), + } + } + + let beacons: Vec = match parse_urls(&request.beacon_nodes).await { + Ok(new_beacons) => { + match block_service + .beacon_nodes + .update_candidates_list(new_beacons, use_long_timeouts) + .await + { + Ok(beacons) => beacons, + Err(e) => { + return convert_rejection::(Err( + warp_utils::reject::custom_bad_request(e.to_string()), + )) + .await; + } + } + } + Err(e) => return e, + }; + + let response: UpdateCandidatesResponse = UpdateCandidatesResponse { + new_beacon_nodes_list: beacons.iter().map(|surl| surl.to_string()).collect(), + }; + + blocking_json_task(move || Ok(api_types::GenericResponse::from(response))).await + }, + ); + // Standard key-manager endpoints. let eth_v1 = warp::path("eth").and(warp::path("v1")); let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); @@ -1316,6 +1376,7 @@ pub fn serve( .or(post_std_keystores) .or(post_std_remotekeys) .or(post_graffiti) + .or(post_lighthouse_beacon_update) .recover(warp_utils::reject::handle_rejection), )) .or(warp::patch() diff --git a/validator_client/http_api/src/remotekeys.rs b/validator_client/http_api/src/remotekeys.rs index 5aa63baac3..987e1b8740 100644 --- a/validator_client/http_api/src/remotekeys.rs +++ b/validator_client/http_api/src/remotekeys.rs @@ -2,6 +2,7 @@ use account_utils::validator_definitions::{ SigningDefinition, ValidatorDefinition, Web3SignerDefinition, }; +use bls::PublicKeyBytes; use eth2::lighthouse_vc::std_types::{ DeleteRemotekeyStatus, DeleteRemotekeysRequest, DeleteRemotekeysResponse, ImportRemotekeyStatus, ImportRemotekeysRequest, ImportRemotekeysResponse, @@ -14,7 +15,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; use tracing::{info, warn}; -use types::{EthSpec, PublicKeyBytes}; +use types::EthSpec; use url::Url; use warp::Rejection; use warp_utils::reject::custom_server_error; diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 08447a82ce..f83d9f4d52 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -4,28 +4,30 @@ use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, }; +use bls::Keypair; use deposit_contract::decode_eth1_tx_data; use doppelganger_service::DoppelgangerService; use eth2::{ + Error as ApiError, lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, types::ErrorMessage as ApiErrorMessage, - Error as ApiError, }; use eth2_keystore::KeystoreBuilder; -use initialized_validators::key_cache::{KeyCache, CACHE_FILENAME}; +use initialized_validators::key_cache::{CACHE_FILENAME, KeyCache}; use initialized_validators::{InitializedValidators, OnDecryptFailure}; use lighthouse_validator_store::{Config as ValidatorStoreConfig, LighthouseValidatorStore}; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use std::time::Duration; use task_executor::test_utils::TestRuntime; -use tempfile::{tempdir, TempDir}; +use tempfile::{TempDir, tempdir}; use tokio::sync::oneshot; +use types::ChainSpec; use validator_services::block_service::BlockService; use zeroize::Zeroizing; @@ -61,6 +63,7 @@ pub struct ApiTester { pub _server_shutdown: oneshot::Sender<()>, pub validator_dir: TempDir, pub secrets_dir: TempDir, + pub spec: Arc, } impl ApiTester { @@ -69,6 +72,19 @@ impl ApiTester { } pub async fn new_with_http_config(http_config: HttpConfig) -> Self { + let slot_clock = + TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + let genesis_validators_root = Hash256::repeat_byte(42); + let spec = Arc::new(E::default_spec()); + Self::new_with_options(http_config, slot_clock, genesis_validators_root, spec).await + } + + pub async fn new_with_options( + http_config: HttpConfig, + slot_clock: TestingSlotClock, + genesis_validators_root: Hash256, + spec: Arc, + ) -> Self { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); let token_path = tempdir().unwrap().path().join(PK_FILENAME); @@ -91,20 +107,15 @@ impl ApiTester { ..Default::default() }; - let spec = Arc::new(E::default_spec()); - let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); - let slot_clock = - TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let test_runtime = TestRuntime::default(); let validator_store = Arc::new(LighthouseValidatorStore::new( initialized_validators, slashing_protection, - Hash256::repeat_byte(42), + genesis_validators_root, spec.clone(), Some(Arc::new(DoppelgangerService::default())), slot_clock.clone(), @@ -127,7 +138,7 @@ impl ApiTester { validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), - spec, + spec: spec.clone(), config: http_config, sse_logging_components: None, slot_clock, @@ -161,6 +172,7 @@ impl ApiTester { _server_shutdown: shutdown_tx, validator_dir, secrets_dir, + spec, } } @@ -173,6 +185,7 @@ impl ApiTester { allow_keystore_export: true, store_passwords_in_secrets_dir: false, http_token_path: tempdir().unwrap().path().join(PK_FILENAME), + bn_long_timeouts: false, } } @@ -244,11 +257,11 @@ impl ApiTester { pub async fn test_get_lighthouse_spec(self) -> Self { let result = self .client - .get_lighthouse_spec::() + .get_lighthouse_spec::() .await - .map(|res| ConfigAndPreset::Fulu(res.data)) + .map(|res| ConfigAndPreset::Gloas(res.data)) .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec()); assert_eq!(result, expected); @@ -358,9 +371,11 @@ impl ApiTester { // Ensure the server lists all of these newly created validators. for validator in &response { - assert!(server_vals - .iter() - .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey)); + assert!( + server_vals + .iter() + .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey) + ); } /* @@ -557,16 +572,17 @@ impl ApiTester { enabled ); - assert!(self - .client - .get_lighthouse_validators() - .await - .unwrap() - .data - .into_iter() - .find(|v| v.voting_pubkey == validator.voting_pubkey) - .map(|v| v.enabled == enabled) - .unwrap()); + assert!( + self.client + .get_lighthouse_validators() + .await + .unwrap() + .data + .into_iter() + .find(|v| v.voting_pubkey == validator.voting_pubkey) + .map(|v| v.enabled == enabled) + .unwrap() + ); // Check the server via an individual request. assert_eq!( diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 4b1a3c0059..5cb631983c 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -11,17 +11,18 @@ use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, random_password_string, validator_definitions::ValidatorDefinitions, }; +use bls::{Keypair, PublicKeyBytes}; use deposit_contract::decode_eth1_tx_data; use eth2::{ + Error as ApiError, lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, types::ErrorMessage as ApiErrorMessage, - Error as ApiError, }; use eth2_keystore::KeystoreBuilder; use lighthouse_validator_store::{Config as ValidatorStoreConfig, LighthouseValidatorStore}; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::net::{IpAddr, Ipv4Addr}; @@ -29,7 +30,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use task_executor::test_utils::TestRuntime; -use tempfile::{tempdir, TempDir}; +use tempfile::{TempDir, tempdir}; use types::graffiti::GraffitiString; use validator_store::ValidatorStore; use zeroize::Zeroizing; @@ -45,6 +46,7 @@ struct ApiTester { validator_store: Arc>, url: SensitiveUrl, slot_clock: TestingSlotClock, + spec: Arc, _validator_dir: TempDir, _secrets_dir: TempDir, _test_runtime: TestRuntime, @@ -117,7 +119,7 @@ impl ApiTester { validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), - spec: E::default_spec().into(), + spec: spec.clone(), config: HttpConfig { enabled: true, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -126,6 +128,7 @@ impl ApiTester { allow_keystore_export: true, store_passwords_in_secrets_dir: false, http_token_path: token_path, + bn_long_timeouts: false, }, sse_logging_components: None, slot_clock: slot_clock.clone(), @@ -151,6 +154,7 @@ impl ApiTester { validator_store, url, slot_clock, + spec, _validator_dir: validator_dir, _secrets_dir: secrets_dir, _test_runtime: test_runtime, @@ -205,13 +209,24 @@ impl ApiTester { } pub async fn test_get_lighthouse_spec(self) -> Self { - let result = self - .client - .get_lighthouse_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let result = if self.spec.is_gloas_scheduled() { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Gloas(res.data)) + } else if self.spec.is_fulu_scheduled() { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&self.spec); assert_eq!(result, expected); @@ -321,9 +336,11 @@ impl ApiTester { // Ensure the server lists all of these newly created validators. for validator in &response { - assert!(server_vals - .iter() - .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey)); + assert!( + server_vals + .iter() + .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey) + ); } /* @@ -547,16 +564,17 @@ impl ApiTester { enabled ); - assert!(self - .client - .get_lighthouse_validators() - .await - .unwrap() - .data - .into_iter() - .find(|v| v.voting_pubkey == validator.voting_pubkey) - .map(|v| v.enabled == enabled) - .unwrap()); + assert!( + self.client + .get_lighthouse_validators() + .await + .unwrap() + .data + .into_iter() + .find(|v| v.voting_pubkey == validator.voting_pubkey) + .map(|v| v.enabled == enabled) + .unwrap() + ); // Check the server via an individual request. assert_eq!( diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index 37f7513f37..eeb3cd94de 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -1,19 +1,24 @@ use super::*; use account_utils::random_password_string; use bls::PublicKeyBytes; +use bls::{AggregateSignature, PublicKey}; use eth2::lighthouse_vc::types::UpdateFeeRecipientRequest; use eth2::lighthouse_vc::{ http_client::ValidatorClientHttpClient as HttpClient, std_types::{KeystoreJsonStr as Keystore, *}, types::Web3SignerValidatorRequest, }; +use fixed_bytes::FixedBytesExtended; use itertools::Itertools; use lighthouse_validator_store::DEFAULT_GAS_LIMIT; -use rand::{rngs::SmallRng, Rng, SeedableRng}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; use slashing_protection::interchange::{Interchange, InterchangeMetadata}; +use ssz_types::BitList; use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; -use types::{attestation::AttestationBase, Address}; +use typenum::Unsigned; +use types::{Address, attestation::AttestationBase}; use validator_store::ValidatorStore; use zeroize::Zeroizing; @@ -1124,11 +1129,14 @@ async fn generic_migration_test( delete_indices.len() ); for &i in &delete_indices { - assert!(delete_res - .slashing_protection - .data - .iter() - .any(|interchange_data| interchange_data.pubkey == keystore_pubkey(&keystores[i]))); + assert!( + delete_res + .slashing_protection + .data + .iter() + .any(|interchange_data| interchange_data.pubkey + == keystore_pubkey(&keystores[i])) + ); } // Filter slashing protection according to `slashing_protection_indices`. @@ -1324,13 +1332,13 @@ async fn delete_concurrent_with_signing() { let all_pubkeys = all_pubkeys.clone(); let handle = handle.spawn(async move { - let mut rng = SmallRng::from_entropy(); + let mut rng: StdRng = SeedableRng::from_os_rng(); let mut slashing_protection = vec![]; for _ in 0..num_delete_attempts { let to_delete = all_pubkeys .iter() - .filter(|_| rng.gen_bool(delete_prob)) + .filter(|_| rng.random_bool(delete_prob)) .copied() .collect::>(); @@ -2087,7 +2095,7 @@ async fn import_remotekey_web3signer_disabled() { // Import web3signers. tester .client - .post_lighthouse_validators_web3signer(&vec![web3signer_req]) + .post_lighthouse_validators_web3signer(&[web3signer_req]) .await .unwrap(); @@ -2142,7 +2150,7 @@ async fn import_remotekey_web3signer_enabled() { // Import web3signers. tester .client - .post_lighthouse_validators_web3signer(&vec![web3signer_req.clone()]) + .post_lighthouse_validators_web3signer(&[web3signer_req.clone()]) .await .unwrap(); diff --git a/validator_client/http_metrics/src/lib.rs b/validator_client/http_metrics/src/lib.rs index 7441939957..70b447a493 100644 --- a/validator_client/http_metrics/src/lib.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -16,7 +16,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tracing::info; use types::EthSpec; use validator_services::duties_service::DutiesService; -use warp::{http::Response, Filter}; +use warp::{Filter, http::Response}; #[derive(Debug)] pub enum Error { @@ -169,34 +169,34 @@ pub fn gather_prometheus_metrics( { let shared = ctx.shared.read(); - if let Some(genesis_time) = shared.genesis_time { - if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) { - let distance = now.as_secs() as i64 - genesis_time as i64; - set_gauge(&GENESIS_DISTANCE, distance); - } + if let Some(genesis_time) = shared.genesis_time + && let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) + { + let distance = now.as_secs() as i64 - genesis_time as i64; + set_gauge(&GENESIS_DISTANCE, distance); } - if let Some(duties_service) = &shared.duties_service { - if let Some(slot) = duties_service.slot_clock.now() { - let current_epoch = slot.epoch(E::slots_per_epoch()); - let next_epoch = current_epoch + 1; + if let Some(duties_service) = &shared.duties_service + && let Some(slot) = duties_service.slot_clock.now() + { + let current_epoch = slot.epoch(E::slots_per_epoch()); + let next_epoch = current_epoch + 1; - set_int_gauge( - &PROPOSER_COUNT, - &[CURRENT_EPOCH], - duties_service.proposer_count(current_epoch) as i64, - ); - set_int_gauge( - &ATTESTER_COUNT, - &[CURRENT_EPOCH], - duties_service.attester_count(current_epoch) as i64, - ); - set_int_gauge( - &ATTESTER_COUNT, - &[NEXT_EPOCH], - duties_service.attester_count(next_epoch) as i64, - ); - } + set_int_gauge( + &PROPOSER_COUNT, + &[CURRENT_EPOCH], + duties_service.proposer_count(current_epoch) as i64, + ); + set_int_gauge( + &ATTESTER_COUNT, + &[CURRENT_EPOCH], + duties_service.attester_count(current_epoch) as i64, + ); + set_int_gauge( + &ATTESTER_COUNT, + &[NEXT_EPOCH], + duties_service.attester_count(next_epoch) as i64, + ); } } diff --git a/validator_client/initialized_validators/src/key_cache.rs b/validator_client/initialized_validators/src/key_cache.rs index c2dd7aa8fe..b600013c8b 100644 --- a/validator_client/initialized_validators/src/key_cache.rs +++ b/validator_client/initialized_validators/src/key_cache.rs @@ -5,8 +5,8 @@ use eth2_keystore::json_keystore::{ Sha256Checksum, }; use eth2_keystore::{ - decrypt, default_kdf, encrypt, keypair_from_secret, Error as KeystoreError, PlainText, Uuid, - ZeroizeHash, IV_SIZE, SALT_SIZE, + Error as KeystoreError, IV_SIZE, PlainText, SALT_SIZE, Uuid, ZeroizeHash, decrypt, default_kdf, + encrypt, keypair_from_secret, }; use rand::prelude::*; use serde::{Deserialize, Serialize}; @@ -65,8 +65,8 @@ impl KeyCache { } pub fn init_crypto() -> Crypto { - let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); - let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + let salt = rand::rng().random::<[u8; SALT_SIZE]>(); + let iv = rand::rng().random::<[u8; IV_SIZE]>().to_vec().into(); let kdf = default_kdf(salt.to_vec()); let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv }); @@ -268,15 +268,7 @@ pub enum Error { #[cfg(test)] mod tests { use super::*; - use eth2_keystore::json_keystore::{HexBytes, Kdf}; - - #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct KeyCacheTest { - pub params: Kdf, - //pub checksum: ChecksumModule, - //pub cipher: CipherModule, - uuids: Vec, - } + use eth2_keystore::json_keystore::HexBytes; #[tokio::test] async fn test_serialization() { @@ -299,7 +291,7 @@ mod tests { #[tokio::test] async fn test_encryption() { let mut key_cache = KeyCache::new(); - let keypairs = vec![Keypair::random(), Keypair::random()]; + let keypairs = [Keypair::random(), Keypair::random()]; let uuids = vec![Uuid::from_u128(1), Uuid::from_u128(2)]; let passwords = vec![ PlainText::from(vec![1, 2, 3, 4, 5, 6]), diff --git a/validator_client/initialized_validators/src/lib.rs b/validator_client/initialized_validators/src/lib.rs index cbc1287a85..db6d03174d 100644 --- a/validator_client/initialized_validators/src/lib.rs +++ b/validator_client/initialized_validators/src/lib.rs @@ -11,10 +11,11 @@ pub mod key_cache; use account_utils::{ read_password, read_password_from_user, read_password_string, validator_definitions::{ - self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, - CONFIG_FILENAME, + self, CONFIG_FILENAME, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, + Web3SignerDefinition, }, }; +use bls::{Keypair, PublicKey, PublicKeyBytes}; use eth2_keystore::Keystore; use lockfile::{Lockfile, LockfileError}; use metrics::set_gauge; @@ -30,7 +31,7 @@ use std::sync::Arc; use std::time::Duration; use tracing::{debug, error, info, warn}; use types::graffiti::GraffitiString; -use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; +use types::{Address, Graffiti}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; use zeroize::Zeroizing; @@ -159,10 +160,10 @@ pub struct InitializedValidator { impl InitializedValidator { /// Return a reference to this validator's lockfile if it has one. - pub fn keystore_lockfile(&self) -> Option> { + pub fn keystore_lockfile(&self) -> Option> { match self.signing_method.as_ref() { SigningMethod::LocalKeystore { - ref voting_keystore_lockfile, + voting_keystore_lockfile, .. } => MutexGuard::try_map(voting_keystore_lockfile.lock(), |option_lockfile| { option_lockfile.as_mut() @@ -671,20 +672,19 @@ impl InitializedValidators { // 3. Delete from `self.validators`, which holds the signing method. // Delete the keystore files. - if let Some(initialized_validator) = self.validators.remove(&pubkey.compress()) { - if let SigningMethod::LocalKeystore { + if let Some(initialized_validator) = self.validators.remove(&pubkey.compress()) + && let SigningMethod::LocalKeystore { ref voting_keystore_path, ref voting_keystore_lockfile, ref voting_keystore, .. } = *initialized_validator.signing_method - { - // Drop the lock file so that it may be deleted. This is particularly important on - // Windows where the lockfile will fail to be deleted if it is still open. - drop(voting_keystore_lockfile.lock().take()); + { + // Drop the lock file so that it may be deleted. This is particularly important on + // Windows where the lockfile will fail to be deleted if it is still open. + drop(voting_keystore_lockfile.lock().take()); - self.delete_keystore_or_validator_dir(voting_keystore_path, voting_keystore)?; - } + self.delete_keystore_or_validator_dir(voting_keystore_path, voting_keystore)?; } // 4. Delete from validator definitions entirely. @@ -695,17 +695,16 @@ impl InitializedValidators { .map_err(Error::UnableToSaveDefinitions)?; // 5. Delete the keystore password if it's not being used by any definition. - if let Some(password_path) = password_path_opt.and_then(|p| p.canonicalize().ok()) { - if self + if let Some(password_path) = password_path_opt.and_then(|p| p.canonicalize().ok()) + && self .definitions .iter_voting_keystore_password_paths() // Require canonicalized paths so we can do a true equality check. .filter_map(|existing| existing.canonicalize().ok()) .all(|existing| existing != password_path) - { - fs::remove_file(&password_path) - .map_err(|e| Error::UnableToDeletePasswordFile(password_path, e))?; - } + { + fs::remove_file(&password_path) + .map_err(|e| Error::UnableToDeletePasswordFile(password_path, e))?; } Ok(keystore_and_password) @@ -723,14 +722,13 @@ impl InitializedValidators { // If the parent directory is a `ValidatorDir` within `self.validators_dir`, then // delete the entire directory so that it may be recreated if the keystore is // re-imported. - if let Some(validator_dir) = voting_keystore_path.parent() { - if validator_dir + if let Some(validator_dir) = voting_keystore_path.parent() + && validator_dir == ValidatorDirBuilder::get_dir_path(&self.validators_dir, voting_keystore) - { - fs::remove_dir_all(validator_dir) - .map_err(|e| Error::UnableToDeleteValidatorDir(validator_dir.into(), e))?; - return Ok(()); - } + { + fs::remove_dir_all(validator_dir) + .map_err(|e| Error::UnableToDeleteValidatorDir(validator_dir.into(), e))?; + return Ok(()); } // Otherwise just delete the keystore file. fs::remove_file(voting_keystore_path) @@ -1415,7 +1413,7 @@ impl InitializedValidators { for def in self.definitions.as_mut_slice() { match &mut def.signing_definition { SigningDefinition::LocalKeystore { - ref mut voting_keystore_password, + voting_keystore_password, .. } => { if let Some(password) = voting_keystore_password.take() { diff --git a/validator_client/lighthouse_validator_store/Cargo.toml b/validator_client/lighthouse_validator_store/Cargo.toml index 0f8220bdc9..01c7616be1 100644 --- a/validator_client/lighthouse_validator_store/Cargo.toml +++ b/validator_client/lighthouse_validator_store/Cargo.toml @@ -7,6 +7,7 @@ authors = ["Sigma Prime "] [dependencies] account_utils = { workspace = true } beacon_node_fallback = { workspace = true } +bls = { workspace = true } doppelganger_service = { workspace = true } either = { workspace = true } environment = { workspace = true } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index d7cf6fe36c..e88b07adf5 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1,4 +1,5 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; +use bls::{PublicKeyBytes, Signature}; use doppelganger_service::DoppelgangerService; use eth2::types::PublishBlockRequest; use initialized_validators::InitializedValidators; @@ -8,23 +9,22 @@ use serde::{Deserialize, Serialize}; use signing_method::Error as SigningError; use signing_method::{SignableMessage, SigningContext, SigningMethod}; use slashing_protection::{ - interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, + InterchangeError, NotSafe, Safe, SlashingDatabase, interchange::Interchange, }; use slot_clock::SlotClock; use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; use task_executor::TaskExecutor; -use tracing::{error, info, warn}; -use types::SignedInclusionList; +use tracing::{error, info, instrument, warn}; use types::{ - graffiti::GraffitiString, AbstractExecPayload, Address, AggregateAndProof, Attestation, - BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, - Graffiti, Hash256, InclusionList, PublicKeyBytes, SelectionProof, Signature, - SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, - SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, - SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, - ValidatorRegistrationData, VoluntaryExit, + AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, + InclusionList, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedInclusionList, SignedRoot, SignedValidatorRegistrationData, + SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, + SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -56,8 +56,8 @@ const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; /// Currently used as the default gas limit in execution clients. /// -/// https://ethresear.ch/t/on-increasing-the-block-gas-limit-technical-considerations-path-forward/21225. -pub const DEFAULT_GAS_LIMIT: u64 = 36_000_000; +/// https://ethpandaops.io/posts/gaslimit-scaling/. +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; pub struct LighthouseValidatorStore { validators: Arc>, @@ -243,6 +243,7 @@ impl LighthouseValidatorStore { /// Returns a `SigningMethod` for `validator_pubkey` *only if* that validator is considered safe /// by doppelganger protection. + #[instrument(skip_all, level = "debug")] fn doppelganger_checked_signing_method( &self, validator_pubkey: PublicKeyBytes, @@ -694,11 +695,7 @@ impl ValidatorStore for LighthouseValidatorS // If builder boost factor is set to 100 it should be treated // as None to prevent unnecessary calculations that could // lead to loss of information. - if factor == 100 { - None - } else { - Some(factor) - } + if factor == 100 { None } else { Some(factor) } }) } @@ -750,6 +747,7 @@ impl ValidatorStore for LighthouseValidatorS } } + #[instrument(skip_all)] async fn sign_attestation( &self, validator_pubkey: PublicKeyBytes, diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml index 3e1a48142f..cb321c2d49 100644 --- a/validator_client/signing_method/Cargo.toml +++ b/validator_client/signing_method/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +bls = { workspace = true } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } lockfile = { workspace = true } @@ -12,6 +13,7 @@ parking_lot = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } task_executor = { workspace = true } +tracing = { workspace = true } types = { workspace = true } url = { workspace = true } validator_metrics = { workspace = true } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index 869c037696..56d9784d8e 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -3,13 +3,15 @@ //! - Via a local `Keypair`. //! - Via a remote signer (Web3Signer) +use bls::{Keypair, PublicKey, Signature}; use eth2_keystore::Keystore; use lockfile::Lockfile; use parking_lot::Mutex; -use reqwest::{header::ACCEPT, Client}; +use reqwest::{Client, header::ACCEPT}; use std::path::PathBuf; use std::sync::Arc; use task_executor::TaskExecutor; +use tracing::instrument; use types::*; use url::Url; use web3signer::{ForkInfo, MessageType, SigningRequest, SigningResponse}; @@ -133,6 +135,7 @@ impl SigningMethod { } /// Return the signature of `signable_message`, with respect to the `signing_context`. + #[instrument(skip_all, level = "debug")] pub async fn get_signature>( &self, signable_message: SignableMessage<'_, E, Payload>, diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 25a5fa4136..4cab2b08b1 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -1,6 +1,7 @@ //! Contains the types required to make JSON requests to Web3Signer servers. use super::Error; +use bls::{PublicKeyBytes, Signature}; use serde::{Deserialize, Serialize}; use types::*; @@ -32,6 +33,7 @@ pub enum ForkName { Electra, Eip7805, Fulu, + Gloas, } #[derive(Debug, PartialEq, Serialize)] @@ -121,6 +123,11 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa block: None, block_header: Some(block.block_header()), }), + BeaconBlock::Gloas(_) => Ok(Web3SignerObject::BeaconBlock { + version: ForkName::Gloas, + block: None, + block_header: Some(block.block_header()), + }), } } diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 88e6dd794d..b80da6c786 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -5,14 +5,17 @@ authors = ["Michael Sproul ", "pscott Checkpoint { Checkpoint { @@ -159,8 +160,10 @@ fn valid_multiple_validators_not_surrounding() { #[test] fn invalid_source_exceeds_target() { StreamTest { - cases: vec![Test::single(attestation_data_builder(1, 0)) - .expect_invalid_att(InvalidAttestation::SourceExceedsTarget)], + cases: vec![ + Test::single(attestation_data_builder(1, 0)) + .expect_invalid_att(InvalidAttestation::SourceExceedsTarget), + ], ..StreamTest::default() } .run() diff --git a/validator_client/slashing_protection/src/bin/test_generator.rs b/validator_client/slashing_protection/src/bin/test_generator.rs index ff5866f986..df1c63f37d 100644 --- a/validator_client/slashing_protection/src/bin/test_generator.rs +++ b/validator_client/slashing_protection/src/bin/test_generator.rs @@ -1,13 +1,12 @@ -use slashing_protection::interchange::{ - Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock, -}; -use slashing_protection::interchange_test::{MultiTestCase, TestCase}; -use slashing_protection::test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}; +use eip_3076::{Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock}; +use fixed_bytes::FixedBytesExtended; use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION; +use slashing_protection::interchange_test::{MultiTestCase, TestCase}; +use slashing_protection::test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey}; use std::fs::{self, File}; use std::io::Write; use std::path::Path; -use types::{Epoch, FixedBytesExtended, Hash256, Slot}; +use types::{Epoch, Hash256, Slot}; fn metadata(genesis_validators_root: Hash256) -> InterchangeMetadata { InterchangeMetadata { diff --git a/validator_client/slashing_protection/src/block_tests.rs b/validator_client/slashing_protection/src/block_tests.rs index b3273015f4..2531f52d8c 100644 --- a/validator_client/slashing_protection/src/block_tests.rs +++ b/validator_client/slashing_protection/src/block_tests.rs @@ -2,7 +2,8 @@ use super::*; use crate::test_utils::*; -use types::{BeaconBlockHeader, FixedBytesExtended, Slot}; +use fixed_bytes::FixedBytesExtended; +use types::{BeaconBlockHeader, Slot}; pub fn block(slot: u64) -> BeaconBlockHeader { BeaconBlockHeader { diff --git a/validator_client/slashing_protection/src/extra_interchange_tests.rs b/validator_client/slashing_protection/src/extra_interchange_tests.rs index 0f88ec8b1d..18457720e4 100644 --- a/validator_client/slashing_protection/src/extra_interchange_tests.rs +++ b/validator_client/slashing_protection/src/extra_interchange_tests.rs @@ -2,8 +2,8 @@ use crate::test_utils::pubkey; use crate::*; +use fixed_bytes::FixedBytesExtended; use tempfile::tempdir; -use types::FixedBytesExtended; #[test] fn export_non_existent_key() { diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs index e1ac841905..0dfcda204d 100644 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -1,12 +1,14 @@ use crate::{ - interchange::{Interchange, SignedAttestation, SignedBlock}, - test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}, SigningRoot, SlashingDatabase, + test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey}, }; +use bls::PublicKeyBytes; +use eip_3076::{Interchange, SignedAttestation, SignedBlock}; +use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use tempfile::tempdir; -use types::{Epoch, FixedBytesExtended, Hash256, PublicKeyBytes, Slot}; +use types::{Epoch, Hash256, Slot}; #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] @@ -270,9 +272,11 @@ pub fn check_minification_invariants(interchange: &Interchange, minified: &Inter assert_eq!(mini_block.signing_root, None); // All original blocks should have slots <= the mini block. - assert!(original_blocks - .iter() - .all(|block| block.slot <= mini_block.slot)); + assert!( + original_blocks + .iter() + .all(|block| block.slot <= mini_block.slot) + ); } // Minified data should contain 1 attestation per validator, unless the validator never @@ -289,10 +293,12 @@ pub fn check_minification_invariants(interchange: &Interchange, minified: &Inter let mini_attestation = minified_attestations.first().unwrap(); assert_eq!(mini_attestation.signing_root, None); - assert!(original_attestations - .iter() - .all(|att| att.source_epoch <= mini_attestation.source_epoch - && att.target_epoch <= mini_attestation.target_epoch)); + assert!( + original_attestations + .iter() + .all(|att| att.source_epoch <= mini_attestation.source_epoch + && att.target_epoch <= mini_attestation.target_epoch) + ); } } } diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index 825a34cabc..f8580e7315 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -1,7 +1,6 @@ mod attestation_tests; mod block_tests; mod extra_interchange_tests; -pub mod interchange; pub mod interchange_test; mod parallel_tests; mod registration_tests; @@ -10,16 +9,21 @@ mod signed_block; mod slashing_database; pub mod test_utils; +pub mod interchange { + pub use eip_3076::{Interchange, InterchangeMetadata}; +} + pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation}; pub use crate::signed_block::{InvalidBlock, SignedBlock}; pub use crate::slashing_database::{ - InterchangeError, InterchangeImportOutcome, SlashingDatabase, - SUPPORTED_INTERCHANGE_FORMAT_VERSION, + InterchangeError, InterchangeImportOutcome, SUPPORTED_INTERCHANGE_FORMAT_VERSION, + SlashingDatabase, }; +use bls::PublicKeyBytes; use rusqlite::Error as SQLError; use std::fmt::Display; use std::io::{Error as IOError, ErrorKind}; -use types::{Hash256, PublicKeyBytes}; +use types::Hash256; /// The filename within the `validators` directory that contains the slashing protection DB. pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; @@ -89,7 +93,7 @@ impl SigningRoot { /// Safely parse a `SigningRoot` from the given `column` of an SQLite `row`. fn signing_root_from_row(column: usize, row: &rusqlite::Row) -> rusqlite::Result { - use rusqlite::{types::Type, Error}; + use rusqlite::{Error, types::Type}; let bytes: Vec = row.get(column)?; if bytes.len() == 32 { @@ -130,7 +134,7 @@ impl Display for NotSafe { #[cfg(test)] mod test { - use types::FixedBytesExtended; + use fixed_bytes::FixedBytesExtended; use super::*; diff --git a/validator_client/slashing_protection/src/signed_attestation.rs b/validator_client/slashing_protection/src/signed_attestation.rs index 332f80c704..c897b54002 100644 --- a/validator_client/slashing_protection/src/signed_attestation.rs +++ b/validator_client/slashing_protection/src/signed_attestation.rs @@ -1,4 +1,4 @@ -use crate::{signing_root_from_row, SigningRoot}; +use crate::{SigningRoot, signing_root_from_row}; use types::{AttestationData, Epoch, Hash256, SignedRoot}; /// An attestation that has previously been signed. diff --git a/validator_client/slashing_protection/src/signed_block.rs b/validator_client/slashing_protection/src/signed_block.rs index d46872529e..5918d2c61d 100644 --- a/validator_client/slashing_protection/src/signed_block.rs +++ b/validator_client/slashing_protection/src/signed_block.rs @@ -1,4 +1,4 @@ -use crate::{signing_root_from_row, SigningRoot}; +use crate::{SigningRoot, signing_root_from_row}; use types::{BeaconBlockHeader, Hash256, SignedRoot, Slot}; /// A block that has previously been signed. diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index f4c844d314..67e1234ac5 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -1,17 +1,19 @@ -use crate::interchange::{ +use crate::signed_attestation::InvalidAttestation; +use crate::signed_block::InvalidBlock; +use crate::{NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot, signing_root_from_row}; +use bls::PublicKeyBytes; +use eip_3076::{ Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation, SignedBlock as InterchangeBlock, }; -use crate::signed_attestation::InvalidAttestation; -use crate::signed_block::InvalidBlock; -use crate::{signing_root_from_row, NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot}; use filesystem::restrict_file_permissions; use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; +use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params}; use std::fs::File; use std::path::Path; use std::time::Duration; -use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKeyBytes, SignedRoot, Slot}; +use tracing::instrument; +use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, SignedRoot, Slot}; type Pool = r2d2::Pool; @@ -356,15 +358,15 @@ impl SlashingDatabase { .prepare("SELECT MIN(slot) FROM signed_blocks WHERE validator_id = ?1")? .query_row(params![validator_id], |row| row.get(0))?; - if let Some(min_slot) = min_slot { - if slot <= min_slot { - return Err(NotSafe::InvalidBlock( - InvalidBlock::SlotViolatesLowerBound { - block_slot: slot, - bound_slot: min_slot, - }, - )); - } + if let Some(min_slot) = min_slot + && slot <= min_slot + { + return Err(NotSafe::InvalidBlock( + InvalidBlock::SlotViolatesLowerBound { + block_slot: slot, + bound_slot: min_slot, + }, + )); } Ok(Safe::Valid) @@ -467,30 +469,30 @@ impl SlashingDatabase { .prepare("SELECT MIN(source_epoch) FROM signed_attestations WHERE validator_id = ?1")? .query_row(params![validator_id], |row| row.get(0))?; - if let Some(min_source) = min_source { - if att_source_epoch < min_source { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::SourceLessThanLowerBound { - source_epoch: att_source_epoch, - bound_epoch: min_source, - }, - )); - } + if let Some(min_source) = min_source + && att_source_epoch < min_source + { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::SourceLessThanLowerBound { + source_epoch: att_source_epoch, + bound_epoch: min_source, + }, + )); } let min_target = txn .prepare("SELECT MIN(target_epoch) FROM signed_attestations WHERE validator_id = ?1")? .query_row(params![validator_id], |row| row.get(0))?; - if let Some(min_target) = min_target { - if att_target_epoch <= min_target { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::TargetLessThanOrEqLowerBound { - target_epoch: att_target_epoch, - bound_epoch: min_target, - }, - )); - } + if let Some(min_target) = min_target + && att_target_epoch <= min_target + { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::TargetLessThanOrEqLowerBound { + target_epoch: att_target_epoch, + bound_epoch: min_target, + }, + )); } // Everything has been checked, return Valid @@ -599,12 +601,47 @@ impl SlashingDatabase { Ok(safe) } + /// Check whether a block would be safe to sign if we were to sign it now. + /// + /// The database is not modified, and therefore multiple threads reading the database might get + /// the same result. Therefore: + /// + /// DO NOT USE THIS FUNCTION TO DECIDE IF A BLOCK IS SAFE TO SIGN! + pub fn preliminary_check_block_proposal( + &self, + validator_pubkey: &PublicKeyBytes, + block_header: &BeaconBlockHeader, + domain: Hash256, + ) -> Result { + #[allow(clippy::disallowed_methods)] + self.preliminary_check_block_signing_root( + validator_pubkey, + block_header.slot, + block_header.signing_root(domain).into(), + ) + } + + /// As for `preliminary_check_block_proposal` but without requiring the whole `BeaconBlockHeader`. + /// + /// DO NOT USE THIS FUNCTION TO DECIDE IF A BLOCK IS SAFE TO SIGN! + pub fn preliminary_check_block_signing_root( + &self, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + signing_root: SigningRoot, + ) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; + self.check_block_proposal(&txn, validator_pubkey, slot, signing_root) + } + /// Check an attestation for slash safety, and if it is safe, record it in the database. /// /// The checking and inserting happen atomically and exclusively. We enforce exclusivity /// to prevent concurrent checks and inserts from resulting in slashable data being inserted. /// /// This is the safe, externally-callable interface for checking attestations. + #[instrument(skip_all, level = "debug")] pub fn check_and_insert_attestation( &self, validator_pubkey: &PublicKeyBytes, @@ -670,6 +707,49 @@ impl SlashingDatabase { Ok(safe) } + /// Check whether an attestation would be safe to sign if we were to sign it now. + /// + /// The database is not modified, and therefore multiple threads reading the database might get + /// the same result. Therefore: + /// + /// DO NOT USE THIS FUNCTION TO DECIDE IF AN ATTESTATION IS SAFE TO SIGN! + pub fn preliminary_check_attestation( + &self, + validator_pubkey: &PublicKeyBytes, + attestation: &AttestationData, + domain: Hash256, + ) -> Result { + let attestation_signing_root = attestation.signing_root(domain).into(); + #[allow(clippy::disallowed_methods)] + self.preliminary_check_attestation_signing_root( + validator_pubkey, + attestation.source.epoch, + attestation.target.epoch, + attestation_signing_root, + ) + } + + /// As for `preliminary_check_attestation` but without requiring the whole `AttestationData`. + /// + /// DO NOT USE THIS FUNCTION TO DECIDE IF AN ATTESTATION IS SAFE TO SIGN! + pub fn preliminary_check_attestation_signing_root( + &self, + validator_pubkey: &PublicKeyBytes, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: SigningRoot, + ) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; + self.check_attestation( + &txn, + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + ) + } + /// Import slashing protection from another client in the interchange format. /// /// This function will atomically import the entire interchange, failing if *any* @@ -1142,7 +1222,7 @@ pub enum InterchangeError { interchange_file: Hash256, client: Hash256, }, - MaxInconsistent, + Eip3076(eip_3076::Error), SummaryInconsistent, SQLError(String), SQLPoolError(r2d2::Error), @@ -1218,9 +1298,10 @@ mod tests { assert_eq!(db.conn_pool.max_size(), POOL_SIZE); assert_eq!(db.conn_pool.connection_timeout(), CONNECTION_TIMEOUT); let conn = db.conn_pool.get().unwrap(); - assert!(conn - .pragma_query_value(None, "foreign_keys", |row| { row.get::<_, bool>(0) }) - .unwrap()); + assert!( + conn.pragma_query_value(None, "foreign_keys", |row| { row.get::<_, bool>(0) }) + .unwrap() + ); assert_eq!( conn.pragma_query_value(None, "locking_mode", |row| { row.get::<_, String>(0) }) .unwrap() diff --git a/validator_client/slashing_protection/src/test_utils.rs b/validator_client/slashing_protection/src/test_utils.rs index 8cbca12a10..39ede58bb2 100644 --- a/validator_client/slashing_protection/src/test_utils.rs +++ b/validator_client/slashing_protection/src/test_utils.rs @@ -1,6 +1,6 @@ use crate::*; -use tempfile::{tempdir, TempDir}; -use types::{test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader}; +use tempfile::{TempDir, tempdir}; +use types::{AttestationData, BeaconBlockHeader, test_utils::generate_deterministic_keypair}; pub const DEFAULT_VALIDATOR_INDEX: usize = 0; pub const DEFAULT_DOMAIN: Hash256 = Hash256::ZERO; @@ -135,10 +135,12 @@ fn roundtrip_database(dir: &TempDir, db: &SlashingDatabase, is_empty: bool) { .export_all_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) .unwrap(); - assert!(exported - .minify() - .unwrap() - .equiv(&reexported.minify().unwrap())); + assert!( + exported + .minify() + .unwrap() + .equiv(&reexported.minify().unwrap()) + ); assert_eq!(is_empty, exported.is_empty()); } diff --git a/validator_client/slashing_protection/tests/migration.rs b/validator_client/slashing_protection/tests/migration.rs index 3d4ec7ea9a..14bf0d63f9 100644 --- a/validator_client/slashing_protection/tests/migration.rs +++ b/validator_client/slashing_protection/tests/migration.rs @@ -1,10 +1,11 @@ //! Tests for upgrading a previous version of the database to the latest schema. +use fixed_bytes::FixedBytesExtended; use slashing_protection::{NotSafe, SlashingDatabase}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; use tempfile::tempdir; -use types::{FixedBytesExtended, Hash256}; +use types::Hash256; fn test_data_dir() -> PathBuf { Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("migration-tests") diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index cdbf9f8472..3e1c46097f 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -1,8 +1,8 @@ use beacon_node_fallback::ApiTopic; use clap::builder::ArgPredicate; pub use clap::{FromArgMatches, Parser}; -use clap_utils::get_color_style; use clap_utils::FLAG_HEADER; +use clap_utils::get_color_style; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use types::Address; @@ -150,6 +150,16 @@ pub struct ValidatorClient { )] pub graffiti: Option, + #[clap( + long, + requires = "graffiti", + help = "When used, client version info will be prepended to user custom graffiti, with a space in between. \ + This should only be used with a Lighthouse beacon node.", + display_order = 0, + help_heading = FLAG_HEADER + )] + pub graffiti_append: bool, + #[clap( long, value_name = "GRAFFITI-FILE", @@ -388,7 +398,7 @@ pub struct ValidatorClient { #[clap( long, value_name = "INTEGER", - default_value_t = 36_000_000, + default_value_t = 60_000_000, requires = "builder_proposals", help = "The gas limit to be used in all builder proposals for all validators managed \ by this validator client. Note this will not necessarily be used if the gas limit \ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 726aa96cf9..1a286a74dc 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,13 +1,13 @@ use crate::cli::ValidatorClient; -use beacon_node_fallback::beacon_node_health::BeaconNodeSyncDistanceTiers; use beacon_node_fallback::ApiTopic; +use beacon_node_fallback::beacon_node_health::BeaconNodeSyncDistanceTiers; use clap::ArgMatches; use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_required}; use directory::{ - get_network_dir, DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, - DEFAULT_VALIDATOR_DIR, + DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_SECRET_DIR, DEFAULT_VALIDATOR_DIR, + get_network_dir, }; -use eth2::types::Graffiti; +use eth2::types::{Graffiti, GraffitiPolicy}; use graffiti_file::GraffitiFile; use initialized_validators::Config as InitializedValidatorsConfig; use lighthouse_validator_store::Config as ValidatorStoreConfig; @@ -55,6 +55,8 @@ pub struct Config { pub graffiti: Option, /// Graffiti file to load per validator graffitis. pub graffiti_file: Option, + /// GraffitiPolicy to append client version info + pub graffiti_policy: Option, /// Configuration for the HTTP REST API. pub http_api: validator_http_api::Config, /// Configuration for the HTTP REST API. @@ -102,8 +104,10 @@ impl Default for Config { let validator_dir = base_dir.join(DEFAULT_VALIDATOR_DIR); let secrets_dir = base_dir.join(DEFAULT_SECRET_DIR); - let beacon_nodes = vec![SensitiveUrl::parse(DEFAULT_BEACON_NODE) - .expect("beacon_nodes must always be a valid url.")]; + let beacon_nodes = vec![ + SensitiveUrl::parse(DEFAULT_BEACON_NODE) + .expect("beacon_nodes must always be a valid url."), + ]; Self { validator_store: ValidatorStoreConfig::default(), validator_dir, @@ -117,6 +121,7 @@ impl Default for Config { long_timeouts_multiplier: 1, graffiti: None, graffiti_file: None, + graffiti_policy: None, http_api: <_>::default(), http_metrics: <_>::default(), beacon_node_fallback: <_>::default(), @@ -231,6 +236,12 @@ impl Config { } } + config.graffiti_policy = if validator_client_config.graffiti_append { + Some(GraffitiPolicy::AppendClientVersions) + } else { + Some(GraffitiPolicy::PreserveUserGraffiti) + }; + if let Some(input_fee_recipient) = validator_client_config.suggested_fee_recipient { config.validator_store.fee_recipient = Some(input_fee_recipient); } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 92b10919f6..5bb4c47c1c 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -2,21 +2,22 @@ pub mod cli; pub mod config; use crate::cli::ValidatorClient; +use crate::duties_service::SelectionProofConfig; pub use config::Config; use initialized_validators::InitializedValidators; use metrics::set_gauge; use monitoring_api::{MonitoringHttpClient, ProcessType}; use sensitive_url::SensitiveUrl; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use account_utils::validator_definitions::ValidatorDefinitions; use beacon_node_fallback::{ - start_fallback_updater_service, BeaconNodeFallback, CandidateBeaconNode, + BeaconNodeFallback, CandidateBeaconNode, start_fallback_updater_service, }; use clap::ArgMatches; use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; -use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts}; +use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts, reqwest::ClientBuilder}; use initialized_validators::Error::UnableToOpenVotingKeystore; use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::RwLock; @@ -31,7 +32,7 @@ use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::{ sync::mpsc, - time::{sleep, Duration}, + time::{Duration, sleep}, }; use tracing::{debug, error, info, warn}; use types::{EthSpec, Hash256}; @@ -55,26 +56,24 @@ const RETRY_DELAY: Duration = Duration::from_secs(2); /// The time between polls when waiting for genesis. const WAITING_FOR_GENESIS_POLL_TIME: Duration = Duration::from_secs(12); -/// Specific timeout constants for HTTP requests involved in different validator duties. -/// This can help ensure that proper endpoint fallback occurs. -const HTTP_ATTESTATION_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT: u32 = 24; -const HTTP_LIVENESS_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_PROPOSAL_TIMEOUT_QUOTIENT: u32 = 2; -const HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_SYNC_COMMITTEE_CONTRIBUTION_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT: u32 = 4; -const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4; -const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4; -const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4; - const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; +/// Compute attestation selection proofs this many slots before they are required. +/// +/// At start-up selection proofs will be computed with less lookahead out of necessity. +const SELECTION_PROOF_SLOT_LOOKAHEAD: u64 = 8; + +/// The attestation selection proof lookahead for those running with the --distributed flag. +const SELECTION_PROOF_SLOT_LOOKAHEAD_DVT: u64 = 1; + +/// Fraction of a slot at which attestation selection proof signing should happen (2 means half way). +const SELECTION_PROOF_SCHEDULE_DENOM: u32 = 2; + +/// Number of epochs in advance to compute sync selection proofs when not in `distributed` mode. +pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; +/// Number of slots in advance to compute sync selection proofs when in `distributed` mode. +pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1; + type ValidatorStore = LighthouseValidatorStore; #[derive(Clone)] @@ -91,7 +90,6 @@ pub struct ProductionValidatorClient { slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option, config: Config, - beacon_nodes: Arc>, genesis_time: u64, } @@ -296,27 +294,7 @@ impl ProductionValidatorClient { // Use quicker timeouts if a fallback beacon node exists. let timeouts = if i < last_beacon_node_index && !config.use_long_timeouts { info!("Fallback endpoints are available, using optimized timeouts."); - Timeouts { - attestation: slot_duration / HTTP_ATTESTATION_TIMEOUT_QUOTIENT, - attester_duties: slot_duration / HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT, - attestation_subscriptions: slot_duration - / HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT, - liveness: slot_duration / HTTP_LIVENESS_TIMEOUT_QUOTIENT, - proposal: slot_duration / HTTP_PROPOSAL_TIMEOUT_QUOTIENT, - proposer_duties: slot_duration / HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT, - sync_committee_contribution: slot_duration - / HTTP_SYNC_COMMITTEE_CONTRIBUTION_TIMEOUT_QUOTIENT, - sync_duties: slot_duration / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, - inclusion_list: slot_duration / HTTP_INCLUSION_LIST_TIMEOUT_QUOTIENT, - inclusion_list_duties: slot_duration - / HTTP_INCLUSION_LIST_DUTIES_TIMEOUT_QUOTIENT, - get_beacon_blocks_ssz: slot_duration - / HTTP_GET_BEACON_BLOCK_SSZ_TIMEOUT_QUOTIENT, - get_debug_beacon_states: slot_duration / HTTP_GET_DEBUG_BEACON_STATE_QUOTIENT, - get_deposit_snapshot: slot_duration / HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT, - get_validator_block: slot_duration / HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT, - default: slot_duration / HTTP_DEFAULT_TIMEOUT_QUOTIENT, - } + Timeouts::use_optimized_timeouts(slot_duration) } else { Timeouts::set_all(slot_duration.saturating_mul(config.long_timeouts_multiplier)) }; @@ -449,6 +427,41 @@ impl ProductionValidatorClient { validator_store.prune_slashing_protection_db(slot.epoch(E::slots_per_epoch()), true); } + // Define a config to be pass to duties_service. + // The defined config here defaults to using selections_endpoint and parallel_sign (i.e., distributed mode) + // Other DVT applications, e.g., Anchor can pass in different configs to suit different needs. + let attestation_selection_proof_config = if config.distributed { + SelectionProofConfig { + lookahead_slot: SELECTION_PROOF_SLOT_LOOKAHEAD_DVT, + computation_offset: slot_clock.slot_duration() / SELECTION_PROOF_SCHEDULE_DENOM, + selections_endpoint: true, + parallel_sign: true, + } + } else { + SelectionProofConfig { + lookahead_slot: SELECTION_PROOF_SLOT_LOOKAHEAD, + computation_offset: slot_clock.slot_duration() / SELECTION_PROOF_SCHEDULE_DENOM, + selections_endpoint: false, + parallel_sign: false, + } + }; + + let sync_selection_proof_config = if config.distributed { + SelectionProofConfig { + lookahead_slot: AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED, + computation_offset: Duration::default(), + selections_endpoint: true, + parallel_sign: true, + } + } else { + SelectionProofConfig { + lookahead_slot: E::slots_per_epoch() * AGGREGATION_PRE_COMPUTE_EPOCHS, + computation_offset: Duration::default(), + selections_endpoint: false, + parallel_sign: false, + } + }; + let duties_service = Arc::new( DutiesServiceBuilder::new() .slot_clock(slot_clock.clone()) @@ -457,7 +470,8 @@ impl ProductionValidatorClient { .spec(context.eth2_config.spec.clone()) .executor(context.executor.clone()) .enable_high_validator_count_metrics(config.enable_high_validator_count_metrics) - .distributed(config.distributed) + .attestation_selection_proof_config(attestation_selection_proof_config) + .sync_selection_proof_config(sync_selection_proof_config) .disable_attesting(config.disable_attesting) .build()?, ); @@ -475,7 +489,8 @@ impl ProductionValidatorClient { .executor(context.executor.clone()) .chain_spec(context.eth2_config.spec.clone()) .graffiti(config.graffiti) - .graffiti_file(config.graffiti_file.clone()); + .graffiti_file(config.graffiti_file.clone()) + .graffiti_policy(config.graffiti_policy); // If we have proposer nodes, add them to the block service builder. if proposer_nodes_num > 0 { @@ -536,7 +551,6 @@ impl ProductionValidatorClient { slot_clock, http_api_listen_addr: None, genesis_time, - beacon_nodes, }) } @@ -582,7 +596,7 @@ impl ProductionValidatorClient { }; // Wait until genesis has occurred. - wait_for_genesis(&self.beacon_nodes, self.genesis_time).await?; + wait_for_genesis(self.genesis_time).await?; duties_service::start_update_service(self.duties_service.clone(), block_service_tx); @@ -728,10 +742,7 @@ async fn init_from_beacon_node( Ok((genesis.genesis_time, genesis.genesis_validators_root)) } -async fn wait_for_genesis( - beacon_nodes: &BeaconNodeFallback, - genesis_time: u64, -) -> Result<(), String> { +async fn wait_for_genesis(genesis_time: u64) -> Result<(), String> { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| format!("Unable to read system time: {:?}", e))?; @@ -751,7 +762,7 @@ async fn wait_for_genesis( // Start polling the node for pre-genesis information, cancelling the polling as soon as the // timer runs out. tokio::select! { - result = poll_whilst_waiting_for_genesis(beacon_nodes, genesis_time) => result?, + result = poll_whilst_waiting_for_genesis(genesis_time) => result?, () = sleep(genesis_time - now) => () }; @@ -771,46 +782,20 @@ async fn wait_for_genesis( /// Request the version from the node, looping back and trying again on failure. Exit once the node /// has been contacted. -async fn poll_whilst_waiting_for_genesis( - beacon_nodes: &BeaconNodeFallback, - genesis_time: Duration, -) -> Result<(), String> { +async fn poll_whilst_waiting_for_genesis(genesis_time: Duration) -> Result<(), String> { loop { - match beacon_nodes - .first_success(|beacon_node| async move { beacon_node.get_lighthouse_staking().await }) - .await - { - Ok(is_staking) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| format!("Unable to read system time: {:?}", e))?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("Unable to read system time: {:?}", e))?; - if !is_staking { - error!( - msg = "this will caused missed duties", - info = "see the --staking CLI flag on the beacon node", - "Staking is disabled for beacon node" - ); - } - - if now < genesis_time { - info!( - bn_staking_enabled = is_staking, - seconds_to_wait = (genesis_time - now).as_secs(), - "Waiting for genesis" - ); - } else { - break Ok(()); - } - } - Err(e) => { - error!( - error = %e, - "Error polling beacon node" - ); - } + if now < genesis_time { + info!( + seconds_to_wait = (genesis_time - now).as_secs(), + "Waiting for genesis" + ); + } else { + break Ok(()); } - sleep(WAITING_FOR_GENESIS_POLL_TIME).await; } } diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index f776567706..587d4668b8 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,6 +1,5 @@ use crate::duties_service::{DutiesService, DutyAndProof}; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use either::Either; use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; @@ -8,8 +7,8 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use task_executor::TaskExecutor; -use tokio::time::{sleep, sleep_until, Duration, Instant}; -use tracing::{debug, error, info, trace, warn}; +use tokio::time::{Duration, Instant, sleep, sleep_until}; +use tracing::{Instrument, Span, debug, error, info, info_span, instrument, trace, warn}; use tree_hash::TreeHash; use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; @@ -181,8 +180,9 @@ impl AttestationService Result<(), String> { let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; let duration_to_next_slot = self @@ -190,6 +190,59 @@ impl AttestationService = self.duties_service.attesters(slot).into_iter().collect(); + + // Return early if there is no attestation duties + if attestation_duties.is_empty() { + return Ok(()); + } + + let attestation_service = self.clone(); + + let attestation_data_handle = self + .inner + .executor + .spawn_handle( + async move { + let attestation_data = attestation_service + .beacon_nodes + .first_success(|beacon_node| async move { + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS_HTTP_GET], + ); + beacon_node + .get_validator_attestation_data(slot, 0) + .await + .map_err(|e| format!("Failed to produce attestation data: {:?}", e)) + .map(|result| result.data) + }) + .await + .map_err(|e| e.to_string())?; + + attestation_service + .sign_and_publish_attestations( + slot, + &attestation_duties, + attestation_data.clone(), + ) + .await + .map_err(|e| { + crit!( + error = format!("{:?}", e), + slot = slot.as_u64(), + "Error during attestation routine" + ); + e + })?; + Ok::(attestation_data) + }, + "unaggregated attestation production", + ) + .ok_or("Failed to spawn attestation data task")?; + // If a validator needs to publish an aggregate attestation, they must do so at 2/3 // through the slot. This delay triggers at this time let aggregate_production_instant = Instant::now() @@ -197,7 +250,7 @@ impl AttestationService> = self + let aggregate_duties_by_committee_index: HashMap> = self .duties_service .attesters(slot) .into_iter() @@ -208,24 +261,45 @@ impl AttestationService data, + Ok(Some(Err(err))) => { + error!(?err, "Attestation production failed"); + return; + } + Ok(None) | Err(_) => { + info!("Aborting attestation production due to shutdown"); + return; + } + }; + + // For each committee index for this slot: + // Create and publish `SignedAggregateAndProof` for all aggregating validators. + aggregate_duties_by_committee_index.into_iter().for_each( + |(committee_index, validator_duties)| { + let attestation_service = attestation_service_clone.clone(); + let attestation_data = attestation_data.clone(); + executor.spawn_ignoring_error( + attestation_service.handle_aggregates( + slot, + committee_index, + validator_duties, + aggregate_production_instant, + attestation_data, + ), + "aggregate publish", + ); + }, + ) + }, + "attestation and aggregate publish", + ); // Schedule pruning of the slashing protection database once all unaggregated // attestations have (hopefully) been signed, i.e. at the same time as aggregate @@ -235,109 +309,73 @@ impl AttestationService, aggregate_production_instant: Instant, + attestation_data: AttestationData, ) -> Result<(), ()> { - let attestations_timer = validator_metrics::start_timer_vec( - &validator_metrics::ATTESTATION_SERVICE_TIMES, - &[validator_metrics::ATTESTATIONS], - ); - - // There's not need to produce `Attestation` or `SignedAggregateAndProof` if we do not have + // There's not need to produce `SignedAggregateAndProof` if we do not have // any validators for the given `slot` and `committee_index`. if validator_duties.is_empty() { return Ok(()); } - // Step 1. - // - // Download, sign and publish an `Attestation` for each validator. - let attestation_opt = self - .produce_and_publish_attestations(slot, committee_index, &validator_duties) + // Wait until the `aggregation_production_instant` (2/3rds + // of the way though the slot). As verified in the + // `delay_triggers_when_in_the_past` test, this code will still run + // even if the instant has already elapsed. + sleep_until(aggregate_production_instant).await; + + // Start the metrics timer *after* we've done the delay. + let _aggregates_timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::AGGREGATES], + ); + + // Download, sign and publish a `SignedAggregateAndProof` for each + // validator that is elected to aggregate for this `slot` and + // `committee_index`. + self.produce_and_publish_aggregates(&attestation_data, committee_index, &validator_duties) .await .map_err(move |e| { crit!( error = format!("{:?}", e), committee_index, slot = slot.as_u64(), - "Error during attestation routine" + "Error during aggregate attestation routine" ) })?; - drop(attestations_timer); - - // Step 2. - // - // If an attestation was produced, make an aggregate. - if let Some(attestation_data) = attestation_opt { - // First, wait until the `aggregation_production_instant` (2/3rds - // of the way though the slot). As verified in the - // `delay_triggers_when_in_the_past` test, this code will still run - // even if the instant has already elapsed. - sleep_until(aggregate_production_instant).await; - - // Start the metrics timer *after* we've done the delay. - let _aggregates_timer = validator_metrics::start_timer_vec( - &validator_metrics::ATTESTATION_SERVICE_TIMES, - &[validator_metrics::AGGREGATES], - ); - - // Then download, sign and publish a `SignedAggregateAndProof` for each - // validator that is elected to aggregate for this `slot` and - // `committee_index`. - self.produce_and_publish_aggregates( - &attestation_data, - committee_index, - &validator_duties, - ) - .await - .map_err(move |e| { - crit!( - error = format!("{:?}", e), - committee_index, - slot = slot.as_u64(), - "Error during attestation routine" - ) - })?; - } - Ok(()) } - /// Performs the first step of the attesting process: downloading `Attestation` objects, - /// signing them and returning them to the validator. + /// Performs the main steps of the attesting process: signing and publishing to the BN. /// - /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting + /// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/validator.md#attesting /// /// ## Detail /// /// The given `validator_duties` should already be filtered to only contain those that match - /// `slot` and `committee_index`. Critical errors will be logged if this is not the case. - /// - /// Only one `Attestation` is downloaded from the BN. It is then cloned and signed by each - /// validator and the list of individually-signed `Attestation` objects is returned to the BN. - async fn produce_and_publish_attestations( + /// `slot`. Critical errors will be logged if this is not the case. + #[instrument(skip_all, fields(%slot, %attestation_data.beacon_block_root))] + async fn sign_and_publish_attestations( &self, slot: Slot, - committee_index: CommitteeIndex, validator_duties: &[DutyAndProof], - ) -> Result, String> { - if validator_duties.is_empty() { - return Ok(None); - } + attestation_data: AttestationData, + ) -> Result<(), String> { + let _attestations_timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS], + ); let current_epoch = self .slot_clock @@ -345,101 +383,90 @@ impl AttestationService(attestation_data, &self.chain_spec) { - crit!( - validator = ?duty.pubkey, - duty_slot = %duty.slot, - attestation_slot = %attestation_data.slot, - duty_index = duty.committee_index, - attestation_index = attestation_data.index, - "Inconsistent validator duties during signing" - ); - return None; - } - - let mut attestation = match Attestation::empty_for_signing( - duty.committee_index, - duty.committee_length as usize, - attestation_data.slot, - attestation_data.beacon_block_root, - attestation_data.source, - attestation_data.target, - &self.chain_spec, - ) { - Ok(attestation) => attestation, - Err(err) => { + // Ensure that the attestation matches the duties. + if !duty.match_attestation_data::(attestation_data, &self.chain_spec) { crit!( validator = ?duty.pubkey, - ?duty, - ?err, - "Invalid validator duties during signing" + duty_slot = %duty.slot, + attestation_slot = %attestation_data.slot, + duty_index = duty.committee_index, + attestation_index = attestation_data.index, + "Inconsistent validator duties during signing" ); return None; } - }; - match self - .validator_store - .sign_attestation( - duty.pubkey, - duty.validator_committee_index as usize, - &mut attestation, - current_epoch, - ) - .await - { - Ok(()) => Some((attestation, duty.validator_index)), - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - warn!( - info = "a validator may have recently been removed from this VC", - pubkey = ?pubkey, - validator = ?duty.pubkey, - committee_index = committee_index, - slot = slot.as_u64(), - "Missing pubkey for attestation" - ); - None - } - Err(e) => { - crit!( - error = ?e, - validator = ?duty.pubkey, - committee_index, - slot = slot.as_u64(), - "Failed to sign attestation" - ); - None + let mut attestation = match Attestation::empty_for_signing( + duty.committee_index, + duty.committee_length as usize, + attestation_data.slot, + attestation_data.beacon_block_root, + attestation_data.source, + attestation_data.target, + &self.chain_spec, + ) { + Ok(attestation) => attestation, + Err(err) => { + crit!( + validator = ?duty.pubkey, + ?duty, + ?err, + "Invalid validator duties during signing" + ); + return None; + } + }; + + match self + .validator_store + .sign_attestation( + duty.pubkey, + duty.validator_committee_index as usize, + &mut attestation, + current_epoch, + ) + .await + { + Ok(()) => Some((attestation, duty.validator_index)), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + warn!( + info = "a validator may have recently been removed from this VC", + pubkey = ?pubkey, + validator = ?duty.pubkey, + slot = slot.as_u64(), + "Missing pubkey for attestation" + ); + None + } + Err(e) => { + crit!( + error = ?e, + validator = ?duty.pubkey, + slot = slot.as_u64(), + "Failed to sign attestation" + ); + None + } } } + .instrument(Span::current()) }); // Execute all the futures in parallel, collecting any successful results. let (ref attestations, ref validator_indices): (Vec<_>, Vec<_>) = join_all(signing_futures) + .instrument(info_span!( + "sign_attestations", + count = validator_duties.len() + )) .await .into_iter() .flatten() @@ -447,7 +474,7 @@ impl AttestationService AttestationService Some(a), - Err(e) => { - // This shouldn't happen unless BN and VC are out of sync with - // respect to the Electra fork. - error!( - error = ?e, - committee_index = attestation_data.index, - slot = slot.as_u64(), - "type" = "unaggregated", - "Unable to convert to SingleAttestation" - ); - None - } - } - }) - .collect::>(); - beacon_node - .post_beacon_pool_attestations_v2::( - Either::Right(single_attestations), - fork_name, - ) - .await - } else { - beacon_node - .post_beacon_pool_attestations_v1(attestations) - .await - } + let single_attestations = attestations + .iter() + .zip(validator_indices) + .filter_map(|(a, i)| { + match a.to_single_attestation_with_attester_index(*i) { + Ok(a) => Some(a), + Err(e) => { + // This shouldn't happen unless BN and VC are out of sync with + // respect to the Electra fork. + error!( + error = ?e, + committee_index = attestation_data.index, + slot = slot.as_u64(), + "type" = "unaggregated", + "Unable to convert to SingleAttestation" + ); + None + } + } + }) + .collect::>(); + + beacon_node + .post_beacon_pool_attestations_v2::(single_attestations, fork_name) + .await }) + .instrument(info_span!( + "publish_attestations", + count = attestations.len() + )) .await { Ok(()) => info!( @@ -516,7 +539,7 @@ impl AttestationService AttestationService AttestationService AttestationService AttestationService { diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 01f786e160..625f8db7cb 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,7 +1,8 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; -use bls::SignatureBytes; +use bls::PublicKeyBytes; +use eth2::types::GraffitiPolicy; use eth2::{BeaconNodeHttpClient, StatusCode}; -use graffiti_file::{determine_graffiti, GraffitiFile}; +use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; use slot_clock::SlotClock; use std::fmt::Debug; @@ -11,8 +12,8 @@ use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; -use tracing::{debug, error, info, trace, warn}; -use types::{BlockType, ChainSpec, EthSpec, Graffiti, PublicKeyBytes, Slot}; +use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; #[derive(Debug)] @@ -50,6 +51,7 @@ pub struct BlockServiceBuilder { chain_spec: Option>, graffiti: Option, graffiti_file: Option, + graffiti_policy: Option, } impl BlockServiceBuilder { @@ -63,6 +65,7 @@ impl BlockServiceBuilder { chain_spec: None, graffiti: None, graffiti_file: None, + graffiti_policy: None, } } @@ -106,6 +109,11 @@ impl BlockServiceBuilder { self } + pub fn graffiti_policy(mut self, graffiti_policy: Option) -> Self { + self.graffiti_policy = graffiti_policy; + self + } + pub fn build(self) -> Result, String> { Ok(BlockService { inner: Arc::new(Inner { @@ -127,6 +135,7 @@ impl BlockServiceBuilder { proposer_nodes: self.proposer_nodes, graffiti: self.graffiti, graffiti_file: self.graffiti_file, + graffiti_policy: self.graffiti_policy, }), }) } @@ -148,14 +157,13 @@ impl ProposerFallback { Err: Debug, { // If there are proposer nodes, try calling `func` on them and return early if they are successful. - if let Some(proposer_nodes) = &self.proposer_nodes { - if proposer_nodes + if let Some(proposer_nodes) = &self.proposer_nodes + && proposer_nodes .request(ApiTopic::Blocks, func.clone()) .await .is_ok() - { - return Ok(()); - } + { + return Ok(()); } // If the proposer nodes failed, try on the non-proposer nodes. @@ -193,6 +201,7 @@ pub struct Inner { chain_spec: Arc, graffiti: Option, graffiti_file: Option, + graffiti_policy: Option, } /// Attempts to produce attestations for any block producer(s) at the start of the epoch. @@ -299,7 +308,7 @@ impl BlockService { self.inner.executor.spawn( async move { let result = service - .publish_block(slot, validator_pubkey, builder_boost_factor) + .get_validator_block_and_publish_block(slot, validator_pubkey, builder_boost_factor) .await; match result { @@ -321,6 +330,7 @@ impl BlockService { } #[allow(clippy::too_many_arguments)] + #[instrument(skip_all, fields(%slot, ?validator_pubkey))] async fn sign_and_publish_block( &self, proposer_fallback: ProposerFallback, @@ -334,6 +344,7 @@ impl BlockService { let res = self .validator_store .sign_block(*validator_pubkey, unsigned_block, slot) + .instrument(info_span!("sign_block")) .await; let signed_block = match res { @@ -353,7 +364,7 @@ impl BlockService { return Err(BlockError::Recoverable(format!( "Unable to sign block: {:?}", e - ))) + ))); } }; @@ -390,7 +401,12 @@ impl BlockService { Ok(()) } - async fn publish_block( + #[instrument( + name = "block_proposal_duty_cycle", + skip_all, + fields(%slot, ?validator_pubkey) + )] + async fn get_validator_block_and_publish_block( self, slot: Slot, validator_pubkey: PublicKeyBytes, @@ -422,7 +438,7 @@ impl BlockService { return Err(BlockError::Recoverable(format!( "Unable to produce randao reveal signature: {:?}", e - ))) + ))); } }; @@ -443,103 +459,68 @@ impl BlockService { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request block from first responsive beacon node. + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. // - // Try the proposer nodes last, since it's likely that they don't have a + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a // great view of attestations on the network. - let unsigned_block = proposer_fallback + let ssz_block_response = proposer_fallback .request_proposers_last(|beacon_node| async move { let _get_timer = validator_metrics::start_timer_vec( &validator_metrics::BLOCK_SERVICE_TIMES, &[validator_metrics::BEACON_BLOCK_HTTP_GET], ); - Self::get_validator_block( - &beacon_node, - slot, - randao_reveal_ref, - graffiti, - proposer_index, - builder_boost_factor, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - }) + beacon_node + .get_validator_blocks_v3_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await }) - .await?; + .await; - self_ref - .sign_and_publish_block( - proposer_fallback, - slot, - graffiti, - &validator_pubkey, - unsigned_block, - ) - .await?; - - Ok(()) - } - - async fn publish_signed_block_contents( - &self, - signed_block: &SignedBlock, - beacon_node: BeaconNodeHttpClient, - ) -> Result<(), BlockError> { - match signed_block { - SignedBlock::Full(signed_block) => { - let _post_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_POST], + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" ); - beacon_node - .post_beacon_blocks_v2_ssz(signed_block, None) - .await - .or_else(|e| { - handle_block_post_error(e, signed_block.signed_block().message().slot()) - })? - } - SignedBlock::Blinded(signed_block) => { - let _post_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BLINDED_BEACON_BLOCK_HTTP_POST], - ); - beacon_node - .post_beacon_blinded_blocks_v2_ssz(signed_block, None) - .await - .or_else(|e| handle_block_post_error(e, signed_block.message().slot()))? - } - } - Ok::<_, BlockError>(()) - } - async fn get_validator_block( - beacon_node: &BeaconNodeHttpClient, - slot: Slot, - randao_reveal_ref: &SignatureBytes, - graffiti: Option, - proposer_index: Option, - builder_boost_factor: Option, - ) -> Result, BlockError> { - let (block_response, _) = beacon_node - .get_validator_blocks_v3::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response.data { + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + let (block_proposer, unsigned_block) = match block_response { eth2::types::ProduceBlockV3Response::Full(block) => { (block.block().proposer_index(), UnsignedBlock::Full(block)) } @@ -555,7 +536,53 @@ impl BlockService { )); } - Ok::<_, BlockError>(unsigned_block) + self_ref + .sign_and_publish_block( + proposer_fallback, + slot, + graffiti, + &validator_pubkey, + unsigned_block, + ) + .await?; + + Ok(()) + } + + #[instrument(skip_all)] + async fn publish_signed_block_contents( + &self, + signed_block: &SignedBlock, + beacon_node: BeaconNodeHttpClient, + ) -> Result<(), BlockError> { + match signed_block { + SignedBlock::Full(signed_block) => { + let _post_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_POST], + ); + beacon_node + .post_beacon_blocks_v2_ssz(signed_block, None) + .await + .map(|_| ()) + .or_else(|e| { + handle_block_post_error(e, signed_block.signed_block().message().slot()) + })? + } + SignedBlock::Blinded(signed_block) => { + let _post_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BLINDED_BEACON_BLOCK_HTTP_POST], + ); + + beacon_node + .post_beacon_blinded_blocks_v2_ssz(signed_block, None) + .await + .map(|_| ()) + .or_else(|e| handle_block_post_error(e, signed_block.message().slot()))?; + } + } + Ok::<_, BlockError>(()) } } diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index e04145f5f0..ff314de60f 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -7,43 +7,36 @@ //! block production. use crate::block_service::BlockServiceNotification; -use crate::sync::poll_sync_committee_duties; use crate::sync::SyncDutiesMap; +use crate::sync::poll_sync_committee_duties; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; +use bls::PublicKeyBytes; use eth2::types::{ - AttesterData, BeaconCommitteeSubscription, DutiesResponse, InclusionListDuty, ProposerData, - StateId, ValidatorId, + AttesterData, BeaconCommitteeSelection, BeaconCommitteeSubscription, DutiesResponse, + InclusionListDuty, ProposerData, StateId, ValidatorId, }; -use futures::{stream, StreamExt}; -use parking_lot::RwLock; +use futures::{ + StreamExt, + stream::{self, FuturesUnordered}, +}; +use parking_lot::{RwLock, RwLockWriteGuard}; use safe_arith::{ArithError, SafeArith}; use slot_clock::SlotClock; use std::cmp::min; -use std::collections::{hash_map, BTreeMap, HashMap, HashSet}; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::collections::{BTreeMap, HashMap, HashSet, hash_map}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use task_executor::TaskExecutor; use tokio::{sync::mpsc::Sender, time::sleep}; use tracing::{debug, error, info, warn}; -use types::{ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes, SelectionProof, Slot}; -use validator_metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; +use types::{ChainSpec, Epoch, EthSpec, Hash256, SelectionProof, Slot}; +use validator_metrics::{ATTESTATION_DUTY, get_int_gauge, set_int_gauge}; use validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; /// Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. const HISTORICAL_DUTIES_EPOCHS: u64 = 2; -/// Compute attestation selection proofs this many slots before they are required. -/// -/// At start-up selection proofs will be computed with less lookahead out of necessity. -const SELECTION_PROOF_SLOT_LOOKAHEAD: u64 = 8; - -/// The attestation selection proof lookahead for those running with the --distributed flag. -const SELECTION_PROOF_SLOT_LOOKAHEAD_DVT: u64 = 1; - -/// Fraction of a slot at which selection proof signing should happen (2 means half way). -const SELECTION_PROOF_SCHEDULE_DENOM: u32 = 2; - /// Minimum number of validators for which we auto-enable per-validator metrics. /// For validators greater than this value, we need to manually set the `enable-per-validator-metrics` /// flag in the cli to enable collection of per validator metrics. @@ -123,18 +116,97 @@ pub struct SubscriptionSlots { duty_slot: Slot, } +#[derive(Copy, Clone, Debug)] +pub struct SelectionProofConfig { + pub lookahead_slot: u64, + /// The seconds to compute the selection proof before a slot. + pub computation_offset: Duration, + /// Whether to call the selections endpoint, true for DVT with middleware. + pub selections_endpoint: bool, + /// Whether to sign the selection proof in parallel, true in distributed mode. + pub parallel_sign: bool, +} + +/// The default config for selection proofs covers the non-DVT case. +impl Default for SelectionProofConfig { + fn default() -> Self { + Self { + lookahead_slot: 0, + computation_offset: Duration::default(), + selections_endpoint: false, + parallel_sign: false, + } + } +} + /// Create a selection proof for `duty`. /// /// Return `Ok(None)` if the attesting validator is not an aggregator. -async fn make_selection_proof( +async fn make_selection_proof( duty: &AttesterData, validator_store: &S, spec: &ChainSpec, + beacon_nodes: &Arc>, + config: &SelectionProofConfig, ) -> Result, Error> { - let selection_proof = validator_store - .produce_selection_proof(duty.pubkey, duty.slot) - .await - .map_err(Error::FailedToProduceSelectionProof)?; + let selection_proof = if config.selections_endpoint { + let beacon_committee_selection = BeaconCommitteeSelection { + validator_index: duty.validator_index, + slot: duty.slot, + // This is partial selection proof + selection_proof: validator_store + .produce_selection_proof(duty.pubkey, duty.slot) + .await + .map_err(Error::FailedToProduceSelectionProof)? + .into(), + }; + // Call the endpoint /eth/v1/validator/beacon_committee_selections + // by sending the BeaconCommitteeSelection that contains partial selection proof + // The middleware should return BeaconCommitteeSelection that contains full selection proof + let middleware_response = beacon_nodes + .first_success(|beacon_node| { + let selection_data = beacon_committee_selection.clone(); + debug!( + "validator_index" = duty.validator_index, + "slot" = %duty.slot, + "partial selection proof" = ?beacon_committee_selection.selection_proof, + "Sending selection to middleware" + ); + async move { + beacon_node + .post_validator_beacon_committee_selections(&[selection_data]) + .await + } + }) + .await; + + let response_data = middleware_response + .map_err(|e| { + Error::FailedToProduceSelectionProof(ValidatorStoreError::Middleware(e.to_string())) + })? + .data + .pop() + .ok_or_else(|| { + Error::FailedToProduceSelectionProof(ValidatorStoreError::Middleware(format!( + "attestation selection proof - empty response for validator {}", + duty.validator_index + ))) + })?; + + debug!( + "validator_index" = response_data.validator_index, + "slot" = %response_data.slot, + // The selection proof from middleware response will be a full selection proof + "full selection proof" = ?response_data.selection_proof, + "Received selection from middleware" + ); + SelectionProof::from(response_data.selection_proof) + } else { + validator_store + .produce_selection_proof(duty.pubkey, duty.slot) + .await + .map_err(Error::FailedToProduceSelectionProof)? + }; selection_proof .is_aggregator(duty.committee_length as usize, spec) @@ -221,8 +293,10 @@ pub struct DutiesServiceBuilder { spec: Option>, //// Whether we permit large validator counts in the metrics. enable_high_validator_count_metrics: bool, - /// If this validator is running in distributed mode. - distributed: bool, + /// Create attestation selection proof config + attestation_selection_proof_config: SelectionProofConfig, + /// Create sync selection proof config + sync_selection_proof_config: SelectionProofConfig, disable_attesting: bool, } @@ -241,7 +315,8 @@ impl DutiesServiceBuilder { executor: None, spec: None, enable_high_validator_count_metrics: false, - distributed: false, + attestation_selection_proof_config: SelectionProofConfig::default(), + sync_selection_proof_config: SelectionProofConfig::default(), disable_attesting: false, } } @@ -279,8 +354,19 @@ impl DutiesServiceBuilder { self } - pub fn distributed(mut self, distributed: bool) -> Self { - self.distributed = distributed; + pub fn attestation_selection_proof_config( + mut self, + attestation_selection_proof_config: SelectionProofConfig, + ) -> Self { + self.attestation_selection_proof_config = attestation_selection_proof_config; + self + } + + pub fn sync_selection_proof_config( + mut self, + sync_selection_proof_config: SelectionProofConfig, + ) -> Self { + self.sync_selection_proof_config = sync_selection_proof_config; self } @@ -293,8 +379,8 @@ impl DutiesServiceBuilder { Ok(DutiesService { attesters: Default::default(), proposers: Default::default(), + sync_duties: SyncDutiesMap::new(self.sync_selection_proof_config), inclusion_list_duties: Default::default(), - sync_duties: SyncDutiesMap::new(self.distributed), validator_store: self .validator_store .ok_or("Cannot build DutiesService without validator_store")?, @@ -310,7 +396,7 @@ impl DutiesServiceBuilder { .ok_or("Cannot build DutiesService without executor")?, spec: self.spec.ok_or("Cannot build DutiesService without spec")?, enable_high_validator_count_metrics: self.enable_high_validator_count_metrics, - distributed: self.distributed, + selection_proof_config: self.attestation_selection_proof_config, disable_attesting: self.disable_attesting, }) } @@ -339,10 +425,10 @@ pub struct DutiesService { pub executor: TaskExecutor, /// The current chain spec. pub spec: Arc, - //// Whether we permit large validator counts in the metrics. + /// Whether we permit large validator counts in the metrics. pub enable_high_validator_count_metrics: bool, - /// If this validator is running in distributed mode. - pub distributed: bool, + /// Pass the config for distributed or non-distributed mode. + pub selection_proof_config: SelectionProofConfig, pub disable_attesting: bool, } @@ -1176,6 +1262,75 @@ async fn post_validator_duties_attester( + attesters: &mut RwLockWriteGuard, + result: Result<(AttesterData, Option), Error>, + dependent_root: Hash256, + current_slot: Slot, +) -> bool { + let (duty, selection_proof) = match result { + Ok(duty_and_proof) => duty_and_proof, + Err(Error::FailedToProduceSelectionProof(ValidatorStoreError::UnknownPubkey(pubkey))) => { + // A pubkey can be missing when a validator was recently removed via the API. + warn!( + info = "A validator may have recently been removed from this VC", + ?pubkey, + "Missing pubkey for duty and proof" + ); + // Do not abort the entire batch for a single failure. + // return true means continue processing duties. + return true; + } + Err(e) => { + error!( + error = ?e, + msg = "may impair attestation duties", + "Failed to produce duty and proof" + ); + return true; + } + }; + + let attester_map = attesters.entry(duty.pubkey).or_default(); + let epoch = duty.slot.epoch(S::E::slots_per_epoch()); + match attester_map.entry(epoch) { + hash_map::Entry::Occupied(mut entry) => { + // No need to update duties for which no proof was computed. + let Some(selection_proof) = selection_proof else { + return true; + }; + + let (existing_dependent_root, existing_duty) = entry.get_mut(); + + if *existing_dependent_root == dependent_root { + // Replace existing proof. + existing_duty.selection_proof = Some(selection_proof); + true + } else { + // Our selection proofs are no longer relevant due to a reorg, abandon this entire background process. + debug!( + reason = "re-org", + "Stopping selection proof background task" + ); + false + } + } + + hash_map::Entry::Vacant(entry) => { + // This probably shouldn't happen, but we have enough info to fill in the entry so we may as well. + let subscription_slots = SubscriptionSlots::new(duty.slot, current_slot); + let duty_and_proof = DutyAndProof { + duty, + selection_proof, + subscription_slots, + }; + entry.insert((dependent_root, duty_and_proof)); + true + } + } +} + /// Compute the attestation selection proofs for the `duties` and add them to the `attesters` map. /// /// Duties are computed in batches each slot. If a re-org is detected then the process will @@ -1195,26 +1350,33 @@ async fn fill_in_selection_proofs(); @@ -1227,87 +1389,69 @@ async fn fill_in_selection_proofs>() - .await; + // In distributed case, we want to send all partial selection proofs to the middleware to determine aggregation duties, + // as the middleware will need to have a threshold of partial selection proofs to be able to return the full selection proof + // Thus, sign selection proofs in parallel in distributed case; Otherwise, sign them serially in non-distributed (normal) case + if duties_service.selection_proof_config.parallel_sign { + let mut duty_and_proof_results = relevant_duties + .into_values() + .flatten() + .map(|duty| async { + let opt_selection_proof = make_selection_proof( + &duty, + duties_service.validator_store.as_ref(), + &duties_service.spec, + &duties_service.beacon_nodes, + &duties_service.selection_proof_config, + ) + .await?; + Ok((duty, opt_selection_proof)) + }) + .collect::>(); - // Add to attesters store. - let mut attesters = duties_service.attesters.write(); - for result in duty_and_proof_results { - let (duty, selection_proof) = match result { - Ok(duty_and_proof) => duty_and_proof, - Err(Error::FailedToProduceSelectionProof( - ValidatorStoreError::UnknownPubkey(pubkey), - )) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - warn!( - info = "a validator may have recently been removed from this VC", - ?pubkey, - "Missing pubkey for duty and proof" - ); - // Do not abort the entire batch for a single failure. - continue; - } - Err(e) => { - error!( - error = ?e, - msg = "may impair attestation duties", - "Failed to produce duty and proof" - ); - // Do not abort the entire batch for a single failure. - continue; - } - }; - - let attester_map = attesters.entry(duty.pubkey).or_default(); - let epoch = duty.slot.epoch(S::E::slots_per_epoch()); - match attester_map.entry(epoch) { - hash_map::Entry::Occupied(mut entry) => { - // No need to update duties for which no proof was computed. - let Some(selection_proof) = selection_proof else { - continue; - }; - - let (existing_dependent_root, existing_duty) = entry.get_mut(); - - if *existing_dependent_root == dependent_root { - // Replace existing proof. - existing_duty.selection_proof = Some(selection_proof); - } else { - // Our selection proofs are no longer relevant due to a reorg, abandon - // this entire background process. - debug!( - reason = "re-org", - "Stopping selection proof background task" - ); - return; - } - } - hash_map::Entry::Vacant(entry) => { - // This probably shouldn't happen, but we have enough info to fill in the - // entry so we may as well. - let subscription_slots = SubscriptionSlots::new(duty.slot, current_slot); - let duty_and_proof = DutyAndProof { - duty, - selection_proof, - subscription_slots, - }; - entry.insert((dependent_root, duty_and_proof)); + while let Some(result) = duty_and_proof_results.next().await { + let mut attesters = duties_service.attesters.write(); + // if process_duty_and_proof returns false, exit the loop + if !process_duty_and_proof::( + &mut attesters, + result, + dependent_root, + current_slot, + ) { + return; } } - } - drop(attesters); + } else { + // In normal (non-distributed case), sign selection proofs serially + let duty_and_proof_results = stream::iter(relevant_duties.into_values().flatten()) + .then(|duty| async { + let opt_selection_proof = make_selection_proof( + &duty, + duties_service.validator_store.as_ref(), + &duties_service.spec, + &duties_service.beacon_nodes, + &duties_service.selection_proof_config, + ) + .await?; + Ok((duty, opt_selection_proof)) + }) + .collect::>() + .await; + + // Add to attesters store. + let mut attesters = duties_service.attesters.write(); + for result in duty_and_proof_results { + if !process_duty_and_proof::( + &mut attesters, + result, + dependent_root, + current_slot, + ) { + return; + } + } + drop(attesters); + }; let time_taken_ms = Duration::from_secs_f64(timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); @@ -1656,15 +1800,14 @@ async fn poll_beacon_proposers( .proposers .write() .insert(current_epoch, (dependent_root, relevant_duties)) + && dependent_root != prior_dependent_root { - if dependent_root != prior_dependent_root { - warn!( - %prior_dependent_root, - %dependent_root, - msg = "this may happen from time to time", - "Proposer duties re-org" - ) - } + warn!( + %prior_dependent_root, + %dependent_root, + msg = "this may happen from time to time", + "Proposer duties re-org" + ) } } // Don't return early here, we still want to try and produce blocks using the cached values. @@ -1727,21 +1870,20 @@ async fn notify_block_production_service( .copied() .collect::>(); - if !non_doppelganger_proposers.is_empty() { - if let Err(e) = block_service_tx + if !non_doppelganger_proposers.is_empty() + && let Err(e) = block_service_tx .send(BlockServiceNotification { slot: current_slot, block_proposers: non_doppelganger_proposers, }) .await - { - error!( - %current_slot, - error = %e, - "Failed to notify block service" - ); - }; - } + { + error!( + %current_slot, + error = %e, + "Failed to notify block service" + ); + }; } #[cfg(test)] diff --git a/validator_client/validator_services/src/inclusion_list_service.rs b/validator_client/validator_services/src/inclusion_list_service.rs index 08423fbe38..b29f119bd6 100644 --- a/validator_client/validator_services/src/inclusion_list_service.rs +++ b/validator_client/validator_services/src/inclusion_list_service.rs @@ -6,11 +6,9 @@ use slot_clock::SlotClock; use std::ops::Deref; use std::sync::Arc; use task_executor::TaskExecutor; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; use tracing::{debug, error, info, trace, warn}; -use types::{ - inclusion_list, ChainSpec, EthSpec, InclusionList, InclusionListDuty, Slot, VariableList, -}; +use types::{ChainSpec, EthSpec, InclusionList, InclusionListDuty, Slot, Transactions}; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// Builds an `AttestationService`. @@ -281,11 +279,16 @@ impl InclusionListService = trimmed_il + .clone() + .try_into() + .map_err(|_| "Failed to create inclusion list".to_string())?; + // Create futures to produce signed `InclusionList` objects. let signing_futures = validator_duties.iter().map(|duty| { let inclusion_list = InclusionList { slot, - transactions: trimmed_il.clone().into(), + transactions: transactions.clone(), inclusion_list_committee_root: duty.committee_root, validator_index: duty.validator_index, }; diff --git a/validator_client/validator_services/src/notifier_service.rs b/validator_client/validator_services/src/notifier_service.rs index 6b8ea04edb..9c5f019c7a 100644 --- a/validator_client/validator_services/src/notifier_service.rs +++ b/validator_client/validator_services/src/notifier_service.rs @@ -2,7 +2,7 @@ use crate::duties_service::DutiesService; use slot_clock::SlotClock; use std::sync::Arc; use task_executor::TaskExecutor; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; use tracing::{debug, error, info}; use types::{ChainSpec, EthSpec}; use validator_metrics::set_gauge; @@ -35,7 +35,9 @@ pub fn spawn_notifier( } /// Performs a single notification routine. -async fn notify(duties_service: &DutiesService) { +pub async fn notify( + duties_service: &DutiesService, +) { let (candidate_info, num_available, num_synced) = duties_service.beacon_nodes.get_notifier_info().await; let num_total = candidate_info.len(); diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index b59e3266dc..063b11512f 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; use tracing::{debug, error, info, warn}; use types::{ Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData, diff --git a/validator_client/validator_services/src/sync.rs b/validator_client/validator_services/src/sync.rs index c13b70db80..0f456a7050 100644 --- a/validator_client/validator_services/src/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -1,19 +1,17 @@ -use crate::duties_service::{DutiesService, Error}; +use crate::duties_service::{DutiesService, Error, SelectionProofConfig}; +use bls::PublicKeyBytes; +use eth2::types::SyncCommitteeSelection; use futures::future::join_all; +use futures::stream::{FuturesUnordered, StreamExt}; use logging::crit; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use tracing::{debug, info, warn}; -use types::{ChainSpec, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId}; +use tracing::{debug, error, info, warn}; +use types::{ChainSpec, EthSpec, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId}; use validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; -/// Number of epochs in advance to compute selection proofs when not in `distributed` mode. -pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; -/// Number of slots in advance to compute selection proofs when in `distributed` mode. -pub const AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED: u64 = 1; - /// Top-level data-structure containing sync duty information. /// /// This data is structured as a series of nested `HashMap`s wrapped in `RwLock`s. Fine-grained @@ -30,7 +28,7 @@ pub struct SyncDutiesMap { /// Map from sync committee period to duties for members of that sync committee. committees: RwLock>, /// Whether we are in `distributed` mode and using reduced lookahead for aggregate pre-compute. - distributed: bool, + pub selection_proof_config: SelectionProofConfig, } /// Duties for a single sync committee period. @@ -79,10 +77,10 @@ pub struct SlotDuties { } impl SyncDutiesMap { - pub fn new(distributed: bool) -> Self { + pub fn new(selection_proof_config: SelectionProofConfig) -> Self { Self { committees: RwLock::new(HashMap::new()), - distributed, + selection_proof_config, } } @@ -99,15 +97,6 @@ impl SyncDutiesMap { }) } - /// Number of slots in advance to compute selection proofs - fn aggregation_pre_compute_slots(&self) -> u64 { - if self.distributed { - AGGREGATION_PRE_COMPUTE_SLOTS_DISTRIBUTED - } else { - E::slots_per_epoch() * AGGREGATION_PRE_COMPUTE_EPOCHS - } - } - /// Prepare for pre-computation of selection proofs for `committee_period`. /// /// Return the slot up to which proofs should be pre-computed, as well as a vec of @@ -123,7 +112,7 @@ impl SyncDutiesMap { current_slot, first_slot_of_period::(committee_period, spec), ); - let pre_compute_lookahead_slots = self.aggregation_pre_compute_slots::(); + let pre_compute_lookahead_slots = self.selection_proof_config.lookahead_slot; let pre_compute_slot = std::cmp::min( current_slot + pre_compute_lookahead_slots, last_slot_of_period::(committee_period, spec), @@ -377,7 +366,7 @@ pub async fn poll_sync_committee_duties(); + let aggregate_pre_compute_lookahead_slots = sync_duties.selection_proof_config.lookahead_slot; if (current_slot + aggregate_pre_compute_lookahead_slots) .epoch(S::E::slots_per_epoch()) .sync_committee_period(spec)? @@ -498,6 +487,114 @@ pub async fn poll_sync_committee_duties_for_period( + duties_service: &Arc>, + duty: &SyncDuty, + proof_slot: Slot, + subnet_id: SyncSubnetId, +) -> Option { + let sync_selection_proof = duties_service + .validator_store + .produce_sync_selection_proof(&duty.pubkey, proof_slot, subnet_id) + .await; + + let selection_proof = match sync_selection_proof { + Ok(proof) => proof, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently removed via the API + debug!( + ?pubkey, + "slot" = %proof_slot, + "Missing pubkey for sync selection proof"); + return None; + } + Err(e) => { + warn!( + "error" = ?e, + "pubkey" = ?duty.pubkey, + "slot" = %proof_slot, + "Unable to sign selection proof" + ); + return None; + } + }; + + // In DVT with middleware, when we want to call the selections endpoint + if duties_service + .sync_duties + .selection_proof_config + .selections_endpoint + { + debug!( + "validator_index" = duty.validator_index, + "slot" = %proof_slot, + "subcommittee_index" = *subnet_id, + // This is partial selection proof + "partial selection proof" = ?selection_proof, + "Sending sync selection to middleware" + ); + + let sync_committee_selection = SyncCommitteeSelection { + validator_index: duty.validator_index, + slot: proof_slot, + subcommittee_index: *subnet_id, + selection_proof: selection_proof.clone().into(), + }; + + // Call the endpoint /eth/v1/validator/sync_committee_selections + // by sending the SyncCommitteeSelection that contains partial sync selection proof + // The middleware should return SyncCommitteeSelection that contains full sync selection proof + let middleware_response = duties_service + .beacon_nodes + .first_success(|beacon_node| { + let selection_data = sync_committee_selection.clone(); + async move { + beacon_node + .post_validator_sync_committee_selections(&[selection_data]) + .await + } + }) + .await; + + match middleware_response { + Ok(mut response) => { + let Some(response_data) = response.data.pop() else { + error!( + validator_index = duty.validator_index, + slot = %proof_slot, + "Empty response from sync selection middleware", + ); + return None; + }; + debug!( + "validator_index" = response_data.validator_index, + "slot" = %response_data.slot, + "subcommittee_index" = response_data.subcommittee_index, + // The selection proof from middleware response will be a full selection proof + "full selection proof" = ?response_data.selection_proof, + "Received sync selection from middleware" + ); + + // Convert the response to a SyncSelectionProof + let full_selection_proof = SyncSelectionProof::from(response_data.selection_proof); + Some(full_selection_proof) + } + Err(e) => { + error!( + "error" = %e, + %proof_slot, + "Failed to get sync selection proofs from middleware" + ); + None + } + } + } else { + // In non-distributed mode, the selection_proof is already a full selection proof + Some(selection_proof) + } +} + pub async fn fill_in_aggregation_proofs( duties_service: Arc>, pre_compute_duties: &[(Slot, SyncDuty)], @@ -505,127 +602,193 @@ pub async fn fill_in_aggregation_proofs() { - Ok(subnet_ids) => subnet_ids, - Err(e) => { - crit!( - error = ?e, - "Arithmetic error computing subnet IDs" - ); - continue; - } - }; - - // Create futures to produce proofs. - let duties_service_ref = &duties_service; - let futures = subnet_ids.iter().map(|subnet_id| async move { - // Construct proof for prior slot. - let proof_slot = slot - 1; - - let proof = match duties_service_ref - .validator_store - .produce_sync_selection_proof(&duty.pubkey, proof_slot, *subnet_id) - .await - { - Ok(proof) => proof, - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently - // removed via the API. - debug!( - ?pubkey, - pubkey = ?duty.pubkey, - slot = %proof_slot, - "Missing pubkey for sync selection proof" - ); - return None; - } + for (_, duty) in pre_compute_duties { + let subnet_ids = match duty.subnet_ids::() { + Ok(subnet_ids) => subnet_ids, Err(e) => { - warn!( - error = ?e, - pubkey = ?duty.pubkey, - slot = %proof_slot, - "Unable to sign selection proof" + crit!( + "error" = ?e, + "Arithmetic error computing subnet IDs" ); - return None; + continue; } }; + // Construct proof for prior slot. + let proof_slot = slot - 1; + + // Calling the make_sync_selection_proof will return a full selection proof + for &subnet_id in &subnet_ids { + let duties_service = duties_service.clone(); + futures_unordered.push(async move { + let result = + make_sync_selection_proof(&duties_service, duty, proof_slot, subnet_id) + .await; + + result.map(|proof| (duty.validator_index, proof_slot, subnet_id, proof)) + }); + } + } + + while let Some(result) = futures_unordered.next().await { + let Some((validator_index, proof_slot, subnet_id, proof)) = result else { + continue; + }; + let sync_map = duties_service.sync_duties.committees.read(); + let Some(committee_duties) = sync_map.get(&sync_committee_period) else { + debug!("period" = sync_committee_period, "Missing sync duties"); + continue; + }; + + let validators = committee_duties.validators.read(); + + // Check if the validator is an aggregator match proof.is_aggregator::() { Ok(true) => { - debug!( - validator_index = duty.validator_index, - slot = %proof_slot, - %subnet_id, - "Validator is sync aggregator" - ); - Some(((proof_slot, *subnet_id), proof)) + if let Some(Some(duty)) = validators.get(&validator_index) { + debug!( + validator_index, + "slot" = %proof_slot, + "subcommittee_index" = *subnet_id, + // log full selection proof for debugging + "full selection proof" = ?proof, + "Validator is sync aggregator" + ); + + // Store the proof + duty.aggregation_duties + .proofs + .write() + .insert((proof_slot, subnet_id), proof); + } } - Ok(false) => None, + Ok(false) => {} // Not an aggregator Err(e) => { warn!( - pubkey = ?duty.pubkey, - slot = %proof_slot, - error = ?e, + validator_index, + %slot, + "error" = ?e, "Error determining is_aggregator" ); - None } } - }); + } + } else { + // For non-distributed mode + debug!( + period = sync_committee_period, + %current_slot, + %pre_compute_slot, + "Calculating sync selection proofs" + ); - // Execute all the futures in parallel, collecting any successful results. - let proofs = join_all(futures) - .await - .into_iter() - .flatten() - .collect::>(); + let mut validator_proofs = vec![]; + for (validator_start_slot, duty) in pre_compute_duties { + // Proofs are already known at this slot for this validator. + if slot < *validator_start_slot { + continue; + } - validator_proofs.push((duty.validator_index, proofs)); - } + let subnet_ids = match duty.subnet_ids::() { + Ok(subnet_ids) => subnet_ids, + Err(e) => { + crit!( + error = ?e, + "Arithmetic error computing subnet IDs" + ); + continue; + } + }; - // Add to global storage (we add regularly so the proofs can be used ASAP). - let sync_map = duties_service.sync_duties.committees.read(); - let Some(committee_duties) = sync_map.get(&sync_committee_period) else { - debug!(period = sync_committee_period, "Missing sync duties"); - continue; - }; - let validators = committee_duties.validators.read(); - let num_validators_updated = validator_proofs.len(); + // Create futures to produce proofs. + let duties_service_ref = &duties_service; + let futures = subnet_ids.iter().map(|subnet_id| async move { + // Construct proof for prior slot. + let proof_slot = slot - 1; - for (validator_index, proofs) in validator_proofs { - if let Some(Some(duty)) = validators.get(&validator_index) { - duty.aggregation_duties.proofs.write().extend(proofs); - } else { + let proof = + make_sync_selection_proof(duties_service_ref, duty, proof_slot, *subnet_id) + .await; + + match proof { + Some(proof) => match proof.is_aggregator::() { + Ok(true) => { + debug!( + validator_index = duty.validator_index, + slot = %proof_slot, + %subnet_id, + "Validator is sync aggregator" + ); + Some(((proof_slot, *subnet_id), proof)) + } + Ok(false) => None, + Err(e) => { + warn!( + pubkey = ?duty.pubkey, + slot = %proof_slot, + error = ?e, + "Error determining is_aggregator" + ); + None + } + }, + + None => None, + } + }); + + // Execute all the futures in parallel, collecting any successful results. + let proofs = join_all(futures) + .await + .into_iter() + .flatten() + .collect::>(); + + validator_proofs.push((duty.validator_index, proofs)); + } + + // Add to global storage (we add regularly so the proofs can be used ASAP). + let sync_map = duties_service.sync_duties.committees.read(); + let Some(committee_duties) = sync_map.get(&sync_committee_period) else { + debug!(period = sync_committee_period, "Missing sync duties"); + continue; + }; + let validators = committee_duties.validators.read(); + let num_validators_updated = validator_proofs.len(); + + for (validator_index, proofs) in validator_proofs { + if let Some(Some(duty)) = validators.get(&validator_index) { + duty.aggregation_duties.proofs.write().extend(proofs); + } else { + debug!( + validator_index, + period = sync_committee_period, + "Missing sync duty to update" + ); + } + } + + if num_validators_updated > 0 { debug!( - validator_index, - period = sync_committee_period, - "Missing sync duty to update" + %slot, + updated_validators = num_validators_updated, + "Finished computing sync selection proofs" ); } } - - if num_validators_updated > 0 { - debug!( - %slot, - updated_validators = num_validators_updated, - "Finished computing sync selection proofs" - ); - } } } diff --git a/validator_client/validator_services/src/sync_committee_service.rs b/validator_client/validator_services/src/sync_committee_service.rs index be9e2918a4..28c3d1caad 100644 --- a/validator_client/validator_services/src/sync_committee_service.rs +++ b/validator_client/validator_services/src/sync_committee_service.rs @@ -1,20 +1,21 @@ use crate::duties_service::DutiesService; use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; +use bls::PublicKeyBytes; use eth2::types::BlockId; -use futures::future::join_all; use futures::future::FutureExt; +use futures::future::join_all; use logging::crit; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use task_executor::TaskExecutor; -use tokio::time::{sleep, sleep_until, Duration, Instant}; -use tracing::{debug, error, info, trace, warn}; +use tokio::time::{Duration, Instant, sleep, sleep_until}; +use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; use types::{ - ChainSpec, EthSpec, Hash256, PublicKeyBytes, Slot, SyncCommitteeSubscription, - SyncContributionData, SyncDuty, SyncSelectionProof, SyncSubnetId, + ChainSpec, EthSpec, Hash256, Slot, SyncCommitteeSubscription, SyncContributionData, SyncDuty, + SyncSelectionProof, SyncSubnetId, }; use validator_store::{Error as ValidatorStoreError, ValidatorStore}; @@ -208,7 +209,8 @@ impl SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService SyncCommitteeService"] [dependencies] +bls = { workspace = true } eth2 = { workspace = true } slashing_protection = { workspace = true } types = { workspace = true } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 9183747004..dbbb3fb839 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,3 +1,4 @@ +use bls::{PublicKeyBytes, Signature}; use eth2::types::{FullBlockContents, PublishBlockRequest}; use slashing_protection::NotSafe; use std::fmt::Debug; @@ -5,10 +6,10 @@ use std::future::Future; use std::sync::Arc; use types::{ Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - InclusionList, PublicKeyBytes, SelectionProof, Signature, SignedAggregateAndProof, - SignedBlindedBeaconBlock, SignedContributionAndProof, SignedInclusionList, - SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, + InclusionList, SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, + SignedContributionAndProof, SignedInclusionList, SignedValidatorRegistrationData, Slot, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +23,7 @@ pub enum Error { GreaterThanCurrentEpoch { epoch: Epoch, current_epoch: Epoch }, UnableToSignAttestation(AttestationError), SpecificError(T), + Middleware(String), } impl From for Error { diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 7cb05616f4..16ce1e023f 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -6,9 +6,10 @@ edition = { workspace = true } [dependencies] account_utils = { workspace = true } +bls = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } -derivative = { workspace = true } +educe = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } eth2_network_config = { workspace = true } @@ -17,12 +18,15 @@ ethereum_serde_utils = { workspace = true } hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +slot_clock = { workspace = true } tokio = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } zeroize = { workspace = true } [dev-dependencies] +beacon_chain = { workspace = true } +http_api = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } validator_http_api = { workspace = true } diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index cc4157990f..a95d2a1fd6 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -1,13 +1,14 @@ -use account_utils::strip_off_newlines; pub use account_utils::STDIN_INPUTS_FLAG; +use account_utils::strip_off_newlines; +use bls::{Keypair, PublicKeyBytes, SignatureBytes}; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ + SensitiveUrl, lighthouse_vc::{ http_client::ValidatorClientHttpClient, std_types::{ImportKeystoreStatus, ImportKeystoresRequest, SingleKeystoreResponse, Status}, types::UpdateFeeRecipientRequest, }, - SensitiveUrl, }; use serde::{Deserialize, Serialize}; use std::fs; @@ -19,9 +20,9 @@ use zeroize::Zeroizing; pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; pub const COUNT_FLAG: &str = "count"; -/// When the `ethereum/staking-deposit-cli` tool generates deposit data JSON, it adds a +/// When the `ethstaker-deposit-cli` tool generates deposit data JSON, it adds a /// `deposit_cli_version` to protect the web-based "Launchpad" tool against a breaking change that -/// was introduced in `ethereum/staking-deposit-cli`. Lighthouse don't really have a version that it +/// was introduced in `ethstaker-deposit-cli`. Lighthouse don't really have a version that it /// can use here, so we choose a static string that is: /// /// 1. High enough that it's accepted by Launchpad. @@ -163,12 +164,12 @@ pub struct CreateSpec { pub validators: Vec, } -/// The structure generated by the `staking-deposit-cli` which has become a quasi-standard for +/// The structure generated by the `ethstaker-deposit-cli` which has become a quasi-standard for /// browser-based deposit submission tools (e.g., the Ethereum Launchpad and Lido). /// /// We assume this code as the canonical definition: /// -/// https://github.com/ethereum/staking-deposit-cli/blob/76ed78224fdfe3daca788d12442b3d1a37978296/staking_deposit/credentials.py#L131-L144 +/// https://github.com/eth-educators/ethstaker-deposit-cli/blob/80d536374de838ccae142974ed0e747b46beb030/ethstaker_deposit/credentials.py#L164-L177 #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct StandardDepositDataJson { #[serde(with = "public_key_bytes_without_0x_prefix")] diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index 07578033cd..8682705956 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -1,12 +1,13 @@ use super::common::*; use crate::DumpConfig; use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; +use bls::PublicKeyBytes; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; use eth2::{ + BeaconNodeHttpClient, SensitiveUrl, Timeouts, lighthouse_vc::std_types::KeystoreJsonStr, types::{StateId, ValidatorId}, - BeaconNodeHttpClient, SensitiveUrl, Timeouts, }; use eth2_wallet::WalletBuilder; use serde::{Deserialize, Serialize}; @@ -43,7 +44,7 @@ pub fn cli_app() -> Command { contains all the validator keystores and other validator data. This file can then \ be imported to a validator client using the \"import-validators\" command. \ Another, optional JSON file is created which contains a list of validator \ - deposits in the same format as the \"ethereum/staking-deposit-cli\" tool.", + deposits in the same format as the \"ethstaker-deposit-cli\" tool.", ) .arg( Arg::new(OUTPUT_PATH_FLAG) @@ -439,17 +440,16 @@ impl ValidatorsAndDeposits { different validator clients. If you understand the risks and are certain you \ wish to generate this validator again, omit the --{} flag.", voting_public_key, derivation_index, BEACON_NODE_FLAG - ))? + ))?; + } + Ok(None) => { + eprintln!("{:?} was not found in the beacon chain", voting_public_key) } - Ok(None) => eprintln!( - "{:?} was not found in the beacon chain", - voting_public_key - ), Err(e) => { return Err(format!( "Error checking if validator exists in beacon chain: {:?}", e - )) + )); } } } @@ -487,7 +487,7 @@ impl ValidatorsAndDeposits { }; // Create a JSON structure equivalent to the one generated by - // `ethereum/staking-deposit-cli`. + // `ethstaker-deposit-cli`. let json_deposit = StandardDepositDataJson::new( &voting_keypair, withdrawal_credentials.into(), @@ -587,16 +587,17 @@ async fn run(config: CreateConfig, spec: &ChainSpec) -> Result<(), S #[cfg(test)] pub mod tests { use super::*; + use bls::SignatureBytes; use eth2_network_config::Eth2NetworkConfig; use regex::Regex; use std::path::Path; use std::str::FromStr; - use tempfile::{tempdir, TempDir}; + use tempfile::{TempDir, tempdir}; use tree_hash::TreeHash; type E = MainnetEthSpec; - const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "2.7.0"; + const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "1.2.2"; // Update to ethstaker-deposit-cli version fn junk_execution_address() -> Option
{ Some(Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap()) @@ -882,7 +883,7 @@ pub mod tests { } #[tokio::test] - async fn staking_deposit_cli_vectors() { + async fn ethstaker_deposit_cli_vectors() { let vectors_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("test_vectors") .join("vectors"); diff --git a/validator_manager/src/delete_validators.rs b/validator_manager/src/delete_validators.rs index 5ef647c5af..2421b002aa 100644 --- a/validator_manager/src/delete_validators.rs +++ b/validator_manager/src/delete_validators.rs @@ -1,13 +1,13 @@ +use bls::PublicKeyBytes; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::{ - lighthouse_vc::types::{DeleteKeystoreStatus, DeleteKeystoresRequest}, SensitiveUrl, + lighthouse_vc::types::{DeleteKeystoreStatus, DeleteKeystoresRequest}, }; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use types::PublicKeyBytes; -use crate::{common::vc_http_client, DumpConfig}; +use crate::{DumpConfig, common::vc_http_client}; pub const CMD: &str = "delete"; pub const VC_URL_FLAG: &str = "vc-url"; @@ -45,7 +45,10 @@ pub fn cli_app() -> Command { Arg::new(VALIDATOR_FLAG) .long(VALIDATOR_FLAG) .value_name("STRING") - .help("Comma-separated list of validators (pubkey) that will be deleted.") + .help( + "Comma-separated list of validators (pubkey) that will be deleted. \ + To delete all validators, use the keyword \"all\".", + ) .action(ArgAction::Set) .required(true) .display_order(0), @@ -64,10 +67,14 @@ impl DeleteConfig { let validators_to_delete_str = clap_utils::parse_required::(matches, VALIDATOR_FLAG)?; - let validators_to_delete = validators_to_delete_str - .split(',') - .map(|s| s.trim().parse()) - .collect::, _>>()?; + let validators_to_delete = if validators_to_delete_str.trim() == "all" { + Vec::new() + } else { + validators_to_delete_str + .split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + }; Ok(Self { vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, @@ -90,11 +97,16 @@ async fn run(config: DeleteConfig) -> Result<(), String> { let DeleteConfig { vc_url, vc_token_path, - validators_to_delete, + mut validators_to_delete, } = config; let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; + // Delete all validators on the VC + if validators_to_delete.is_empty() { + validators_to_delete = validators.iter().map(|v| v.validating_pubkey).collect(); + } + for validator_to_delete in &validators_to_delete { if !validators .iter() @@ -148,7 +160,7 @@ mod test { use crate::{ common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, }; - use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + use validator_http_api::{Config as HttpConfig, test_utils::ApiTester}; struct TestBuilder { delete_config: Option, diff --git a/validator_manager/src/exit_validators.rs b/validator_manager/src/exit_validators.rs new file mode 100644 index 0000000000..b53d9c0a16 --- /dev/null +++ b/validator_manager/src/exit_validators.rs @@ -0,0 +1,589 @@ +use crate::{DumpConfig, common::vc_http_client}; + +use bls::PublicKeyBytes; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap_utils::FLAG_HEADER; +use eth2::types::{ConfigAndPreset, Epoch, StateId, ValidatorId, ValidatorStatus}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use serde::{Deserialize, Serialize}; +use serde_json; +use slot_clock::{SlotClock, SystemTimeSlotClock}; +use std::fs::write; +use std::path::PathBuf; +use std::time::Duration; +use types::{ChainSpec, EthSpec}; + +pub const CMD: &str = "exit"; +pub const BEACON_URL_FLAG: &str = "beacon-node"; +pub const VC_URL_FLAG: &str = "vc-url"; +pub const VC_TOKEN_FLAG: &str = "vc-token"; +pub const VALIDATOR_FLAG: &str = "validators"; +pub const EXIT_EPOCH_FLAG: &str = "exit-epoch"; +pub const PRESIGN_FLAG: &str = "presign"; + +pub fn cli_app() -> Command { + Command::new(CMD) + .about( + "Exits one or more validators using the HTTP API. It can \ + also be used to generate a presigned voluntary exit message for a particular future epoch.", + ) + .arg( + Arg::new(BEACON_URL_FLAG) + .long(BEACON_URL_FLAG) + .value_name("NETWORK_ADDRESS") + .help("Address to a beacon node HTTP API") + .action(ArgAction::Set) + .display_order(0) + .conflicts_with(PRESIGN_FLAG), + ) + .arg( + Arg::new(VC_URL_FLAG) + .long(VC_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help("A HTTP(S) address of a validator client using the keymanager-API.") + .default_value("http://localhost:5062") + .requires(VC_TOKEN_FLAG) + .action(ArgAction::Set) + .display_order(0), + ) + .arg( + Arg::new(VC_TOKEN_FLAG) + .long(VC_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the validator client.") + .action(ArgAction::Set) + .display_order(0), + ) + .arg( + Arg::new(VALIDATOR_FLAG) + .long(VALIDATOR_FLAG) + .value_name("STRING") + .help( + "Comma-separated list of validators (pubkey) to exit. \ + To exit all validators, use the keyword \"all\".", + ) + .action(ArgAction::Set) + .required(true) + .display_order(0), + ) + .arg( + Arg::new(EXIT_EPOCH_FLAG) + .long(EXIT_EPOCH_FLAG) + .value_name("EPOCH") + .help( + "Provide the minimum epoch for processing voluntary exit. \ + This flag is required to be used in combination with `--presign` to \ + save the voluntary exit presign to a file for future use.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(PRESIGN_FLAG) + .conflicts_with(BEACON_URL_FLAG), + ) + .arg( + Arg::new(PRESIGN_FLAG) + .long(PRESIGN_FLAG) + .help( + "Generate the voluntary exit presign and save it to a file \ + named {validator_pubkey}.json. Note: Using this without the \ + `--beacon-node` flag will not publish the voluntary exit to the network.", + ) + .help_heading(FLAG_HEADER) + .action(ArgAction::SetTrue) + .display_order(0) + .conflicts_with(BEACON_URL_FLAG), + ) +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct ExitConfig { + pub vc_url: SensitiveUrl, + pub vc_token_path: PathBuf, + pub validators_to_exit: Vec, + pub beacon_url: Option, + pub exit_epoch: Option, + pub presign: bool, +} + +impl ExitConfig { + fn from_cli(matches: &ArgMatches) -> Result { + let validators_to_exit_str = clap_utils::parse_required::(matches, VALIDATOR_FLAG)?; + + // Keyword "all" to exit all validators, vector to be created later + let validators_to_exit = if validators_to_exit_str.trim() == "all" { + Vec::new() + } else { + validators_to_exit_str + .split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + }; + + Ok(Self { + vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, + validators_to_exit, + beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?, + exit_epoch: clap_utils::parse_optional(matches, EXIT_EPOCH_FLAG)?, + presign: matches.get_flag(PRESIGN_FLAG), + }) + } +} + +pub async fn cli_run( + matches: &ArgMatches, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = ExitConfig::from_cli(matches)?; + + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run::(config).await + } +} + +async fn run(config: ExitConfig) -> Result<(), String> { + let ExitConfig { + vc_url, + vc_token_path, + mut validators_to_exit, + beacon_url, + exit_epoch, + presign, + } = config; + + let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; + + if validators_to_exit.is_empty() { + validators_to_exit = validators.iter().map(|v| v.validating_pubkey).collect(); + } + + for validator_to_exit in validators_to_exit { + // Check that the validators_to_exit is in the validator client + if !validators + .iter() + .any(|validator| validator.validating_pubkey == validator_to_exit) + { + return Err(format!("Validator {} doesn't exist", validator_to_exit)); + } + + let exit_message = http_client + .post_validator_voluntary_exit(&validator_to_exit, exit_epoch) + .await + .map_err(|e| format!("Failed to generate voluntary exit message: {}", e))?; + + if presign { + let exit_message_json = serde_json::to_string(&exit_message.data); + + match exit_message_json { + Ok(json) => { + // Save the exit message to JSON file(s) + let file_path = format!("{}.json", validator_to_exit); + write(&file_path, json).map_err(|e| { + format!("Failed to write voluntary exit message to file: {}", e) + })?; + println!("Voluntary exit message saved to {}", file_path); + } + Err(e) => eprintln!("Failed to serialize voluntary exit message: {}", e), + } + } + + // Only publish the voluntary exit if the --beacon-node flag is present + if let Some(ref beacon_url) = beacon_url { + let beacon_node = BeaconNodeHttpClient::new( + beacon_url.clone(), + Timeouts::set_all(Duration::from_secs(12)), + ); + + if beacon_node + .get_node_syncing() + .await + .map_err(|e| format!("Failed to get beacon node sync status: {:?}", e))? + .data + .is_syncing + { + return Err( + "Beacon node is syncing, submit the voluntary exit later when beacon node is synced" + .to_string(), + ); + } + + let genesis_data = beacon_node + .get_beacon_genesis() + .await + .map_err(|e| format!("Failed to get genesis data: {}", e))? + .data; + + let config_and_preset = beacon_node + .get_config_spec::() + .await + .map_err(|e| format!("Failed to get config spec: {}", e))? + .data; + + let spec = ChainSpec::from_config::(config_and_preset.config()) + .ok_or("Failed to create chain spec")?; + + let validator_data = beacon_node + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(validator_to_exit), + ) + .await + .map_err(|e| format!("Failed to get validator details: {:?}", e))? + .ok_or_else(|| { + format!( + "Validator {} is not present in the beacon state. \ + Please ensure that your beacon node is synced \ + and the validator has been deposited.", + validator_to_exit + ) + })? + .data; + + let activation_epoch = validator_data.validator.activation_epoch; + let current_epoch = get_current_epoch::(genesis_data.genesis_time, &spec) + .ok_or("Failed to get current epoch. Please check your system time")?; + + // Check if validator is eligible for exit + if validator_data.status == ValidatorStatus::ActiveOngoing + && current_epoch < activation_epoch + spec.shard_committee_period + { + eprintln!( + "Validator {} is not eligible for exit. It will become eligible at epoch {}", + validator_to_exit, + activation_epoch + spec.shard_committee_period + ) + } else if validator_data.status != ValidatorStatus::ActiveOngoing { + eprintln!( + "Validator {} is not eligible for exit. Validator status is: {:?}", + validator_to_exit, validator_data.status + ) + } else { + // Only publish voluntary exit if validator status is ActiveOngoing + beacon_node + .post_beacon_pool_voluntary_exits(&exit_message.data) + .await + .map_err(|e| format!("Failed to publish voluntary exit: {}", e))?; + eprintln!( + "Successfully validated and published voluntary exit for validator {}", + validator_to_exit + ); + } + } + } + + Ok(()) +} + +pub fn get_current_epoch(genesis_time: u64, spec: &ChainSpec) -> Option { + let slot_clock = SystemTimeSlotClock::new( + spec.genesis_slot, + Duration::from_secs(genesis_time), + Duration::from_secs(spec.seconds_per_slot), + ); + slot_clock.now().map(|s| s.epoch(E::slots_per_epoch())) +} + +#[cfg(not(debug_assertions))] +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, + }; + use account_utils::eth2_keystore::KeystoreBuilder; + use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; + use eth2::lighthouse_vc::types::KeystoreJsonStr; + use http_api::test_utils::InteractiveTester; + use std::{ + fs::{self, File}, + io::Write, + sync::Arc, + }; + use types::{ChainSpec, MainnetEthSpec}; + use validator_http_api::{Config as HttpConfig, test_utils::ApiTester}; + use zeroize::Zeroizing; + type E = MainnetEthSpec; + + struct TestBuilder { + exit_config: Option, + src_import_builder: Option, + http_config: HttpConfig, + vc_token: Option, + validators: Vec, + beacon_node: InteractiveTester, + index_of_validators_to_exit: Vec, + spec: Arc, + } + + impl TestBuilder { + async fn new() -> Self { + let mut spec = ChainSpec::mainnet(); + spec.shard_committee_period = 1; + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(1)); + spec.capella_fork_epoch = Some(Epoch::new(2)); + spec.deneb_fork_epoch = Some(Epoch::new(3)); + + let beacon_node = InteractiveTester::new(Some(spec.clone()), 64).await; + + let harness = &beacon_node.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + let execution_ctx = mock_el.server.ctx.clone(); + + // Move to terminal block. + mock_el.server.all_payloads_valid(); + execution_ctx + .execution_block_generator + .write() + .move_to_terminal_block() + .unwrap(); + + Self { + exit_config: None, + src_import_builder: None, + http_config: ApiTester::default_http_config(), + vc_token: None, + validators: vec![], + beacon_node, + index_of_validators_to_exit: vec![], + spec: spec.into(), + } + } + + async fn with_validators(mut self, index_of_validators_to_exit: Vec) -> Self { + // Ensure genesis validators root matches the beacon node. + let genesis_validators_root = self + .beacon_node + .harness + .get_current_state() + .genesis_validators_root(); + // And use a single slot clock and same spec for BN and VC to keep things simple. + let slot_clock = self.beacon_node.harness.chain.slot_clock.clone(); + let vc = ApiTester::new_with_options( + self.http_config.clone(), + slot_clock, + genesis_validators_root, + self.spec.clone(), + ) + .await; + let mut builder = ImportTestBuilder::new_with_vc(vc).await; + + self.vc_token = + Some(fs::read_to_string(builder.get_import_config().vc_token_path).unwrap()); + + let local_validators: Vec = index_of_validators_to_exit + .iter() + .map(|&index| { + let keystore = KeystoreBuilder::new( + &self.beacon_node.harness.validator_keypairs[index], + "password".as_bytes(), + "".into(), + ) + .unwrap() + .build() + .unwrap(); + + ValidatorSpecification { + voting_keystore: KeystoreJsonStr(keystore), + voting_keystore_password: Zeroizing::new("password".into()), + slashing_protection: None, + fee_recipient: None, + gas_limit: None, + builder_proposals: None, + builder_boost_factor: None, + prefer_builder_proposals: None, + enabled: Some(true), + } + }) + .collect(); + + let beacon_url = self.beacon_node.client.server().clone(); + + let validators_to_exit = index_of_validators_to_exit + .iter() + .map(|&index| { + self.beacon_node.harness.validator_keypairs[index] + .pk + .clone() + .into() + }) + .collect(); + + let import_config = builder.get_import_config(); + + let validators_dir = import_config.vc_token_path.parent().unwrap(); + let validators_file = validators_dir.join("validators.json"); + + builder = builder.mutate_import_config(|config| { + config.validators_file_path = Some(validators_file.clone()); + }); + + fs::write( + &validators_file, + serde_json::to_string(&local_validators).unwrap(), + ) + .unwrap(); + + self.exit_config = Some(ExitConfig { + vc_url: import_config.vc_url, + vc_token_path: import_config.vc_token_path, + validators_to_exit, + beacon_url: Some(beacon_url), + exit_epoch: None, + presign: false, + }); + + self.validators = local_validators.clone(); + self.src_import_builder = Some(builder); + self.index_of_validators_to_exit = index_of_validators_to_exit; + self + } + + pub async fn run_test(self) -> TestResult { + let import_builder = self.src_import_builder.unwrap(); + let initialized_validators = import_builder.vc.initialized_validators.clone(); + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + + // only assign the validator index after validator is imported to the VC + for &index in &self.index_of_validators_to_exit { + initialized_validators.write().set_index( + &self.beacon_node.harness.validator_keypairs[index] + .pk + .compress(), + index as u64, + ); + } + + let path = self.exit_config.clone().unwrap().vc_token_path; + let parent = path.parent().unwrap(); + + fs::create_dir_all(parent).expect("Was not able to create parent directory"); + + File::options() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(path.clone()) + .unwrap() + .write_all(self.vc_token.clone().unwrap().as_bytes()) + .unwrap(); + + // Advance beacon chain + self.beacon_node.harness.advance_slot(); + + self.beacon_node + .harness + .extend_chain( + 100, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let result = run::(self.exit_config.clone().unwrap()).await; + + self.beacon_node.harness.advance_slot(); + + self.beacon_node + .harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let validator_data = self + .index_of_validators_to_exit + .iter() + .map(|&index| { + self.beacon_node + .harness + .get_current_state() + .get_validator(index) + .unwrap() + .clone() + }) + .collect::>(); + + let validator_exit_epoch = validator_data + .iter() + .map(|validator| validator.exit_epoch) + .collect::>(); + + let validator_withdrawable_epoch = validator_data + .iter() + .map(|validator| validator.withdrawable_epoch) + .collect::>(); + + let current_epoch = self.beacon_node.harness.get_current_state().current_epoch(); + let max_seed_lookahead = self.beacon_node.harness.spec.max_seed_lookahead; + let min_withdrawability_delay = self + .beacon_node + .harness + .spec + .min_validator_withdrawability_delay; + + // As per the spec: + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch + let beacon_exit_epoch = current_epoch + 1 + max_seed_lookahead; + let beacon_withdrawable_epoch = beacon_exit_epoch + min_withdrawability_delay; + + assert!( + validator_exit_epoch + .iter() + .all(|&epoch| epoch == beacon_exit_epoch) + ); + + assert!( + validator_withdrawable_epoch + .iter() + .all(|&epoch| epoch == beacon_withdrawable_epoch) + ); + + if result.is_ok() { + return TestResult { result: Ok(()) }; + } + + TestResult { + result: Err(result.unwrap_err()), + } + } + } + + #[must_use] + struct TestResult { + result: Result<(), String>, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + } + #[tokio::test] + async fn exit_single_validator() { + TestBuilder::new() + .await + .with_validators(vec![0]) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn exit_multiple_validators() { + TestBuilder::new() + .await + .with_validators(vec![10, 20, 30]) + .await + .run_test() + .await + .assert_ok(); + } +} diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 63c7ca4596..24917f7d1b 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -3,9 +3,9 @@ use crate::DumpConfig; use account_utils::eth2_keystore::Keystore; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use derivative::Derivative; +use educe::Educe; use eth2::lighthouse_vc::types::KeystoreJsonStr; -use eth2::{lighthouse_vc::std_types::ImportKeystoreStatus, SensitiveUrl}; +use eth2::{SensitiveUrl, lighthouse_vc::std_types::ImportKeystoreStatus}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -55,7 +55,7 @@ pub fn cli_app() -> Command { .help( "The path to a keystore JSON file to be \ imported to the validator client. This file is usually created \ - using staking-deposit-cli or ethstaker-deposit-cli", + using ethstaker-deposit-cli", ) .action(ArgAction::Set) .display_order(0) @@ -159,15 +159,15 @@ pub fn cli_app() -> Command { ) } -#[derive(Clone, PartialEq, Serialize, Deserialize, Derivative)] -#[derivative(Debug)] +#[derive(Clone, PartialEq, Serialize, Deserialize, Educe)] +#[educe(Debug)] pub struct ImportConfig { pub validators_file_path: Option, pub keystore_file_path: Option, pub vc_url: SensitiveUrl, pub vc_token_path: PathBuf, pub ignore_duplicates: bool, - #[derivative(Debug = "ignore")] + #[educe(Debug(ignore))] pub password: Option>, pub fee_recipient: Option
, pub gas_limit: Option, @@ -279,38 +279,38 @@ async fn run(config: ImportConfig) -> Result<(), String> { for (i, validator) in validators.into_iter().enumerate() { match validator.upload(&http_client, ignore_duplicates).await { - Ok(status) => { - match status.status { - ImportKeystoreStatus::Imported => { - eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count) - } - ImportKeystoreStatus::Duplicate => { - if ignore_duplicates { - eprintln!("Re-uploaded keystore {} of {} to the VC", i + 1, count) - } else { - eprintln!( - "Keystore {} of {} was uploaded to the VC, but it was a duplicate. \ - Exiting now, use --{} to allow duplicates.", - i + 1, count, IGNORE_DUPLICATES_FLAG - ); - return Err(DETECTED_DUPLICATE_MESSAGE.to_string()); - } - } - ImportKeystoreStatus::Error => { + Ok(status) => match status.status { + ImportKeystoreStatus::Imported => { + eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count) + } + ImportKeystoreStatus::Duplicate => { + if ignore_duplicates { + eprintln!("Re-uploaded keystore {} of {} to the VC", i + 1, count) + } else { eprintln!( - "Upload of keystore {} of {} failed with message: {:?}. \ + "Keystore {} of {} was uploaded to the VC, but it was a duplicate. \ + Exiting now, use --{} to allow duplicates.", + i + 1, + count, + IGNORE_DUPLICATES_FLAG + ); + return Err(DETECTED_DUPLICATE_MESSAGE.to_string()); + } + } + ImportKeystoreStatus::Error => { + eprintln!( + "Upload of keystore {} of {} failed with message: {:?}. \ A potential solution is run this command again \ using the --{} flag, however care should be taken to ensure \ that there are no duplicate deposits submitted.", - i + 1, - count, - status.message, - IGNORE_DUPLICATES_FLAG - ); - return Err(format!("Upload failed with {:?}", status.message)); - } + i + 1, + count, + status.message, + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("Upload failed with {:?}", status.message)); } - } + }, e @ Err(UploadError::InvalidPublicKey) => { eprintln!("Validator {} has an invalid public key", i); return Err(format!("{:?}", e)); @@ -384,8 +384,8 @@ pub mod tests { use super::*; use crate::create_validators::tests::TestBuilder as CreateTestBuilder; use std::fs::{self, File}; - use tempfile::{tempdir, TempDir}; - use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + use tempfile::{TempDir, tempdir}; + use validator_http_api::{Config as HttpConfig, test_utils::ApiTester}; const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; @@ -404,8 +404,12 @@ pub mod tests { } pub async fn new_with_http_config(http_config: HttpConfig) -> Self { - let dir = tempdir().unwrap(); let vc = ApiTester::new_with_http_config(http_config).await; + Self::new_with_vc(vc).await + } + + pub async fn new_with_vc(vc: ApiTester) -> Self { + let dir = tempdir().unwrap(); let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); fs::write(&vc_token_path, &vc.api_token).unwrap(); diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 9beccd3bde..fb74779304 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -9,6 +9,7 @@ use types::EthSpec; pub mod common; pub mod create_validators; pub mod delete_validators; +pub mod exit_validators; pub mod import_validators; pub mod list_validators; pub mod move_validators; @@ -51,6 +52,7 @@ pub fn cli_app() -> Command { .subcommand(move_validators::cli_app()) .subcommand(list_validators::cli_app()) .subcommand(delete_validators::cli_app()) + .subcommand(exit_validators::cli_app()) } /// Run the account manager, returning an error if the operation did not succeed. @@ -79,11 +81,14 @@ pub fn run(matches: &ArgMatches, env: Environment) -> Result<(), move_validators::cli_run(matches, dump_config).await } Some((list_validators::CMD, matches)) => { - list_validators::cli_run(matches, dump_config).await + list_validators::cli_run::(matches, dump_config).await } Some((delete_validators::CMD, matches)) => { delete_validators::cli_run(matches, dump_config).await } + Some((exit_validators::CMD, matches)) => { + exit_validators::cli_run::(matches, dump_config).await + } Some(("", _)) => Err("No command supplied. See --help.".to_string()), Some((unknown, _)) => Err(format!( "{} is not a valid {} command. See --help.", diff --git a/validator_manager/src/list_validators.rs b/validator_manager/src/list_validators.rs index a0a1c5fb40..f7a09f8d8e 100644 --- a/validator_manager/src/list_validators.rs +++ b/validator_manager/src/list_validators.rs @@ -1,14 +1,21 @@ +use bls::PublicKeyBytes; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::lighthouse_vc::types::SingleKeystoreResponse; -use eth2::SensitiveUrl; +use eth2::types::{ConfigAndPreset, StateId, ValidatorId, ValidatorStatus}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::time::Duration; +use types::{ChainSpec, EthSpec}; -use crate::{common::vc_http_client, DumpConfig}; +use crate::exit_validators::get_current_epoch; +use crate::{DumpConfig, common::vc_http_client}; pub const CMD: &str = "list"; pub const VC_URL_FLAG: &str = "vc-url"; pub const VC_TOKEN_FLAG: &str = "vc-token"; +pub const BEACON_URL_FLAG: &str = "beacon-node"; +pub const VALIDATOR_FLAG: &str = "validators"; pub fn cli_app() -> Command { Command::new(CMD) @@ -31,47 +38,176 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0), ) + .arg( + Arg::new(BEACON_URL_FLAG) + .long(BEACON_URL_FLAG) + .value_name("NETWORK_ADDRESS") + .help( + "Address to a beacon node HTTP API. When supplied, \ + the status of validators (with regard to voluntary exit) \ + will be displayed. This flag is to be used together with \ + the --validators flag.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(VALIDATOR_FLAG), + ) + .arg( + Arg::new(VALIDATOR_FLAG) + .long(VALIDATOR_FLAG) + .value_name("STRING") + .help( + "Comma-separated list of validators (pubkey) to display status for. \ + To display the status for all validators, use the keyword \"all\". \ + This flag is to be used together with the --beacon-node flag.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(BEACON_URL_FLAG), + ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct ListConfig { pub vc_url: SensitiveUrl, pub vc_token_path: PathBuf, + pub beacon_url: Option, + pub validators_to_display: Vec, } impl ListConfig { fn from_cli(matches: &ArgMatches) -> Result { + let validators_to_display_str = + clap_utils::parse_optional::(matches, VALIDATOR_FLAG)?; + + // Keyword "all" to list all validators, vector to be created later + let validators_to_display = match validators_to_display_str { + Some(str) => { + if str.trim() == "all" { + Vec::new() + } else { + str.split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + } + } + None => Vec::new(), + }; + Ok(Self { vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?, + validators_to_display, }) } } -pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<(), String> { +pub async fn cli_run( + matches: &ArgMatches, + dump_config: DumpConfig, +) -> Result<(), String> { let config = ListConfig::from_cli(matches)?; if dump_config.should_exit_early(&config)? { Ok(()) } else { - run(config).await?; + run::(config).await?; Ok(()) } } -async fn run(config: ListConfig) -> Result, String> { +async fn run(config: ListConfig) -> Result, String> { let ListConfig { vc_url, vc_token_path, + beacon_url, + mut validators_to_display, } = config; let (_, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; println!("List of validators ({}):", validators.len()); - for validator in &validators { - println!("{}", validator.validating_pubkey); + if validators_to_display.is_empty() { + validators_to_display = validators.iter().map(|v| v.validating_pubkey).collect(); } + if let Some(ref beacon_url) = beacon_url { + for validator in &validators_to_display { + let beacon_node = BeaconNodeHttpClient::new( + beacon_url.clone(), + Timeouts::set_all(Duration::from_secs(12)), + ); + + let validator_data = beacon_node + .get_beacon_states_validator_id(StateId::Head, &ValidatorId::PublicKey(*validator)) + .await + .map_err(|e| format!("Failed to get updated validator details: {:?}", e))? + .ok_or_else(|| { + format!("Validator {} is not present in the beacon state", validator) + })? + .data; + + match validator_data.status { + ValidatorStatus::ActiveExiting => { + let exit_epoch = validator_data.validator.exit_epoch; + let withdrawal_epoch = validator_data.validator.withdrawable_epoch; + + let genesis_data = beacon_node + .get_beacon_genesis() + .await + .map_err(|e| format!("Failed to get genesis data: {}", e))? + .data; + + let config_and_preset = beacon_node + .get_config_spec::() + .await + .map_err(|e| format!("Failed to get config spec: {}", e))? + .data; + + let spec = ChainSpec::from_config::(config_and_preset.config()) + .ok_or("Failed to create chain spec")?; + + let current_epoch = get_current_epoch::(genesis_data.genesis_time, &spec) + .ok_or("Failed to get current epoch. Please check your system time")?; + + eprintln!( + "Voluntary exit for validator {} has been accepted into the beacon chain. \ + Note that the voluntary exit is subject chain finalization. \ + Before the chain has finalized, there is a low \ + probability that the exit may be reverted.", + validator + ); + eprintln!( + "Current epoch: {}, Exit epoch: {}, Withdrawable epoch: {}", + current_epoch, exit_epoch, withdrawal_epoch + ); + eprintln!("Please keep your validator running till exit epoch"); + eprintln!( + "Exit epoch in approximately {} secs", + (exit_epoch - current_epoch) * spec.seconds_per_slot * E::slots_per_epoch() + ); + } + ValidatorStatus::ExitedSlashed | ValidatorStatus::ExitedUnslashed => { + eprintln!( + "Validator {} has exited at epoch: {}", + validator, validator_data.validator.exit_epoch + ); + } + _ => { + eprintln!( + "Validator {} has not initiated voluntary exit or the voluntary exit \ + is yet to be accepted into the beacon chain. Validator status is: {}", + validator, validator_data.status + ) + } + } + } + } else { + for validator in &validators { + println!("{}", validator.validating_pubkey); + } + } Ok(validators) } @@ -87,7 +223,9 @@ mod test { use crate::{ common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, }; - use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + use types::MainnetEthSpec; + use validator_http_api::{Config as HttpConfig, test_utils::ApiTester}; + type E = MainnetEthSpec; struct TestBuilder { list_config: Option, @@ -116,6 +254,8 @@ mod test { self.list_config = Some(ListConfig { vc_url: builder.get_import_config().vc_url, vc_token_path: builder.get_import_config().vc_token_path, + beacon_url: None, + validators_to_display: vec![], }); self.vc_token = @@ -152,11 +292,9 @@ mod test { .write_all(self.vc_token.clone().unwrap().as_bytes()) .unwrap(); - let result = run(self.list_config.clone().unwrap()).await; - - if result.is_ok() { - let result_ref = result.as_ref().unwrap(); + let result = run::(self.list_config.clone().unwrap()).await; + if let Ok(result_ref) = &result { for local_validator in &self.validators { let local_keystore = &local_validator.voting_keystore.0; let local_pubkey = local_keystore.public_key().unwrap(); diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index abac071673..ace1d1941f 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -1,8 +1,10 @@ use super::common::*; use crate::DumpConfig; use account_utils::read_password_from_user; +use bls::PublicKeyBytes; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::{ + SensitiveUrl, lighthouse_vc::{ std_types::{ DeleteKeystoreStatus, DeleteKeystoresRequest, ImportKeystoreStatus, InterchangeJsonStr, @@ -10,7 +12,6 @@ use eth2::{ }, types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }, - SensitiveUrl, }; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -18,7 +19,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use tokio::time::sleep; -use types::{Address, PublicKeyBytes}; +use types::Address; use zeroize::Zeroizing; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; @@ -458,8 +459,7 @@ async fn run(config: MoveConfig) -> Result<(), String> { Err(e) => { eprintln!( "Retrying after error: {:?}. If this error persists the user will need to \ - manually recover their keystore for validator {:?} from the mnemonic." - , + manually recover their keystore for validator {:?} from the mnemonic.", e, pubkey_to_move ); } @@ -668,8 +668,8 @@ mod test { use crate::import_validators::tests::TestBuilder as ImportTestBuilder; use account_utils::validator_definitions::SigningDefinition; use std::fs; - use tempfile::{tempdir, TempDir}; - use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + use tempfile::{TempDir, tempdir}; + use validator_http_api::{Config as HttpConfig, test_utils::ApiTester}; const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; @@ -901,13 +901,13 @@ mod test { ); if self.reuse_password_files.is_some() { assert!( - src_vc - .secrets_dir - .path() - .join(format!("{:?}", pubkey)) - .exists(), - "the source password file was used by another validator and should not be deleted" - ) + src_vc + .secrets_dir + .path() + .join(format!("{:?}", pubkey)) + .exists(), + "the source password file was used by another validator and should not be deleted" + ) } else { assert!( !src_vc diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py index 8bf7f5f52d..4f584bd876 100644 --- a/validator_manager/test_vectors/generate.py +++ b/validator_manager/test_vectors/generate.py @@ -1,4 +1,4 @@ -# This script uses the `ethereum/staking-deposit-cli` tool to generate +# This script uses the `ethstaker-deposit-cli` tool to generate # deposit data files which are then used for testing by Lighthouse. # # To generate vectors, run this Python script: @@ -6,7 +6,7 @@ # `python generate.py` # # This script was last run on Linux using Python v3.10.4. Python v3.11.0 was not working at time -# of writing due to dependency issues in `staking-deposit-cli`. You should probably use `pyenv` and +# of writing due to dependency issues in `ethstaker-deposit-cli`. You should probably use `pyenv` and # `virtualenv`. import os import sys @@ -23,7 +23,7 @@ WALLET_NAME="test_wallet" tmp_dir = os.path.join(".", "tmp") mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") sdc_dir = os.path.join(tmp_dir, "sdc") -sdc_git_dir = os.path.join(sdc_dir, "staking-deposit-cli") +sdc_git_dir = os.path.join(sdc_dir, "ethstaker-deposit-cli") vectors_dir = os.path.join(".", "vectors") @@ -59,7 +59,7 @@ def setup_sdc(): "git", "clone", "--single-branch", - "https://github.com/ethereum/staking-deposit-cli.git", + "https://github.com/eth-educators/ethstaker-deposit-cli.git", str(sdc_git_dir) ]) assert(result.returncode == 0) @@ -71,9 +71,9 @@ def setup_sdc(): ], cwd=sdc_git_dir) assert(result.returncode == 0) result = subprocess.run([ - "python", - "setup.py", + "pip", "install", + ".", ], cwd=sdc_git_dir) assert(result.returncode == 0) @@ -100,7 +100,9 @@ def sdc_generate(network, first_index, count, eth1_withdrawal_address=None): '--num_validators', str(count), '--mnemonic', TEST_MNEMONIC, '--chain', network, - '--keystore_password', 'MyPassword', + '--keystore_password', 'MyPassword1234', # minimum 12 characters for password + '--withdrawal_address', '', # no withdrawal address set so it maintains 0x00 withdrawal credentials + '--regular-withdrawal', # no compounding '--folder', os.path.abspath(output_dir), ] + eth1_flags diff --git a/validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584111.json b/validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939223.json similarity index 90% rename from validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584111.json rename to validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939223.json index 6b343d087a..b2c6085197 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584111.json +++ b/validator_manager/test_vectors/vectors/holesky_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939223.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "846c83b1ec80038974ded0ef5b89d86c862a7bd4559c10528cd4bb6a48e71987f17a963bc6165a6f51c8b87474e64b450b549ce2d14a25bea3c86c241f3740f3d3edc3dc36fddbeadb1ec8969d7193da602270fea8dd31d3e64674aa2090b73d", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "cdfe14518026e99b9dfa8a029054349e37d4632ee2bbed7c2f5af19a01912368", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "846c83b1ec80038974ded0ef5b89d86c862a7bd4559c10528cd4bb6a48e71987f17a963bc6165a6f51c8b87474e64b450b549ce2d14a25bea3c86c241f3740f3d3edc3dc36fddbeadb1ec8969d7193da602270fea8dd31d3e64674aa2090b73d", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "cdfe14518026e99b9dfa8a029054349e37d4632ee2bbed7c2f5af19a01912368", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584114.json b/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939227.json similarity index 90% rename from validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584114.json rename to validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939227.json index f70410746b..e12b813e3c 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584114.json +++ b/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939227.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "846c83b1ec80038974ded0ef5b89d86c862a7bd4559c10528cd4bb6a48e71987f17a963bc6165a6f51c8b87474e64b450b549ce2d14a25bea3c86c241f3740f3d3edc3dc36fddbeadb1ec8969d7193da602270fea8dd31d3e64674aa2090b73d", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "cdfe14518026e99b9dfa8a029054349e37d4632ee2bbed7c2f5af19a01912368", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "997cff67c1675ecd2467ac050850ddec8b0488995abf363cee40cbe1461043acf4e68422e9731340437d566542e010cd186031dc0de30b2f56d19f3bb866e0fa9be31dd49ea27777f25ad786cc8587fb745598e5870647b6deeaab77fba4a9e4", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "8787f86d699426783983d03945a8ebe45b349118d28e8af528b9695887f98fac", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "846c83b1ec80038974ded0ef5b89d86c862a7bd4559c10528cd4bb6a48e71987f17a963bc6165a6f51c8b87474e64b450b549ce2d14a25bea3c86c241f3740f3d3edc3dc36fddbeadb1ec8969d7193da602270fea8dd31d3e64674aa2090b73d", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "cdfe14518026e99b9dfa8a029054349e37d4632ee2bbed7c2f5af19a01912368", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "997cff67c1675ecd2467ac050850ddec8b0488995abf363cee40cbe1461043acf4e68422e9731340437d566542e010cd186031dc0de30b2f56d19f3bb866e0fa9be31dd49ea27777f25ad786cc8587fb745598e5870647b6deeaab77fba4a9e4", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "8787f86d699426783983d03945a8ebe45b349118d28e8af528b9695887f98fac", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584129.json b/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939246.json similarity index 90% rename from validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584129.json rename to validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939246.json index 9b2678651f..bdb31d8bf2 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584129.json +++ b/validator_manager/test_vectors/vectors/holesky_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939246.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "a8eed5bb34dec5fdee4a3e68a774143072af0ebdae26a9b24ea0601d516a5eeb18aa2ec804be3f05f8475f2e472ce91809d93b7586c3a90fc8a7bbb63ad1f762eee3df0dc0ea3d33dd8ba782e48de495b3bc76e280658c1406e11d07db659e69", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "74ead0279baa86ed7106268e4806484eaae26a8f1c42f693e4b3cb626c724b63", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "8d87cdd627ed169114c00653fd3167e2afc917010071bbbbddd60e331ed0d0d7273cb4a887efe63e7b840bac713420d907e9dac20df56e50e7346b59e3acfe56753234a34c7ab3d8c40ea00b447db005b4b780701a0a2416c4fdadbdb18bf174", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "978b04b76d0a56ff28beb8eb1859792e0967d0b51e4a31485d2078b8390954d2", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "a8eed5bb34dec5fdee4a3e68a774143072af0ebdae26a9b24ea0601d516a5eeb18aa2ec804be3f05f8475f2e472ce91809d93b7586c3a90fc8a7bbb63ad1f762eee3df0dc0ea3d33dd8ba782e48de495b3bc76e280658c1406e11d07db659e69", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "74ead0279baa86ed7106268e4806484eaae26a8f1c42f693e4b3cb626c724b63", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "8d87cdd627ed169114c00653fd3167e2afc917010071bbbbddd60e331ed0d0d7273cb4a887efe63e7b840bac713420d907e9dac20df56e50e7346b59e3acfe56753234a34c7ab3d8c40ea00b447db005b4b780701a0a2416c4fdadbdb18bf174", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "978b04b76d0a56ff28beb8eb1859792e0967d0b51e4a31485d2078b8390954d2", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584124.json b/validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939241.json similarity index 87% rename from validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584124.json rename to validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939241.json index 997260bb87..aa7b311ef9 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584124.json +++ b/validator_manager/test_vectors/vectors/holesky_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939241.json @@ -1 +1 @@ -[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "818141f1f2fdba651f6a3de4ed43c774974b6cec82b3e6c3fa00569b6b67a88c37742d0033275dc98b4bbaac875e48b416b89cebfd1fe9996e2a29c0a2c512d1cedff558420a1a2b50cf5c743a622d85d941b896b00520b3e9a3eaf1f5eff12c", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "9c9f6ed171b93a08f4e1bc46c0a7feace6466e3e213c6c2d567428c73e22e242", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "b62103a32290ec8c710d48f3147895a2dddb25231c9ae38b8ca12bcaf30770a9fc632f4da6b3c5b7a43cfa6a9f096f5e13d26b2c68a42c1c86385aea268dcd2ad3cf766b3f01ee2ba19379ddae9c15830aac8acbef20accc82c734f4c40e5ffd", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "37b75d75086f4b980c85c021ca22343008d445061714cff41d63aea4dca49a5f", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "af2dc295084b4a3eff01a52fe5d42aa931509c24328d5304e59026d0957b55bc35e64802a8d64fdb4a9700bf12e1d6bb184eba01682d8413d86b737e63d3d79a16243d9c8e00115a202efc889ef7129861d8aa32bf8ec9ef5305eecce87b2eda", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "fd0c081818d2ce1bc54b7979e9b348bbbdb8fe5904694143bf4b355dcbbde692", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "818141f1f2fdba651f6a3de4ed43c774974b6cec82b3e6c3fa00569b6b67a88c37742d0033275dc98b4bbaac875e48b416b89cebfd1fe9996e2a29c0a2c512d1cedff558420a1a2b50cf5c743a622d85d941b896b00520b3e9a3eaf1f5eff12c", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "9c9f6ed171b93a08f4e1bc46c0a7feace6466e3e213c6c2d567428c73e22e242", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "b62103a32290ec8c710d48f3147895a2dddb25231c9ae38b8ca12bcaf30770a9fc632f4da6b3c5b7a43cfa6a9f096f5e13d26b2c68a42c1c86385aea268dcd2ad3cf766b3f01ee2ba19379ddae9c15830aac8acbef20accc82c734f4c40e5ffd", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "37b75d75086f4b980c85c021ca22343008d445061714cff41d63aea4dca49a5f", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "af2dc295084b4a3eff01a52fe5d42aa931509c24328d5304e59026d0957b55bc35e64802a8d64fdb4a9700bf12e1d6bb184eba01682d8413d86b737e63d3d79a16243d9c8e00115a202efc889ef7129861d8aa32bf8ec9ef5305eecce87b2eda", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "fd0c081818d2ce1bc54b7979e9b348bbbdb8fe5904694143bf4b355dcbbde692", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584117.json b/validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939232.json similarity index 90% rename from validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584117.json rename to validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939232.json index 4fa3724c59..344bc8e5c0 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584117.json +++ b/validator_manager/test_vectors/vectors/holesky_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939232.json @@ -1 +1 @@ -[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "b687aa7d55752f00a060c21fa9287485bab94c841d96b3516263fb384a812c92e60ef9fa2e09add9f55db71961fc051e0bb83d214b6f31d04ee59eaba3b43e27eadd2a64884c5d4125a1f5bd6e1d930e5a1e420c278c697d4af6ed3fcdac16cf", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "54dc56d2838ca70bac89ca92ae1f8d04945d3305ce8507b390756b646163387a", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "b687aa7d55752f00a060c21fa9287485bab94c841d96b3516263fb384a812c92e60ef9fa2e09add9f55db71961fc051e0bb83d214b6f31d04ee59eaba3b43e27eadd2a64884c5d4125a1f5bd6e1d930e5a1e420c278c697d4af6ed3fcdac16cf", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "54dc56d2838ca70bac89ca92ae1f8d04945d3305ce8507b390756b646163387a", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584120.json b/validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939236.json similarity index 90% rename from validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584120.json rename to validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939236.json index 7436b53f24..9dffddd89a 100644 --- a/validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584120.json +++ b/validator_manager/test_vectors/vectors/holesky_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939236.json @@ -1 +1 @@ -[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "a59a2c510c5ce378b514f62550a7115cd6cfebaf73a5ba20c2cf21456a2d2c11d6e117b91d23743fc0361794cf7e5405030eb296926b526e8a2d68aa87569358e69d3884563a23770714730b6fab6ba639977d725a5ed4f29abe3ccc34575610", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "149a5dfbba87109dac65142cc067aed97c9579730488cfe16625be3ce4f753a6", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "966ae45b81402f1155ff313e48ca3a5346264dcc4bc9ee9e69994ee74368852d9d27c1684752735feba6c21042ad366b13f12c6e772c453518900435d87e2d743e1818e7471cf3574598e3b085c4527f643efe679841ddf8a480cac12b2c6e08", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "f44dac412ae36929a84f64d5f7f91cada908a8f9e837fc70628f58804591798d", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "a59a2c510c5ce378b514f62550a7115cd6cfebaf73a5ba20c2cf21456a2d2c11d6e117b91d23743fc0361794cf7e5405030eb296926b526e8a2d68aa87569358e69d3884563a23770714730b6fab6ba639977d725a5ed4f29abe3ccc34575610", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "149a5dfbba87109dac65142cc067aed97c9579730488cfe16625be3ce4f753a6", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "966ae45b81402f1155ff313e48ca3a5346264dcc4bc9ee9e69994ee74368852d9d27c1684752735feba6c21042ad366b13f12c6e772c453518900435d87e2d743e1818e7471cf3574598e3b085c4527f643efe679841ddf8a480cac12b2c6e08", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "f44dac412ae36929a84f64d5f7f91cada908a8f9e837fc70628f58804591798d", "fork_version": "01017000", "network_name": "holesky", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584089.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939195.json similarity index 90% rename from validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584089.json rename to validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939195.json index d9ba926d1c..f8005651aa 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1715584089.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1748939195.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584092.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939200.json similarity index 90% rename from validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584092.json rename to validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939200.json index f1ea4c6ad3..a8b1a056c4 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1715584092.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1748939200.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "84b9fc8f260a1488c4c9a438f875edfa2bac964d651b2bc886d8442829b13f89752e807c8ca9bae9d50b1b506d3a64730015dd7f91e271ff9c1757d1996dcf6082fe5205cf6329fa2b6be303c21b66d75be608757a123da6ee4a4f14c01716d7", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "cd991ea8ff32e6b3940aed43b476c720fc1abd3040893b77a8a3efb306320d4c", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "84b9fc8f260a1488c4c9a438f875edfa2bac964d651b2bc886d8442829b13f89752e807c8ca9bae9d50b1b506d3a64730015dd7f91e271ff9c1757d1996dcf6082fe5205cf6329fa2b6be303c21b66d75be608757a123da6ee4a4f14c01716d7", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "cd991ea8ff32e6b3940aed43b476c720fc1abd3040893b77a8a3efb306320d4c", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584107.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939218.json similarity index 90% rename from validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584107.json rename to validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939218.json index 5741f23d8f..c3c25e9854 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1715584107.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1748939218.json @@ -1 +1 @@ -[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "a8461b58a5a5a0573c4af37da6ee4ba63e35894cffad6797d4a2c80f8f2c79d2c30c0de0299d8edde76e0c3f3e6d4f1e03cc377969f56d8760717d6e86f9316da9375573ce7bb87a8520daedb13c49284377f7a4f64a70aa2ca44b1581d47e20", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "d26d642a880ff8a109260fe69681840f6e1868c8c1cd2163a1db5a094e8db03a", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "93a398c09143203beb94c9223c7e18f36e5ea36090875284b222c2fcb16982e6f2e26f27ca9d30e3c6f6b5ad44857fc50f531925f4736810712f68a9d7a9c0eb664a851180f3b7d2e44a35717d43b3d3e4fd555354fa1dfa92f451870f36084d", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "7c7617a2c11870ec49e975b3691b9f822d63938df38555161e23aa245b150c66", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "a8461b58a5a5a0573c4af37da6ee4ba63e35894cffad6797d4a2c80f8f2c79d2c30c0de0299d8edde76e0c3f3e6d4f1e03cc377969f56d8760717d6e86f9316da9375573ce7bb87a8520daedb13c49284377f7a4f64a70aa2ca44b1581d47e20", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "d26d642a880ff8a109260fe69681840f6e1868c8c1cd2163a1db5a094e8db03a", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "93a398c09143203beb94c9223c7e18f36e5ea36090875284b222c2fcb16982e6f2e26f27ca9d30e3c6f6b5ad44857fc50f531925f4736810712f68a9d7a9c0eb664a851180f3b7d2e44a35717d43b3d3e4fd555354fa1dfa92f451870f36084d", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "7c7617a2c11870ec49e975b3691b9f822d63938df38555161e23aa245b150c66", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584103.json b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939214.json similarity index 87% rename from validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584103.json rename to validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939214.json index 9b9556cf9d..6bb47f5280 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1715584103.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1748939214.json @@ -1 +1 @@ -[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "a0a96851892b257c032284928641021e58e0bcd277c3da5a2c41bcce6633d144781e4761261138277b5a8cf0ead59cce073e5a3bbc4704a37abf8cd1e290dc52e56cb0c334303945ebbb79be453c8177937e44e08f980679f1a2997fe58d2d86", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "2bedaf48f8315d8631defc97c1c4c05a8152e2dc3fe779fc8e800dd67bd839a2", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "b469179ad8ba9d6ad71b99a3c7ae662d9b77cca3ee53b20ab2eb20beee31874ad47224e94e75578fa6ecd30c1d40a0b300053817f934169d84425691edf13216445fbc6dd9b0953ad3af20c834fba63c1f50c0b0f92dd8bf383cd2cc8e0431f1", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "69862477671957ab0b3f1167c5cd550c107132a0079eb70eaa4bc5c5fe06b5a0", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "a8b05626657ce5b1801e0824aaeb21de2e1a11bc16cad6100ac911bcb873aaf7e7282f1f8465df4aaea998a1a4e1645f075e7e65f8c6b8688b0162f86be2128541f91fc9feb628bcab3b4afec1f7aeccaba04aaa54dc17c738233d360f94b97e", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "34ef32901d793cd9a0a3d93e7ee40e7be9abe6fb26f0b49a86b8ff29dc649930", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "a0a96851892b257c032284928641021e58e0bcd277c3da5a2c41bcce6633d144781e4761261138277b5a8cf0ead59cce073e5a3bbc4704a37abf8cd1e290dc52e56cb0c334303945ebbb79be453c8177937e44e08f980679f1a2997fe58d2d86", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "2bedaf48f8315d8631defc97c1c4c05a8152e2dc3fe779fc8e800dd67bd839a2", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "b469179ad8ba9d6ad71b99a3c7ae662d9b77cca3ee53b20ab2eb20beee31874ad47224e94e75578fa6ecd30c1d40a0b300053817f934169d84425691edf13216445fbc6dd9b0953ad3af20c834fba63c1f50c0b0f92dd8bf383cd2cc8e0431f1", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "69862477671957ab0b3f1167c5cd550c107132a0079eb70eaa4bc5c5fe06b5a0", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "a8b05626657ce5b1801e0824aaeb21de2e1a11bc16cad6100ac911bcb873aaf7e7282f1f8465df4aaea998a1a4e1645f075e7e65f8c6b8688b0162f86be2128541f91fc9feb628bcab3b4afec1f7aeccaba04aaa54dc17c738233d360f94b97e", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "34ef32901d793cd9a0a3d93e7ee40e7be9abe6fb26f0b49a86b8ff29dc649930", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584095.json b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939204.json similarity index 90% rename from validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584095.json rename to validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939204.json index 84140f53fe..ec53025149 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1715584095.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1748939204.json @@ -1 +1 @@ -[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "a57299cde3c2ea8dc17ad3ce5a38a5f6de69d198599150dc4df02624ba1d8672440d02c0d27c3dc3b8c9f86c679571ab14c798426acd9b059895f1f5887bdee805fb4e31bd8f93ec9e78403c23d7924f23eae6af056154f35fee03bf9ffe0e98", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "246619823b45d80f53a30404542ec4be447d4e268cc0afcdf480e6a846d58411", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "a57299cde3c2ea8dc17ad3ce5a38a5f6de69d198599150dc4df02624ba1d8672440d02c0d27c3dc3b8c9f86c679571ab14c798426acd9b059895f1f5887bdee805fb4e31bd8f93ec9e78403c23d7924f23eae6af056154f35fee03bf9ffe0e98", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "246619823b45d80f53a30404542ec4be447d4e268cc0afcdf480e6a846d58411", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584098.json b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939209.json similarity index 90% rename from validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584098.json rename to validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939209.json index 3205390a43..7374811091 100644 --- a/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1715584098.json +++ b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1748939209.json @@ -1 +1 @@ -[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "8ca8a6f30b4346d7b9912e3dcd820652bc472511f89d91fd102acfb0c8df1cfc7a2629f44170727e126e88f2847fe5c9081b13fb0838a2b2343a95cabf16f57708fc0cf846bc5307209ae976c34500cc826ff48ab64169d8bebec99dded5dd1d", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "c0c6cd40b43ea0fe7fcc284de9acd9c1bd001bb88c059c155393af22a6c85d46", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "8c0784645c611b4f514a6519b737f2d02df3eba0e04cd30efebffcca769af8cc599ce28e4421cefe665ec31d3c34e44c174e0cca4891d8196796085e712459b45e411efecd07cf3258f1d6309a07a6dd52a0ae186e6184d37bf11cee36ec84e8", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "c57790b77ef97318d4ec7b97ea07ea458d08209ba372bfe76171e2ece22d6130", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.7.0"}] \ No newline at end of file +[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "8ca8a6f30b4346d7b9912e3dcd820652bc472511f89d91fd102acfb0c8df1cfc7a2629f44170727e126e88f2847fe5c9081b13fb0838a2b2343a95cabf16f57708fc0cf846bc5307209ae976c34500cc826ff48ab64169d8bebec99dded5dd1d", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "c0c6cd40b43ea0fe7fcc284de9acd9c1bd001bb88c059c155393af22a6c85d46", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "8c0784645c611b4f514a6519b737f2d02df3eba0e04cd30efebffcca769af8cc599ce28e4421cefe665ec31d3c34e44c174e0cca4891d8196796085e712459b45e411efecd07cf3258f1d6309a07a6dd52a0ae186e6184d37bf11cee36ec84e8", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "c57790b77ef97318d4ec7b97ea07ea458d08209ba372bfe76171e2ece22d6130", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "1.2.2"}] \ No newline at end of file diff --git a/wordlist.txt b/wordlist.txt index 682fae0261..e0e1fe7d73 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -1,6 +1,8 @@ +allocator APIs ARMv AUR +autocomplete Backends Backfilling Beaconcha @@ -34,10 +36,12 @@ Esat's ETH EthDocker Ethereum -Ethstaker +EthStaker Exercism Extractable FFG +Fulu +Fusaka Geth GiB Gitcoin @@ -68,6 +72,7 @@ NodeJS NullLogger PathBuf Pectra +PeerDAS PowerShell PPA Pre @@ -75,8 +80,10 @@ Proto PRs Prysm QUIC -RasPi +QuickNode README +RasPi +Reown RESTful Reth RHEL @@ -89,6 +96,7 @@ SSD SSL SSZ Styleguide +TBD TCP Teku TLS @@ -103,6 +111,8 @@ Validator VC VCs VPN +VSCode +WalletConnect Withdrawable WSL XFS @@ -184,6 +194,7 @@ namespace natively nd ness +nextest nginx nitty oom @@ -195,6 +206,8 @@ pem performant pid pre +presign +presigned pubkey pubkeys rc @@ -215,6 +228,7 @@ src stakers subnet subnets +supernode systemd testnet testnets