diff --git a/.editorconfig b/.editorconfig index a14dd7a51..149415d12 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ end_of_line=lf charset=utf-8 trim_trailing_whitespace=true max_line_length=100 -insert_final_newline=false +insert_final_newline=true \ No newline at end of file diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index 598754368..db458a3db 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -5,6 +5,10 @@ on: branches: - unstable +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-upload-to-s3: runs-on: ubuntu-20.04 diff --git a/.github/workflows/cancel-previous-runs.yml b/.github/workflows/cancel-previous-runs.yml deleted file mode 100644 index 2eaefa40c..000000000 --- a/.github/workflows/cancel-previous-runs.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: cancel previous runs -on: [push] -jobs: - cancel: - name: 'Cancel Previous Runs' - runs-on: ubuntu-latest - timeout-minutes: 3 - steps: - # https://github.com/styfle/cancel-workflow-action/releases - - uses: styfle/cancel-workflow-action@514c783324374c6940d1b92bfb962d0763d22de3 # 0.7.0 - with: - # https://api.github.com/repos/sigp/lighthouse/actions/workflows - workflow_id: 697364,2434944,4462424,308241,2883401,316 - access_token: ${{ github.token }} diff --git a/.github/workflows/docker-antithesis.yml b/.github/workflows/docker-antithesis.yml deleted file mode 100644 index 84f5541a3..000000000 --- a/.github/workflows/docker-antithesis.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: docker antithesis - -on: - push: - branches: - - unstable - -env: - ANTITHESIS_PASSWORD: ${{ secrets.ANTITHESIS_PASSWORD }} - ANTITHESIS_USERNAME: ${{ secrets.ANTITHESIS_USERNAME }} - ANTITHESIS_SERVER: ${{ secrets.ANTITHESIS_SERVER }} - REPOSITORY: ${{ secrets.ANTITHESIS_REPOSITORY }} - IMAGE_NAME: lighthouse - TAG: libvoidstar - -jobs: - build-docker: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - name: Update Rust - run: rustup update stable - - name: Dockerhub login - run: | - echo "${ANTITHESIS_PASSWORD}" | docker login --username ${ANTITHESIS_USERNAME} https://${ANTITHESIS_SERVER} --password-stdin - - name: Build AMD64 dockerfile (with push) - run: | - docker build \ - --tag ${ANTITHESIS_SERVER}/${REPOSITORY}/${IMAGE_NAME}:${TAG} \ - --file ./testing/antithesis/Dockerfile.libvoidstar . - docker push ${ANTITHESIS_SERVER}/${REPOSITORY}/${IMAGE_NAME}:${TAG} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c3119db37..007070dbb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,11 +8,17 @@ on: tags: - v* +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} IMAGE_NAME: ${{ github.repository_owner}}/lighthouse LCLI_IMAGE_NAME: ${{ github.repository_owner }}/lcli + # Enable self-hosted runners for the sigp repo only. + SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} jobs: # Extract the VERSION which is either `latest` or `vX.Y.Z`, and the VERSION_SUFFIX @@ -44,7 +50,8 @@ jobs: VERSION_SUFFIX: ${{ env.VERSION_SUFFIX }} build-docker-single-arch: name: build-docker-${{ matrix.binary }}${{ matrix.features.version_suffix }} - runs-on: ubuntu-22.04 + # Use self-hosted runners only on the sigp repo. + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release"]') || 'ubuntu-22.04' }} strategy: matrix: binary: [aarch64, @@ -60,14 +67,13 @@ jobs: needs: [extract-version] env: - # We need to enable experimental docker features in order to use `docker buildx` - DOCKER_CLI_EXPERIMENTAL: enabled VERSION: ${{ needs.extract-version.outputs.VERSION }} VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} FEATURE_SUFFIX: ${{ matrix.features.version_suffix }} steps: - uses: actions/checkout@v3 - name: Update Rust + if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable - name: Dockerhub login run: | @@ -76,16 +82,14 @@ jobs: run: | cargo install cross env CROSS_PROFILE=${{ matrix.profile }} CROSS_FEATURES=${{ matrix.features.env }} make build-${{ matrix.binary }} + - name: Make bin dir + run: mkdir ./bin - name: Move cross-built binary into Docker scope (if ARM) if: startsWith(matrix.binary, 'aarch64') - run: | - mkdir ./bin; - mv ./target/aarch64-unknown-linux-gnu/${{ matrix.profile }}/lighthouse ./bin; + run: mv ./target/aarch64-unknown-linux-gnu/${{ matrix.profile }}/lighthouse ./bin - name: Move cross-built binary into Docker scope (if x86_64) if: startsWith(matrix.binary, 'x86_64') - run: | - mkdir ./bin; - mv ./target/x86_64-unknown-linux-gnu/${{ matrix.profile }}/lighthouse ./bin; + run: mv ./target/x86_64-unknown-linux-gnu/${{ matrix.profile }}/lighthouse ./bin - name: Map aarch64 to arm64 short arch if: startsWith(matrix.binary, 'aarch64') run: echo "SHORT_ARCH=arm64" >> $GITHUB_ENV @@ -95,17 +99,24 @@ jobs: - name: Set modernity suffix if: endsWith(matrix.binary, '-portable') != true run: echo "MODERNITY_SUFFIX=-modern" >> $GITHUB_ENV; - # Install dependencies for emulation. Have to create a new builder to pick up emulation support. - - name: Build Dockerfile and push - run: | - docker run --privileged --rm tonistiigi/binfmt --install ${SHORT_ARCH} - docker buildx create --use --name cross-builder - docker buildx build \ - --platform=linux/${SHORT_ARCH} \ - --file ./Dockerfile.cross . \ - --tag ${IMAGE_NAME}:${VERSION}-${SHORT_ARCH}${VERSION_SUFFIX}${MODERNITY_SUFFIX}${FEATURE_SUFFIX} \ - --provenance=false \ - --push + + - name: Install QEMU + if: env.SELF_HOSTED_RUNNERS == 'false' + run: sudo apt-get update && sudo apt-get install -y qemu-user-static + + - name: Set up Docker Buildx + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v4 + with: + file: ./Dockerfile.cross + context: . + platforms: linux/${{ env.SHORT_ARCH }} + push: true + tags: ${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.SHORT_ARCH }}${{ env.VERSION_SUFFIX }}${{ env.MODERNITY_SUFFIX }}${{ env.FEATURE_SUFFIX }} + build-docker-multiarch: name: build-docker-multiarch${{ matrix.modernity }} runs-on: ubuntu-22.04 @@ -114,20 +125,22 @@ jobs: matrix: modernity: ["", "-modern"] env: - # We need to enable experimental docker features in order to use `docker manifest` - DOCKER_CLI_EXPERIMENTAL: enabled VERSION: ${{ needs.extract-version.outputs.VERSION }} VERSION_SUFFIX: ${{ needs.extract-version.outputs.VERSION_SUFFIX }} steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Dockerhub login run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin + - name: Create and push multiarch manifest run: | - docker manifest create ${IMAGE_NAME}:${VERSION}${VERSION_SUFFIX}${{ matrix.modernity }} \ - --amend ${IMAGE_NAME}:${VERSION}-arm64${VERSION_SUFFIX}${{ matrix.modernity }} \ - --amend ${IMAGE_NAME}:${VERSION}-amd64${VERSION_SUFFIX}${{ matrix.modernity }}; - docker manifest push ${IMAGE_NAME}:${VERSION}${VERSION_SUFFIX}${{ matrix.modernity }} + docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION}${VERSION_SUFFIX}${{ matrix.modernity }} \ + ${IMAGE_NAME}:${VERSION}-arm64${VERSION_SUFFIX}${{ matrix.modernity }} \ + ${IMAGE_NAME}:${VERSION}-amd64${VERSION_SUFFIX}${{ matrix.modernity }}; + build-docker-lcli: runs-on: ubuntu-22.04 needs: [extract-version] diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 8428c0a3b..19236691f 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -9,6 +9,10 @@ on: - 'book/**' merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: linkcheck: name: Check broken links diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index 9223c40e1..1269aee62 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -8,6 +8,10 @@ on: pull_request: merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: run-local-testnet: strategy: @@ -21,10 +25,6 @@ jobs: - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install geth (ubuntu) if: matrix.os == 'ubuntu-22.04' run: | diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml deleted file mode 100644 index 736057f78..000000000 --- a/.github/workflows/publish-crate.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish Crate - -on: - push: - tags: - - tree-hash-v* - - tree-hash-derive-v* - - eth2-ssz-v* - - eth2-ssz-derive-v* - - eth2-ssz-types-v* - - eth2-serde-util-v* - - eth2-hashing-v* - -env: - CARGO_API_TOKEN: ${{ secrets.CARGO_API_TOKEN }} - -jobs: - extract-tag: - runs-on: ubuntu-latest - steps: - - name: Extract tag - run: echo "TAG=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT - id: extract_tag - outputs: - TAG: ${{ steps.extract_tag.outputs.TAG }} - - publish-crate: - runs-on: ubuntu-latest - needs: [extract-tag] - env: - TAG: ${{ needs.extract-tag.outputs.TAG }} - steps: - - uses: actions/checkout@v3 - - name: Update Rust - run: rustup update stable - - name: Cargo login - run: | - echo "${CARGO_API_TOKEN}" | cargo login - - name: publish eth2 ssz derive - if: startsWith(env.TAG, 'eth2-ssz-derive-v') - run: | - ./scripts/ci/publish.sh consensus/ssz_derive eth2_ssz_derive "$TAG" - - name: publish eth2 ssz - if: startsWith(env.TAG, 'eth2-ssz-v') - run: | - ./scripts/ci/publish.sh consensus/ssz eth2_ssz "$TAG" - - name: publish eth2 hashing - if: startsWith(env.TAG, 'eth2-hashing-v') - run: | - ./scripts/ci/publish.sh crypto/eth2_hashing eth2_hashing "$TAG" - - name: publish tree hash derive - if: startsWith(env.TAG, 'tree-hash-derive-v') - run: | - ./scripts/ci/publish.sh consensus/tree_hash_derive tree_hash_derive "$TAG" - - name: publish tree hash - if: startsWith(env.TAG, 'tree-hash-v') - run: | - ./scripts/ci/publish.sh consensus/tree_hash tree_hash "$TAG" - - name: publish ssz types - if: startsWith(env.TAG, 'eth2-ssz-types-v') - run: | - ./scripts/ci/publish.sh consensus/ssz_types eth2_ssz_types "$TAG" - - name: publish serde util - if: startsWith(env.TAG, 'eth2-serde-util-v') - run: | - ./scripts/ci/publish.sh consensus/serde_utils eth2_serde_utils "$TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6d79bd5e..24ca09ec0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,11 +5,17 @@ on: tags: - v* +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} REPO_NAME: ${{ github.repository_owner }}/lighthouse IMAGE_NAME: ${{ github.repository_owner }}/lighthouse + # Enable self-hosted runners for the sigp repo only. + SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} jobs: extract-version: @@ -34,36 +40,37 @@ jobs: x86_64-windows-portable] include: - arch: aarch64-unknown-linux-gnu - platform: ubuntu-latest + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} profile: maxperf - arch: aarch64-unknown-linux-gnu-portable - platform: ubuntu-latest + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} profile: maxperf - arch: x86_64-unknown-linux-gnu - platform: ubuntu-latest + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} profile: maxperf - arch: x86_64-unknown-linux-gnu-portable - platform: ubuntu-latest + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "release", "large"]') || 'ubuntu-latest' }} profile: maxperf - arch: x86_64-apple-darwin - platform: macos-latest + runner: macos-latest profile: maxperf - arch: x86_64-apple-darwin-portable - platform: macos-latest + runner: macos-latest profile: maxperf - arch: x86_64-windows - platform: windows-2019 + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "release"]') || 'windows-2019' }} profile: maxperf - arch: x86_64-windows-portable - platform: windows-2019 + runner: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "release"]') || 'windows-2019' }} profile: maxperf - runs-on: ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} needs: extract-version steps: - name: Checkout sources uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' run: rustup update stable # ============================== @@ -71,7 +78,7 @@ jobs: # ============================== - uses: KyleMayes/install-llvm-action@v1 - if: startsWith(matrix.arch, 'x86_64-windows') + if: env.SELF_HOSTED_RUNNERS == 'false' && startsWith(matrix.arch, 'x86_64-windows') with: version: "15.0" directory: ${{ runner.temp }}/llvm @@ -79,15 +86,6 @@ jobs: if: startsWith(matrix.arch, 'x86_64-windows') run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV - # ============================== - # Windows & Mac dependencies - # ============================== - - name: Install Protoc - if: contains(matrix.arch, 'darwin') || contains(matrix.arch, 'windows') - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # ============================== # Builds # ============================== @@ -134,17 +132,11 @@ jobs: - name: Build Lighthouse for Windows portable if: matrix.arch == 'x86_64-windows-portable' - # NOTE: profile set to release until this rustc issue is fixed: - # - # https://github.com/rust-lang/rust/issues/107781 - # - # tracked at: https://github.com/sigp/lighthouse/issues/3964 - run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features portable,gnosis --profile ${{ matrix.profile }} - name: Build Lighthouse for Windows modern if: matrix.arch == 'x86_64-windows' - # NOTE: profile set to release (see above) - run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile release + run: cargo install --path lighthouse --force --locked --features modern,gnosis --profile ${{ matrix.profile }} - name: Configure GPG and create artifacts if: startsWith(matrix.arch, 'x86_64-windows') != true diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 32643b147..fd9b77ae2 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -9,6 +9,11 @@ on: - 'pr/*' pull_request: merge_group: + +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) @@ -17,6 +22,10 @@ env: PINNED_NIGHTLY: nightly-2023-04-16 # Prevent Github API rate limiting. LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Enable self-hosted runners for the sigp repo only. + SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} + # Self-hosted runners need to reference a different host for `./watch` tests. + WATCH_HOST: ${{ github.repository == 'sigp/lighthouse' && 'host.docker.internal' || 'localhost' }} jobs: target-branch-check: name: target-branch-check @@ -48,41 +57,37 @@ jobs: run: make cargo-fmt release-tests-ubuntu: name: release-tests-ubuntu - runs-on: ubuntu-latest + # Use self-hosted runners only on the sigp repo. + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} needs: cargo-fmt steps: - uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == false run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run tests in release run: make test-release release-tests-windows: name: release-tests-windows - runs-on: windows-2019 + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} needs: cargo-fmt steps: - uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == false run: rustup update stable - - name: Use Node.js - uses: actions/setup-node@v2 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - node-version: '14' - - name: Install windows build tools - run: | - choco install python protoc visualstudio2019-workload-vctools -y - npm config set msvs_version 2019 - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Install make run: choco install -y make - uses: KyleMayes/install-llvm-action@v1 + if: env.SELF_HOSTED_RUNNERS == false with: version: "15.0" directory: ${{ runner.temp }}/llvm @@ -92,16 +97,14 @@ jobs: run: make test-release beacon-chain-tests: name: beacon-chain-tests - runs-on: ubuntu-latest + # Use self-hosted runners only on the sigp repo. + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} needs: cargo-fmt steps: - uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == false run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run beacon_chain tests for all known forks run: make test-beacon-chain op-pool-tests: @@ -112,10 +115,6 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run operation_pool tests for all known forks run: make test-op-pool slasher-tests: @@ -130,18 +129,18 @@ jobs: run: make test-slasher debug-tests-ubuntu: name: debug-tests-ubuntu - runs-on: ubuntu-22.04 + # Use self-hosted runners only on the sigp repo. + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} needs: cargo-fmt steps: - uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == false run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run tests in debug run: make test-debug state-transition-vectors-ubuntu: @@ -152,24 +151,18 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run state_transition_vectors in release. run: make run-state-transition-tests ef-tests-ubuntu: name: ef-tests-ubuntu - runs-on: ubuntu-latest + # Use self-hosted runners only on the sigp repo. + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} needs: cargo-fmt steps: - uses: actions/checkout@v3 - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == false run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run consensus-spec-tests with blst, milagro and fake_crypto run: make test-ef dockerfile-ubuntu: @@ -192,12 +185,10 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run the beacon chain sim that starts from an eth1 contract run: cargo run --release --bin simulator eth1-sim merge-transition-ubuntu: @@ -208,12 +199,10 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run the beacon chain sim and go through the merge transition run: cargo run --release --bin simulator eth1-sim --post-merge no-eth1-simulator-ubuntu: @@ -224,10 +213,6 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run the beacon chain sim without an eth1 connection run: cargo run --release --bin simulator no-eth1-sim syncing-simulator-ubuntu: @@ -238,12 +223,10 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install anvil - run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - name: Run the syncing simulator run: cargo run --release --bin simulator syncing-sim doppelganger-protection-test: @@ -254,10 +237,6 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install geth run: | sudo add-apt-repository -y ppa:ethereum/ethereum @@ -289,10 +268,6 @@ jobs: dotnet-version: '6.0.201' - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run exec engine integration tests in release run: make test-exec-engine check-benchmarks: @@ -303,10 +278,6 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Typecheck benchmark code without running it run: make check-benches clippy: @@ -317,10 +288,6 @@ jobs: - uses: actions/checkout@v3 - name: Get latest version of stable Rust run: rustup update stable - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Lint code for quality and style with Clippy run: make lint - name: Certify Cargo.lock freshness @@ -333,10 +300,6 @@ jobs: - uses: actions/checkout@v3 - name: Install Rust @ MSRV (${{ needs.extract-msrv.outputs.MSRV }}) run: rustup override set ${{ needs.extract-msrv.outputs.MSRV }} - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run cargo check run: cargo check --workspace arbitrary-check: @@ -375,10 +338,6 @@ jobs: - uses: actions/checkout@v3 - name: Install Rust (${{ env.PINNED_NIGHTLY }}) run: rustup toolchain install $PINNED_NIGHTLY - - name: Install Protoc - uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install cargo-udeps run: cargo install cargo-udeps --locked --force - name: Create Cargo config dir @@ -396,7 +355,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install dependencies - run: sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang protobuf-compiler + run: sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang - name: Use Rust beta run: rustup override set beta - name: Run make diff --git a/Cargo.lock b/Cargo.lock index 6f40e53c3..90a5373db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,10 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde", + "serde_json", "slashing_protection", + "slog", "slot_clock", "tempfile", "tokio", @@ -46,7 +49,7 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "filesystem", - "rand 0.8.5", + "rand", "regex", "rpassword", "serde", @@ -60,9 +63,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -79,15 +82,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "aead" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" -dependencies = [ - "generic-array", -] - [[package]] name = "aead" version = "0.4.3" @@ -95,28 +89,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array", - "rand_core 0.6.4", -] - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -dependencies = [ - "aes-soft", - "aesni", - "cipher 0.2.5", ] [[package]] @@ -128,15 +100,15 @@ dependencies = [ "cfg-if", "cipher 0.3.0", "cpufeatures", - "ctr 0.8.0", + "ctr", "opaque-debug", ] [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -149,55 +121,21 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" dependencies = [ - "aead 0.4.3", + "aead", "aes 0.7.5", "cipher 0.3.0", - "ctr 0.8.0", - "ghash 0.4.4", + "ctr", + "ghash", "subtle", ] -[[package]] -name = "aes-gcm" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" -dependencies = [ - "aead 0.5.2", - "aes 0.8.2", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.0", - "subtle", -] - -[[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - -[[package]] -name = "aesni" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -215,17 +153,29 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "amcl" version = "0.3.0" -source = "git+https://github.com/sigp/milagro_bls?tag=v1.4.2#16655aa033175a90c10ef02aa144e2835de23aec" +source = "git+https://github.com/sigp/milagro_bls?tag=v1.5.1#d3fc0a40cfe8b72ccda46ba050ee6786a59ce753" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" @@ -245,16 +195,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "anvil-rpc" +version = "0.1.0" +source = "git+https://github.com/foundry-rs/foundry?rev=b45456717ffae1af65acdc71099f8cb95e6683a0#b45456717ffae1af65acdc71099f8cb95e6683a0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arbitrary" version = "1.3.0" -source = "git+https://github.com/michaelsproul/arbitrary?rev=f002b99989b561ddce62e4cf2887b0f8860ae991#f002b99989b561ddce62e4cf2887b0f8860ae991" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" dependencies = [ "derive_arbitrary", ] @@ -265,6 +225,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "array-init" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -273,25 +242,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "asn1-rs" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" -dependencies = [ - "asn1-rs-derive 0.1.0", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror", - "time 0.3.21", -] +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "asn1-rs" @@ -299,26 +252,14 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" dependencies = [ - "asn1-rs-derive 0.4.0", + "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom 7.1.3", + "nom", "num-traits", "rusticata-macros", "thiserror", - "time 0.3.21", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", + "time", ] [[package]] @@ -357,14 +298,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", - "autocfg 1.1.0", + "autocfg", "cfg-if", "concurrent-queue", "futures-lite", "log", "parking", "polling", - "rustix", + "rustix 0.37.23", "slab", "socket2 0.4.9", "waker-fn", @@ -372,9 +313,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -387,7 +328,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.9", + "pin-project-lite", ] [[package]] @@ -398,18 +339,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -420,28 +361,22 @@ checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" dependencies = [ "futures", "pharos", - "rustc_version 0.4.0", + "rustc_version", ] [[package]] name = "asynchronous-codec" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a0daa378f5fd10634e44b0a29b2a87b890657658e072a30d6f26e57ddee182" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" dependencies = [ "bytes", "futures-sink", "futures-util", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", ] -[[package]] -name = "atomic-waker" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" - [[package]] name = "attohttpc" version = "0.16.3" @@ -477,15 +412,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -494,13 +420,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.17" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -511,23 +437,24 @@ dependencies = [ "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", + "rustversion", "serde", "serde_json", + "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", - "tower-http", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.2.9" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", @@ -535,21 +462,22 @@ dependencies = [ "http", "http-body", "mime", + "rustversion", "tower-layer", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", - "miniz_oxide 0.6.2", + "miniz_oxide", "object", "rustc-demangle", ] @@ -566,6 +494,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -574,9 +508,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.1" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -587,7 +521,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon-api-client" version = "0.1.0" -source = "git+https://github.com/ralexstokes/beacon-api-client#30679e9e25d61731cde54e14cd8a3688a39d8e5b" +source = "git+https://github.com/ralexstokes/beacon-api-client?rev=93d7e8c#93d7e8c38fe9782c4862909663e7b57c44f805a9" dependencies = [ "ethereum-consensus", "http", @@ -606,13 +540,14 @@ dependencies = [ name = "beacon_chain" version = "0.2.0" dependencies = [ - "bitvec 0.20.4", + "bitvec 1.0.1", "bls", "derivative", "environment", "eth1", "eth2", "ethereum_hashing", + "ethereum_serde_utils", "ethereum_ssz", "ethereum_ssz_derive", "execution_layer", @@ -633,35 +568,34 @@ dependencies = [ "operation_pool", "parking_lot 0.12.1", "proto_array", - "rand 0.8.5", + "rand", "rayon", "safe_arith", "sensitive_url", "serde", - "serde_derive", "serde_json", "slasher", "slog", "sloggers", "slot_clock", - "smallvec", + "smallvec 1.11.0", "ssz_types", "state_processing", "store", "strum", - "superstruct 0.5.0", + "superstruct", "task_executor", "tempfile", "tokio", "tokio-stream", "tree_hash", + "tree_hash_derive", "types", - "unused_port", ] [[package]] name = "beacon_node" -version = "4.2.0" +version = "4.5.0" dependencies = [ "beacon_chain", "clap", @@ -693,6 +627,32 @@ dependencies = [ "unused_port", ] +[[package]] +name = "beacon_processor" +version = "0.1.0" +dependencies = [ + "derivative", + "ethereum_ssz", + "fnv", + "futures", + "hex", + "itertools", + "lazy_static", + "lighthouse_metrics", + "lighthouse_network", + "logging", + "num_cpus", + "parking_lot 0.12.1", + "serde", + "slog", + "slot_clock", + "strum", + "task_executor", + "tokio", + "tokio-util 0.6.10", + "types", +] + [[package]] name = "bincode" version = "1.3.3" @@ -708,7 +668,7 @@ version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -727,6 +687,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bitvec" version = "0.20.4" @@ -779,16 +745,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-modes" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" -dependencies = [ - "block-padding", - "cipher 0.2.5", -] - [[package]] name = "block-padding" version = "0.2.1" @@ -807,7 +763,7 @@ dependencies = [ "ethereum_ssz", "hex", "milagro_bls", - "rand 0.7.3", + "rand", "serde", "serde_derive", "tree_hash", @@ -816,31 +772,29 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a30d0edd9dd1c60ddb42b80341c7852f6f985279a5c1a83659dcb65899dec99" +checksum = "c94087b935a822949d3291a9989ad2b2051ea141eda0fd4e478a75f6aa3e604b" dependencies = [ "cc", "glob", "threadpool", - "which", "zeroize", ] [[package]] name = "bollard-stubs" -version = "1.41.0" +version = "1.42.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" dependencies = [ - "chrono", "serde", "serde_with", ] [[package]] name = "boot_node" -version = "4.2.0" +version = "4.5.0" dependencies = [ "beacon_node", "clap", @@ -871,13 +825,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] -name = "buf_redux" -version = "0.8.4" +name = "bs58" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" dependencies = [ - "memchr", - "safemem", + "tinyvec", ] [[package]] @@ -894,9 +847,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byte-slice-cast" @@ -912,9 +865,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" dependencies = [ "serde", ] @@ -950,25 +903,25 @@ dependencies = [ "ethereum_ssz_derive", "quickcheck", "quickcheck_macros", - "smallvec", + "smallvec 1.11.0", "ssz_types", "tree_hash", ] [[package]] name = "camino" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" dependencies = [ "serde", ] @@ -981,7 +934,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver 1.0.17", + "semver", "serde", "serde_json", "thiserror", @@ -995,19 +948,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "ccm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca1a8fbc20b50ac9673ff014abfb2b5f4085ee1a850d408f14a159c5853ac7" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "aead 0.3.2", - "cipher 0.2.5", - "subtle", + "jobserver", + "libc", ] [[package]] @@ -1016,7 +962,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -1043,7 +989,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" dependencies = [ - "aead 0.4.3", + "aead", "chacha20", "cipher 0.3.0", "poly1305", @@ -1052,27 +998,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", - "js-sys", - "num-integer", "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" -dependencies = [ - "generic-array", + "windows-targets 0.48.5", ] [[package]] @@ -1113,7 +1046,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim 0.8.0", "textwrap", "unicode-width", @@ -1141,6 +1074,7 @@ name = "client" version = "0.2.0" dependencies = [ "beacon_chain", + "beacon_processor", "directory", "dirs", "environment", @@ -1158,6 +1092,7 @@ dependencies = [ "logging", "monitoring_api", "network", + "num_cpus", "operation_pool", "parking_lot 0.12.1", "sensitive_url", @@ -1171,7 +1106,7 @@ dependencies = [ "state_processing", "store", "task_executor", - "time 0.3.21", + "time", "timer", "tokio", "types", @@ -1212,9 +1147,15 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "convert_case" @@ -1249,28 +1190,13 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" - [[package]] name = "crc32fast" version = "1.3.2" @@ -1339,22 +1265,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ - "autocfg 1.1.0", + "autocfg", "cfg-if", "crossbeam-utils", - "memoffset 0.8.0", + "memoffset 0.9.0", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -1377,6 +1303,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1384,7 +1322,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -1410,9 +1347,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" dependencies = [ "csv-core", "itoa", @@ -1438,22 +1375,13 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "ctrlc" -version = "3.3.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d778600249295e82b6ab12e291ed9029407efee0cfb7baf67157edc65964df" +checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" dependencies = [ - "nix 0.26.2", + "nix 0.27.1", "windows-sys 0.48.0", ] @@ -1472,36 +1400,40 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" dependencies = [ "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", - "packed_simd_2", - "platforms 3.0.2", + "platforms 3.1.2", + "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", + "darling_core", + "darling_macro", ] [[package]] @@ -1518,38 +1450,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core 0.13.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", + "darling_core", "quote", "syn 1.0.109", ] @@ -1643,7 +1550,7 @@ dependencies = [ "hex", "reqwest", "serde_json", - "sha2 0.10.6", + "sha2 0.9.9", "tree_hash", "types", ] @@ -1655,22 +1562,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] [[package]] -name = "der-parser" -version = "7.0.0" +name = "der" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ - "asn1-rs 0.3.1", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] @@ -1679,14 +1582,20 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ - "asn1-rs 0.5.2", + "asn1-rs", "displaydoc", - "nom 7.1.3", + "nom", "num-bigint", "num-traits", "rusticata-macros", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derivative" version = "2.2.0" @@ -1700,43 +1609,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.0" -source = "git+https://github.com/michaelsproul/arbitrary?rev=f002b99989b561ddce62e4cf2887b0f8860ae991#f002b99989b561ddce62e4cf2887b0f8860ae991" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_macro" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" -dependencies = [ - "derive_builder_core", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] @@ -1748,17 +1627,17 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 1.0.109", ] [[package]] name = "diesel" -version = "2.0.4" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" +checksum = "d98235fdc2f355d330a8244184ab6b4b33c28679c0b4158f63138e51d6cf7e88" dependencies = [ - "bitflags", + "bitflags 2.4.0", "byteorder", "diesel_derives", "itoa", @@ -1768,27 +1647,36 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.0.2" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad74fdcf086be3d4fdd142f67937678fe60ed431c3b2f08599e7687269410c4" +checksum = "e054665eaf6d97d1e7125512bb2d35d07c73ac86cc6920174cb42d1ab697a554" dependencies = [ - "proc-macro-error", + "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] name = "diesel_migrations" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" dependencies = [ "diesel", "migrations_internals", "migrations_macros", ] +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.37", +] + [[package]] name = "digest" version = "0.9.0" @@ -1805,6 +1693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1861,32 +1750,31 @@ dependencies = [ [[package]] name = "discv5" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b009a99b85b58900df46435307fc5c4c845af7e182582b1fbf869572fa9fce69" +checksum = "98c05fa26996c6141f78ac4fafbe297a7fa69690565ba4e0d1f2e60bde5ce501" dependencies = [ "aes 0.7.5", - "aes-gcm 0.9.4", + "aes-gcm", "arrayvec", "delay_map", - "enr 0.7.0", + "enr 0.9.0", "fnv", "futures", "hashlink 0.7.0", "hex", "hkdf", "lazy_static", - "libp2p-core 0.36.0", + "libp2p-core", + "libp2p-identity", "lru 0.7.8", "more-asserts", "parking_lot 0.11.2", - "rand 0.8.5", + "rand", "rlp", - "smallvec", + "smallvec 1.11.0", "socket2 0.4.9", "tokio", - "tokio-stream", - "tokio-util 0.6.10", "tracing", "tracing-subscriber", "uint", @@ -1901,14 +1789,14 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "dtoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dunce" @@ -1922,32 +1810,47 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", ] [[package]] name = "ed25519" -version = "1.5.3" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" dependencies = [ - "signature", + "pkcs8 0.10.2", + "signature 2.1.0", ] [[package]] name = "ed25519-dalek" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.1.0", "ed25519", - "rand 0.7.3", + "rand_core 0.6.4", "serde", - "sha2 0.9.9", + "sha2 0.10.7", "zeroize", ] @@ -1984,9 +1887,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -1994,27 +1897,45 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.7", - "ff", + "ff 0.12.1", "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", + "group 0.12.1", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.3", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", "subtle", "zeroize", ] [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -2026,12 +1947,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26fa0a0be8915790626d5759eb51fe47435a8eac92c2f212bd2da9aa7f30ea56" dependencies = [ "base64 0.13.1", - "bs58", + "bs58 0.4.0", "bytes", "hex", - "k256", + "k256 0.11.6", "log", - "rand 0.8.5", + "rand", "rlp", "serde", "sha3 0.10.8", @@ -2040,20 +1961,20 @@ dependencies = [ [[package]] name = "enr" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492a7e5fc2504d5fdce8e124d3e263b244a68b283cac67a69eda0cd43e0aebad" +checksum = "0be7b2ac146c1f99fe245c02d16af0696450d8e06c135db75e10eeb9e642c20d" dependencies = [ - "base64 0.13.1", - "bs58", + "base64 0.21.4", "bytes", "ed25519-dalek", "hex", - "k256", + "k256 0.13.1", "log", - "rand 0.8.5", + "rand", "rlp", "serde", + "serde-hex", "sha3 0.10.8", "zeroize", ] @@ -2072,9 +1993,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.7.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "log", "regex", @@ -2116,10 +2037,16 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.3.1" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -2170,7 +2097,7 @@ dependencies = [ "slog", "sloggers", "state_processing", - "superstruct 0.5.0", + "superstruct", "task_executor", "tokio", "tree_hash", @@ -2206,8 +2133,10 @@ dependencies = [ "futures-util", "libsecp256k1", "lighthouse_network", + "mediatype", "mime", - "procinfo", + "pretty_reqwest_error", + "procfs", "proto_array", "psutil", "reqwest", @@ -2217,6 +2146,7 @@ dependencies = [ "serde_json", "slashing_protection", "store", + "tokio", "types", ] @@ -2251,7 +2181,7 @@ dependencies = [ "hex", "num-bigint-dig", "ring", - "sha2 0.10.6", + "sha2 0.9.9", "zeroize", ] @@ -2265,7 +2195,7 @@ dependencies = [ "hex", "hmac 0.11.0", "pbkdf2 0.8.0", - "rand 0.8.5", + "rand", "scrypt", "serde", "serde_json", @@ -2273,7 +2203,7 @@ dependencies = [ "sha2 0.9.9", "tempfile", "unicode-normalization", - "uuid 0.8.2", + "uuid", "zeroize", ] @@ -2281,12 +2211,21 @@ dependencies = [ name = "eth2_network_config" version = "0.2.0" dependencies = [ + "bytes", "discv5", "eth2_config", "ethereum_ssz", + "logging", + "pretty_reqwest_error", + "reqwest", + "sensitive_url", "serde_yaml", + "sha2 0.9.9", + "slog", "tempfile", + "tokio", "types", + "url", "zip", ] @@ -2297,13 +2236,13 @@ dependencies = [ "eth2_key_derivation", "eth2_keystore", "hex", - "rand 0.8.5", + "rand", "serde", "serde_json", "serde_repr", "tempfile", "tiny-bip39", - "uuid 0.8.2", + "uuid", ] [[package]] @@ -2378,21 +2317,22 @@ dependencies = [ [[package]] name = "ethereum-consensus" version = "0.1.1" -source = "git+https://github.com/ralexstokes//ethereum-consensus?rev=9b0ee0a8a45b968c8df5e7e64ea1c094e16f053d#9b0ee0a8a45b968c8df5e7e64ea1c094e16f053d" +source = "git+https://github.com/ralexstokes/ethereum-consensus?rev=e380108#e380108d15fcc40349927fdf3d11c71f9edb67c2" dependencies = [ "async-stream", "blst", - "bs58", + "bs58 0.4.0", "enr 0.6.2", "hex", "integer-sqrt", "multiaddr 0.14.0", "multihash 0.16.3", - "rand 0.8.5", + "rand", "serde", "serde_json", + "serde_yaml", "sha2 0.9.9", - "ssz-rs", + "ssz_rs", "thiserror", "tokio", "tokio-stream", @@ -2437,7 +2377,7 @@ dependencies = [ "cpufeatures", "lazy_static", "ring", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -2455,22 +2395,22 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32749e96305376af40d7a7ee8ea4c4c64c68d09ff94a81ab78c8d9bc7153c221" +checksum = "e61ffea29f26e8249d35128a82ec8d3bd4fbc80179ea5f5e5e3daafef6a80fcb" dependencies = [ "ethereum-types 0.14.1", "itertools", - "smallvec", + "smallvec 1.11.0", ] [[package]] name = "ethereum_ssz_derive" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cac7ef2107926cea34c0064056f9bb134d2085eef882388d151d2e59174cf0" +checksum = "6085d7fd3cf84bd2b8fec150d54c8467fb491d8db9c460607c5534f653a0ee38" dependencies = [ - "darling 0.13.4", + "darling", "proc-macro2", "quote", "syn 1.0.109", @@ -2506,7 +2446,7 @@ dependencies = [ "dunce", "ethers-core", "eyre", - "getrandom 0.2.9", + "getrandom 0.2.10", "hex", "proc-macro2", "quote", @@ -2515,7 +2455,7 @@ dependencies = [ "serde", "serde_json", "syn 1.0.109", - "toml", + "toml 0.5.11", "url", "walkdir", ] @@ -2545,14 +2485,14 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", - "elliptic-curve", + "elliptic-curve 0.12.3", "ethabi 18.0.0", "generic-array", "hex", - "k256", + "k256 0.11.6", "once_cell", "open-fastrlp", - "rand 0.8.5", + "rand", "rlp", "rlp-derive", "serde", @@ -2574,11 +2514,10 @@ dependencies = [ "auto_impl", "base64 0.13.1", "ethers-core", - "futures-channel", "futures-core", "futures-timer", "futures-util", - "getrandom 0.2.9", + "getrandom 0.2.10", "hashers", "hex", "http", @@ -2590,7 +2529,6 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-tungstenite 0.17.2", "tracing", "tracing-futures", "url", @@ -2635,7 +2573,9 @@ dependencies = [ name = "execution_layer" version = "0.1.0" dependencies = [ + "arc-swap", "async-trait", + "axum", "builder_client", "bytes", "environment", @@ -2650,6 +2590,7 @@ dependencies = [ "hash-db", "hash256-std-hasher", "hex", + "hyper", "jsonwebtoken", "keccak-hash", "lazy_static", @@ -2657,18 +2598,19 @@ dependencies = [ "lru 0.7.8", "mev-rs", "parking_lot 0.12.1", - "rand 0.8.5", + "pretty_reqwest_error", + "rand", "reqwest", "sensitive_url", "serde", "serde_json", "slog", "slot_clock", - "ssz-rs", + "ssz_rs", "ssz_types", "state_processing", "strum", - "superstruct 0.6.0", + "superstruct", "task_executor", "tempfile", "tokio", @@ -2721,6 +2663,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "ff" version = "0.12.1" @@ -2731,6 +2679,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ffi-opaque" version = "2.0.1" @@ -2739,18 +2697,18 @@ checksum = "ec54ac60a7f2ee9a97cad9946f9bf629a3bc6a7ae59e68983dc9318f5a54b81a" [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" [[package]] name = "field-offset" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset 0.8.0", - "rustc_version 0.4.0", + "memoffset 0.9.0", + "rustc_version", ] [[package]] @@ -2761,6 +2719,12 @@ dependencies = [ "windows-acl", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixed-hash" version = "0.7.0" @@ -2768,7 +2732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.5", + "rand", "rustc-hex", "static_assertions", ] @@ -2781,26 +2745,20 @@ checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "arbitrary", "byteorder", - "rand 0.8.5", + "rand", "rustc-hex", "static_assertions", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide 0.7.1", + "miniz_oxide", ] [[package]] @@ -2841,9 +2799,9 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -2925,12 +2883,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", "parking", - "pin-project-lite 0.2.9", + "pin-project-lite", "waker-fn", ] @@ -2942,18 +2900,17 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "futures-rustls" -version = "0.22.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" +checksum = "35bd3cf68c183738046838e300353e4716c674dc5e56890de4826801a6622a28" dependencies = [ "futures-io", - "rustls 0.20.8", - "webpki 0.22.0", + "rustls", ] [[package]] @@ -2968,6 +2925,17 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-ticker" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e" +dependencies = [ + "futures", + "futures-timer", + "instant", +] + [[package]] name = "futures-timer" version = "3.0.2" @@ -2987,7 +2955,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", "pin-utils", "slab", ] @@ -3009,6 +2977,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -3039,17 +3008,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -3065,24 +3032,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" dependencies = [ "opaque-debug", - "polyval 0.5.3", -] - -[[package]] -name = "ghash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" -dependencies = [ - "opaque-debug", - "polyval 0.6.0", + "polyval", ] [[package]] name = "gimli" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git-version" @@ -3118,16 +3075,27 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "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.0", "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -3135,7 +3103,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util 0.7.8", @@ -3190,6 +3158,16 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + [[package]] name = "hashers" version = "1.0.1" @@ -3210,21 +3188,20 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.0", ] [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags", + "base64 0.21.4", "bytes", "headers-core", "http", @@ -3259,18 +3236,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -3363,21 +3331,17 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite 0.2.9", + "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - [[package]] name = "http_api" version = "0.1.0" dependencies = [ "beacon_chain", - "bs58", + "beacon_processor", + "bs58 0.4.0", + "bytes", "directory", "environment", "eth1", @@ -3413,7 +3377,6 @@ dependencies = [ "tokio-stream", "tree_hash", "types", - "unused_port", "warp", "warp_utils", ] @@ -3447,9 +3410,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -3459,9 +3422,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -3473,7 +3436,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.9", + "pin-project-lite", "socket2 0.4.9", "tokio", "tower-service", @@ -3483,15 +3446,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ + "futures-util", "http", "hyper", - "rustls 0.21.1", + "rustls", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls", ] [[package]] @@ -3509,9 +3473,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3549,9 +3513,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -3615,7 +3579,7 @@ checksum = "556b5a75cd4adb7c4ea21c64af1c48cefb2ce7d43dc4352c720a1fe47c21f355" dependencies = [ "attohttpc", "log", - "rand 0.8.5", + "rand", "url", "xmltree", ] @@ -3635,7 +3599,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.5.0", + "parity-scale-codec 3.6.5", ] [[package]] @@ -3688,10 +3652,20 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg 1.1.0", + "autocfg", "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "inout" version = "0.1.3" @@ -3731,53 +3705,34 @@ dependencies = [ "num-traits", ] -[[package]] -name = "interceptor" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8a11ae2da61704edada656798b61c94b35ecac2c58eb955156987d5e6be90b" -dependencies = [ - "async-trait", - "bytes", - "log", - "rand 0.8.5", - "rtcp", - "rtp", - "thiserror", - "tokio", - "waitgroup", - "webrtc-srtp", - "webrtc-util", -] - [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi 0.3.2", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipconfig" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.4.9", - "widestring 0.5.1", - "winapi", + "socket2 0.5.4", + "widestring 1.0.2", + "windows-sys 0.48.0", "winreg", ] [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itertools" @@ -3790,15 +3745,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jemalloc-ctl" -version = "0.5.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1891c671f3db85d8ea8525dd43ab147f9977041911d24a03e5a36187a7bfde9" +checksum = "7cffc705424a344c054e135d12ee591402f4539245e8bbd64e6c9eaa9458b63c" dependencies = [ "jemalloc-sys", "libc", @@ -3807,9 +3762,9 @@ dependencies = [ [[package]] name = "jemalloc-sys" -version = "0.5.3+5.3.0-patched" +version = "0.5.4+5.3.0-patched" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9bd5d616ea7ed58b571b2e209a65759664d7fb021a0819d7a790afc67e47ca1" +checksum = "ac6c1946e1cea1788cbfde01c993b52a10e2da07f4bac608228d1bed20bfebf2" dependencies = [ "cc", "libc", @@ -3817,19 +3772,28 @@ dependencies = [ [[package]] name = "jemallocator" -version = "0.5.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" +checksum = "a0de374a9f8e63150e6f5e8a60cc14c668226d7a347d8aee1a45766e3c4dd3bc" dependencies = [ "jemalloc-sys", "libc", ] [[package]] -name = "js-sys" -version = "0.3.63" +name = "jobserver" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -3840,7 +3804,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.1", + "base64 0.21.4", "pem", "ring", "serde", @@ -3855,12 +3819,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2 0.10.6", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.7", "sha3 0.10.8", ] +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.7", + "signature 2.1.0", +] + [[package]] name = "keccak" version = "0.1.4" @@ -3897,7 +3875,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "4.2.0" +version = "4.5.0" dependencies = [ "account_utils", "beacon_chain", @@ -3958,9 +3936,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.144" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libflate" @@ -3992,12 +3970,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - [[package]] name = "libm" version = "0.2.7" @@ -4009,10 +3981,10 @@ name = "libmdbx" version = "0.1.4" source = "git+https://github.com/sigp/libmdbx-rs?tag=v0.1.4#096da80a83d14343f8df833006483f48075cd135" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "derive_more", - "indexmap", + "indexmap 1.9.3", "libc", "mdbx-sys", "parking_lot 0.12.1", @@ -4021,112 +3993,63 @@ dependencies = [ [[package]] name = "libp2p" -version = "0.50.1" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7b0104790be871edcf97db9bd2356604984e623a08d825c3f27852290266b8" +checksum = "32d07d1502a027366d55afe187621c2d7895dc111a3df13b35fed698049681d7" dependencies = [ "bytes", "futures", "futures-timer", - "getrandom 0.2.9", + "getrandom 0.2.10", "instant", - "libp2p-core 0.38.0", + "libp2p-allow-block-list", + "libp2p-connection-limits", + "libp2p-core", "libp2p-dns", "libp2p-gossipsub", "libp2p-identify", + "libp2p-identity", "libp2p-mdns", "libp2p-metrics", - "libp2p-mplex", "libp2p-noise", "libp2p-plaintext", "libp2p-quic", "libp2p-swarm", "libp2p-tcp", - "libp2p-webrtc", - "libp2p-websocket", "libp2p-yamux", - "multiaddr 0.16.0", - "parking_lot 0.12.1", + "multiaddr 0.18.0", "pin-project", - "smallvec", ] [[package]] -name = "libp2p-core" -version = "0.36.0" +name = "libp2p-allow-block-list" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fff5bd889c82a0aec668f2045edd066f559d4e5c40354e5a4c77ac00caac38" +checksum = "55b46558c5c0bf99d3e2a1a38fd54ff5476ca66dd1737b12466a1824dd219311" dependencies = [ - "asn1_der", - "bs58", - "ed25519-dalek", - "either", - "fnv", - "futures", - "futures-timer", - "instant", - "lazy_static", - "libsecp256k1", - "log", - "multiaddr 0.14.0", - "multihash 0.16.3", - "multistream-select 0.11.0", - "p256", - "parking_lot 0.12.1", - "pin-project", - "prost", - "prost-build", - "rand 0.8.5", - "rw-stream-sink", - "sha2 0.10.6", - "smallvec", - "thiserror", - "unsigned-varint 0.7.1", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", "void", - "zeroize", ] [[package]] -name = "libp2p-core" -version = "0.38.0" +name = "libp2p-connection-limits" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a8fcd392ff67af6cc3f03b1426c41f7f26b6b9aff2dc632c1c56dd649e571f" +checksum = "2f5107ad45cb20b2f6c3628c7b6014b996fcb13a88053f4569c872c6e30abf58" dependencies = [ - "asn1_der", - "bs58", - "ed25519-dalek", - "either", - "fnv", - "futures", - "futures-timer", - "instant", - "libsecp256k1", - "log", - "multiaddr 0.16.0", - "multihash 0.16.3", - "multistream-select 0.12.1", - "once_cell", - "p256", - "parking_lot 0.12.1", - "pin-project", - "prost", - "prost-build", - "rand 0.8.5", - "rw-stream-sink", - "sec1", - "sha2 0.10.6", - "smallvec", - "thiserror", - "unsigned-varint 0.7.1", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", "void", - "zeroize", ] [[package]] name = "libp2p-core" -version = "0.39.2" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1df63c0b582aa434fb09b2d86897fa2b419ffeccf934b36f87fcedc8e835c2" +checksum = "ef7dd7b09e71aac9271c60031d0e558966cdb3253ba0308ab369bb2de80630d0" dependencies = [ "either", "fnv", @@ -4135,119 +4058,128 @@ dependencies = [ "instant", "libp2p-identity", "log", - "multiaddr 0.17.1", - "multihash 0.17.0", - "multistream-select 0.12.1", + "multiaddr 0.18.0", + "multihash 0.19.1", + "multistream-select", "once_cell", "parking_lot 0.12.1", "pin-project", "quick-protobuf", - "rand 0.8.5", + "rand", "rw-stream-sink", - "smallvec", + "smallvec 1.11.0", "thiserror", - "unsigned-varint 0.7.1", + "unsigned-varint 0.7.2", "void", ] [[package]] name = "libp2p-dns" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e42a271c1b49f789b92f7fc87749fa79ce5c7bdc88cbdfacb818a4bca47fec5" +checksum = "fd4394c81c0c06d7b4a60f3face7e8e8a9b246840f98d2c80508d0721b032147" dependencies = [ "futures", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "log", "parking_lot 0.12.1", - "smallvec", + "smallvec 1.11.0", "trust-dns-resolver", ] [[package]] name = "libp2p-gossipsub" -version = "0.43.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a173171c71c29bb156f98886c7c4824596de3903dadf01e2e79d2ccdcf38cd9f" +checksum = "2d157562dba6017193e5285acf6b1054759e83540bfd79f75b69d6ce774c88da" dependencies = [ "asynchronous-codec", - "base64 0.13.1", + "base64 0.21.4", "byteorder", "bytes", + "either", "fnv", "futures", + "futures-ticker", + "getrandom 0.2.10", "hex_fmt", "instant", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "log", "prometheus-client", - "prost", - "prost-build", - "prost-codec", - "rand 0.8.5", + "quick-protobuf", + "quick-protobuf-codec", + "rand", "regex", - "sha2 0.10.6", - "smallvec", - "thiserror", - "unsigned-varint 0.7.1", - "wasm-timer", + "sha2 0.10.7", + "smallvec 1.11.0", + "unsigned-varint 0.7.2", + "void", ] [[package]] name = "libp2p-identify" -version = "0.41.1" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c052d0026f4817b44869bfb6810f4e1112f43aec8553f2cb38881c524b563abf" +checksum = "6a29675a32dbcc87790db6cf599709e64308f1ae9d5ecea2d259155889982db8" dependencies = [ "asynchronous-codec", + "either", "futures", "futures-timer", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "log", - "lru 0.8.1", - "prost", - "prost-build", - "prost-codec", - "smallvec", + "lru 0.10.1", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec 1.11.0", "thiserror", "void", ] [[package]] name = "libp2p-identity" -version = "0.1.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2d584751cecb2aabaa56106be6be91338a60a0f4e420cf2af639204f596fc1" +checksum = "686e73aff5e23efbb99bc85340ea6fd8686986aa7b283a881ba182cfca535ca9" dependencies = [ - "bs58", + "asn1_der", + "bs58 0.5.0", "ed25519-dalek", + "libsecp256k1", "log", - "multiaddr 0.17.1", - "multihash 0.17.0", + "multihash 0.19.1", + "p256", "quick-protobuf", - "rand 0.8.5", - "sha2 0.10.6", + "rand", + "sec1 0.7.3", + "sha2 0.10.7", "thiserror", + "void", "zeroize", ] [[package]] name = "libp2p-mdns" -version = "0.42.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f378264aade9872d6ccd315c0accc18be3a35d15fc1b9c36e5b6f983b62b5b" +checksum = "42a2567c305232f5ef54185e9604579a894fd0674819402bb0ac0246da82f52a" dependencies = [ "data-encoding", "futures", "if-watch", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.5", - "smallvec", - "socket2 0.4.9", + "rand", + "smallvec 1.11.0", + "socket2 0.5.4", "tokio", "trust-dns-proto", "void", @@ -4255,224 +4187,184 @@ dependencies = [ [[package]] name = "libp2p-metrics" -version = "0.11.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad8a64f29da86005c86a4d2728b8a0719e9b192f4092b609fd8790acb9dec55" +checksum = "239ba7d28f8d0b5d77760dc6619c05c7e88e74ec8fbbe97f856f20a56745e620" dependencies = [ - "libp2p-core 0.38.0", + "instant", + "libp2p-core", "libp2p-gossipsub", "libp2p-identify", + "libp2p-identity", "libp2p-swarm", + "once_cell", "prometheus-client", ] [[package]] name = "libp2p-mplex" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03805b44107aa013e7cbbfa5627b31c36cbedfdfb00603c0311998882bc4bace" +checksum = "93959ed08b6caf9810e067655e25f1362098797fef7c44d3103e63dcb6f0fabe" dependencies = [ "asynchronous-codec", "bytes", "futures", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "log", "nohash-hasher", "parking_lot 0.12.1", - "rand 0.8.5", - "smallvec", - "unsigned-varint 0.7.1", + "rand", + "smallvec 1.11.0", + "unsigned-varint 0.7.2", ] [[package]] name = "libp2p-noise" -version = "0.41.0" +version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978cb57efe82e892ec6f348a536bfbd9fee677adbe5689d7a93ad3a9bffbf2e" +checksum = "71ce70757f2c0d82e9a3ef738fb10ea0723d16cec37f078f719e2c247704c1bb" dependencies = [ "bytes", - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.1.0", "futures", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "log", + "multiaddr 0.18.0", + "multihash 0.19.1", "once_cell", - "prost", - "prost-build", - "rand 0.8.5", - "sha2 0.10.6", + "quick-protobuf", + "rand", + "sha2 0.10.7", "snow", "static_assertions", "thiserror", - "x25519-dalek 1.1.1", + "x25519-dalek", "zeroize", ] [[package]] name = "libp2p-plaintext" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c43ab37fb4102682ae9a248dc2e6a8e7b941ec75cf24aed103060a788e0fd15" +checksum = "37266c683a757df713f7dcda0cdcb5ad4681355ffa1b37b77c113c176a531195" dependencies = [ "asynchronous-codec", "bytes", "futures", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "log", - "prost", - "prost-build", - "unsigned-varint 0.7.1", - "void", + "quick-protobuf", + "unsigned-varint 0.7.2", ] [[package]] name = "libp2p-quic" -version = "0.7.0-alpha" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e7c867e95c8130667b24409d236d37598270e6da69b3baf54213ba31ffca59" +checksum = "4cb763e88f9a043546bfebd3575f340e7dd3d6c1b2cf2629600ec8965360c63a" dependencies = [ "bytes", "futures", "futures-timer", "if-watch", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "libp2p-tls", "log", "parking_lot 0.12.1", - "quinn-proto", - "rand 0.8.5", - "rustls 0.20.8", + "quinn", + "rand", + "rustls", + "socket2 0.5.4", "thiserror", "tokio", ] [[package]] name = "libp2p-swarm" -version = "0.41.1" +version = "0.43.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a35472fe3276b3855c00f1c032ea8413615e030256429ad5349cdf67c6e1a0" +checksum = "28016944851bd73526d3c146aabf0fa9bbe27c558f080f9e5447da3a1772c01a" dependencies = [ "either", "fnv", "futures", "futures-timer", "instant", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "libp2p-swarm-derive", "log", - "pin-project", - "rand 0.8.5", - "smallvec", - "thiserror", + "multistream-select", + "once_cell", + "rand", + "smallvec 1.11.0", "tokio", "void", ] [[package]] name = "libp2p-swarm-derive" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d527d5827582abd44a6d80c07ff8b50b4ee238a8979e05998474179e79dc400" +checksum = "c4d5ec2a3df00c7836d7696c136274c9c59705bac69133253696a6c932cd1d74" dependencies = [ "heck", + "proc-macro-warning", + "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] name = "libp2p-tcp" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b257baf6df8f2df39678b86c578961d48cc8b68642a12f0f763f56c8e5858d" +checksum = "09bfdfb6f945c5c014b87872a0bdb6e0aef90e92f380ef57cd9013f118f9289d" dependencies = [ "futures", "futures-timer", "if-watch", "libc", - "libp2p-core 0.38.0", + "libp2p-core", + "libp2p-identity", "log", - "socket2 0.4.9", + "socket2 0.5.4", "tokio", ] [[package]] name = "libp2p-tls" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff08d13d0dc66e5e9ba6279c1de417b84fa0d0adc3b03e5732928c180ec02781" +checksum = "8218d1d5482b122ccae396bbf38abdcb283ecc96fa54760e1dfd251f0546ac61" dependencies = [ "futures", "futures-rustls", - "libp2p-core 0.39.2", + "libp2p-core", "libp2p-identity", - "rcgen 0.10.0", + "rcgen", "ring", - "rustls 0.20.8", + "rustls", + "rustls-webpki", "thiserror", - "webpki 0.22.0", - "x509-parser 0.14.0", + "x509-parser", "yasna", ] -[[package]] -name = "libp2p-webrtc" -version = "0.4.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb6cd86dd68cba72308ea05de1cebf3ba0ae6e187c40548167955d4e3970f6a" -dependencies = [ - "async-trait", - "asynchronous-codec", - "bytes", - "futures", - "futures-timer", - "hex", - "if-watch", - "libp2p-core 0.38.0", - "libp2p-noise", - "log", - "multihash 0.16.3", - "prost", - "prost-build", - "prost-codec", - "rand 0.8.5", - "rcgen 0.9.3", - "serde", - "stun", - "thiserror", - "tinytemplate", - "tokio", - "tokio-util 0.7.8", - "webrtc", -] - -[[package]] -name = "libp2p-websocket" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d705506030d5c0aaf2882437c70dab437605f21c5f9811978f694e6917a3b54" -dependencies = [ - "either", - "futures", - "futures-rustls", - "libp2p-core 0.38.0", - "log", - "parking_lot 0.12.1", - "quicksink", - "rw-stream-sink", - "soketto", - "url", - "webpki-roots", -] - [[package]] name = "libp2p-yamux" -version = "0.42.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f63594a0aa818642d9d4915c791945053877253f08a3626f13416b5cd928a29" +checksum = "8eedcb62824c4300efb9cfd4e2a6edaf3ca097b9e68b36dabe45a44469fd6a85" dependencies = [ "futures", - "libp2p-core 0.38.0", + "libp2p-core", "log", - "parking_lot 0.12.1", "thiserror", "yamux", ] @@ -4490,7 +4382,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.5", + "rand", "serde", "sha2 0.9.9", "typenum", @@ -4538,9 +4430,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.9" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" dependencies = [ "cc", "pkg-config", @@ -4549,11 +4441,12 @@ dependencies = [ [[package]] name = "lighthouse" -version = "4.2.0" +version = "4.5.0" dependencies = [ "account_manager", "account_utils", "beacon_node", + "beacon_processor", "bls", "boot_node", "clap", @@ -4563,6 +4456,7 @@ dependencies = [ "env_logger 0.9.3", "environment", "eth1", + "eth2", "eth2_network_config", "ethereum_hashing", "futures", @@ -4585,6 +4479,7 @@ dependencies = [ "unused_port", "validator_client", "validator_dir", + "validator_manager", ] [[package]] @@ -4612,6 +4507,8 @@ dependencies = [ "hex", "lazy_static", "libp2p", + "libp2p-mplex", + "libp2p-quic", "lighthouse_metrics", "lighthouse_version", "lru 0.7.8", @@ -4620,19 +4517,19 @@ dependencies = [ "prometheus-client", "quickcheck", "quickcheck_macros", - "rand 0.8.5", + "rand", "regex", "serde", "serde_derive", - "sha2 0.10.6", + "sha2 0.9.9", "slog", "slog-async", "slog-term", - "smallvec", + "smallvec 1.11.0", "snap", "ssz_types", "strum", - "superstruct 0.5.0", + "superstruct", "task_executor", "tempfile", "tiny-keccak", @@ -4662,18 +4559,30 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[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.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "lmdb-rkv" version = "0.14.0" source = "git+https://github.com/sigp/lmdb-rs?rev=f33845c6469b94265319aac0ed5085597862c27e#f33845c6469b94265319aac0ed5085597862c27e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "libc", "lmdb-rkv-sys", @@ -4691,11 +4600,11 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ - "autocfg 1.1.0", + "autocfg", "scopeguard", ] @@ -4709,12 +4618,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "logging" @@ -4745,11 +4651,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.8.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" +checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" dependencies = [ - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -4807,7 +4713,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -4818,9 +4724,15 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.5.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" +checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "md-5" @@ -4843,10 +4755,16 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.5.0" +name = "mediatype" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8c408dc227d302f1496c84d9dc68c00fec6f56f9228a18f3023f976f3ca7c945" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -4854,16 +4772,16 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -4880,39 +4798,43 @@ dependencies = [ [[package]] name = "metastruct" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734788dec2091fe9afa39530ca2ea7994f4a2c9aff3dbfebb63f2c1945c6f10b" +checksum = "ccfbb8826226b09b05bb62a0937cf6abb16f1f7d4b746eb95a83db14aec60f06" dependencies = [ "metastruct_macro", ] [[package]] name = "metastruct_macro" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ded15e7570c2a507a23e6c3a1c8d74507b779476e43afe93ddfc261d44173d" +checksum = "37cb4045d5677b7da537f8cb5d0730d5b6414e3cc81c61e4b50e1f0cbdc73909" dependencies = [ - "darling 0.13.4", + "darling", "itertools", "proc-macro2", "quote", - "smallvec", + "smallvec 1.11.0", "syn 1.0.109", ] [[package]] name = "mev-rs" -version = "0.2.1" -source = "git+https://github.com/ralexstokes//mev-rs?rev=7813d4a4a564e0754e9aaab2d95520ba437c3889#7813d4a4a564e0754e9aaab2d95520ba437c3889" +version = "0.3.0" +source = "git+https://github.com/ralexstokes/mev-rs?rev=216657016d5c0889b505857c89ae42c7aa2764af#216657016d5c0889b505857c89ae42c7aa2764af" dependencies = [ + "anvil-rpc", "async-trait", "axum", "beacon-api-client", "ethereum-consensus", "hyper", + "parking_lot 0.12.1", + "reqwest", "serde", - "ssz-rs", + "serde_json", + "ssz_rs", "thiserror", "tokio", "tracing", @@ -4920,19 +4842,19 @@ dependencies = [ [[package]] name = "migrations_internals" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" dependencies = [ "serde", - "toml", + "toml 0.7.8", ] [[package]] name = "migrations_macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" dependencies = [ "migrations_internals", "proc-macro2", @@ -4941,13 +4863,13 @@ dependencies = [ [[package]] name = "milagro_bls" -version = "1.4.2" -source = "git+https://github.com/sigp/milagro_bls?tag=v1.4.2#16655aa033175a90c10ef02aa144e2835de23aec" +version = "1.5.1" +source = "git+https://github.com/sigp/milagro_bls?tag=v1.5.1#d3fc0a40cfe8b72ccda46ba050ee6786a59ce753" dependencies = [ "amcl", "hex", "lazy_static", - "rand 0.7.3", + "rand", "zeroize", ] @@ -4973,15 +4895,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -4993,14 +4906,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -5036,51 +4948,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c580bfdd8803cce319b047d239559a22f809094aaea4ac13902a1fdcfcd4261" dependencies = [ "arrayref", - "bs58", + "bs58 0.4.0", "byteorder", "data-encoding", "multihash 0.16.3", "percent-encoding", "serde", "static_assertions", - "unsigned-varint 0.7.1", + "unsigned-varint 0.7.2", "url", ] [[package]] name = "multiaddr" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aebdb21e90f81d13ed01dc84123320838e53963c2ca94b60b305d3fa64f31e" +checksum = "92a651988b3ed3ad1bc8c87d016bb92f6f395b84ed1db9b926b32b1fc5a2c8b5" dependencies = [ "arrayref", "byteorder", "data-encoding", + "libp2p-identity", "multibase", - "multihash 0.16.3", + "multihash 0.19.1", "percent-encoding", "serde", "static_assertions", - "unsigned-varint 0.7.1", - "url", -] - -[[package]] -name = "multiaddr" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" -dependencies = [ - "arrayref", - "byteorder", - "data-encoding", - "log", - "multibase", - "multihash 0.17.0", - "percent-encoding", - "serde", - "static_assertions", - "unsigned-varint 0.7.1", + "unsigned-varint 0.7.2", "url", ] @@ -5104,19 +4998,18 @@ dependencies = [ "core2", "digest 0.10.7", "multihash-derive", - "sha2 0.10.6", - "unsigned-varint 0.7.1", + "sha2 0.10.7", + "unsigned-varint 0.7.2", ] [[package]] name = "multihash" -version = "0.17.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +checksum = "076d548d76a0e2a0d4ab471d0b1c36c577786dfc4471242035d97a12a735c492" dependencies = [ "core2", - "multihash-derive", - "unsigned-varint 0.7.1", + "unsigned-varint 0.7.2", ] [[package]] @@ -5133,56 +5026,18 @@ dependencies = [ "synstructure", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - -[[package]] -name = "multipart" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" -dependencies = [ - "buf_redux", - "httparse", - "log", - "mime", - "mime_guess", - "quick-error", - "rand 0.8.5", - "safemem", - "tempfile", - "twoway", -] - [[package]] name = "multistream-select" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363a84be6453a70e63513660f4894ef815daf88e3356bffcda9ca27d810ce83b" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ "bytes", "futures", "log", "pin-project", - "smallvec", - "unsigned-varint 0.7.1", -] - -[[package]] -name = "multistream-select" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8552ab875c1313b97b8d20cb857b9fd63e2d1d6a0a1b53ce9821e575405f27a" -dependencies = [ - "bytes", - "futures", - "log", - "pin-project", - "smallvec", - "unsigned-varint 0.7.1", + "smallvec 1.11.0", + "unsigned-varint 0.7.2", ] [[package]] @@ -5222,7 +5077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" dependencies = [ "anyhow", - "bitflags", + "bitflags 1.3.2", "byteorder", "libc", "netlink-packet-core", @@ -5274,6 +5129,7 @@ name = "network" version = "0.2.0" dependencies = [ "beacon_chain", + "beacon_processor", "delay_map", "derivative", "environment", @@ -5297,14 +5153,15 @@ dependencies = [ "matches", "num_cpus", "operation_pool", - "rand 0.8.5", + "parking_lot 0.12.1", + "rand", "rlp", "slog", "slog-async", "slog-term", "sloggers", "slot_clock", - "smallvec", + "smallvec 1.11.0", "ssz_types", "store", "strum", @@ -5321,7 +5178,7 @@ version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if", "libc", @@ -5334,22 +5191,20 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.6.5", ] [[package]] name = "nix" -version = "0.26.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags", + "bitflags 2.4.0", "cfg-if", "libc", - "static_assertions", ] [[package]] @@ -5362,23 +5217,24 @@ dependencies = [ "execution_layer", "sensitive_url", "tempfile", + "tokio", "types", "validator_client", "validator_dir", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "nom" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" - [[package]] name = "nom" version = "7.1.3" @@ -5410,31 +5266,30 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-bigint-dig" -version = "0.6.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d51546d704f52ef14b3c962b5776e53d5b862e5790e40a350d366c209bd7f7a" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "autocfg 0.1.8", "byteorder", "lazy_static", - "libm 0.2.7", + "libm", "num-integer", "num-iter", "num-traits", - "rand 0.7.3", + "rand", "serde", - "smallvec", + "smallvec 1.11.0", "zeroize", ] @@ -5444,7 +5299,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-traits", ] @@ -5454,27 +5309,27 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.2", "libc", ] @@ -5489,36 +5344,27 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" -dependencies = [ - "asn1-rs 0.3.1", -] - [[package]] name = "oid-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ - "asn1-rs 0.5.2", + "asn1-rs", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oneshot_broadcast" @@ -5566,11 +5412,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -5587,7 +5433,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -5598,18 +5444,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.3+1.1.1t" +version = "300.1.3+3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" +checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -5632,7 +5478,7 @@ dependencies = [ "lighthouse_metrics", "maplit", "parking_lot 0.12.1", - "rand 0.8.5", + "rand", "rayon", "serde", "serde_derive", @@ -5650,34 +5496,14 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "p256" -version = "0.11.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2 0.10.6", -] - -[[package]] -name = "p384" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2 0.10.6", -] - -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if", - "libm 0.1.4", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "primeorder", + "sha2 0.10.7", ] [[package]] @@ -5696,15 +5522,15 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.5.0" +version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ddb756ca205bd108aee3c62c6d3c994e1df84a59b9d6d4a5ea42ee1fd5a9a28" +checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" dependencies = [ "arrayvec", "bitvec 1.0.1", "byte-slice-cast", "impl-trait-for-tuples", - "parity-scale-codec-derive 3.1.4", + "parity-scale-codec-derive 3.6.5", "serde", ] @@ -5722,9 +5548,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.1.4" +version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5756,7 +5582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] [[package]] @@ -5769,37 +5595,39 @@ dependencies = [ "instant", "libc", "redox_syscall 0.2.16", - "smallvec", + "smallvec 1.11.0", "winapi", ] [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "smallvec", - "windows-sys 0.45.0", + "redox_syscall 0.3.5", + "smallvec 1.11.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "pbkdf2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" -dependencies = [ - "crypto-mac 0.8.0", -] +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pbkdf2" @@ -5810,6 +5638,18 @@ dependencies = [ "crypto-mac 0.11.1", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.7", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -5827,28 +5667,18 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "petgraph" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" -dependencies = [ - "fixedbitset", - "indexmap", -] +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pharos" @@ -5857,58 +5687,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" dependencies = [ "futures", - "rustc_version 0.4.0", + "rustc_version", ] [[package]] name = "phf" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_shared" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "pin-project-lite" -version = "0.1.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -5922,8 +5746,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "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.8", + "spki 0.7.2", ] [[package]] @@ -5940,15 +5774,15 @@ checksum = "e8d0eef3571242013a0d5dc84861c3ae4a652e56e12adf8bdc26ff5f8cb34c94" [[package]] name = "platforms" -version = "3.0.2" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" +checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -5959,15 +5793,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] @@ -5978,13 +5812,13 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ - "autocfg 1.1.0", - "bitflags", + "autocfg", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "windows-sys 0.48.0", ] @@ -5996,7 +5830,7 @@ checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ "cpufeatures", "opaque-debug", - "universal-hash 0.4.1", + "universal-hash", ] [[package]] @@ -6008,44 +5842,32 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.4.1", -] - -[[package]] -name = "polyval" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash 0.5.1", + "universal-hash", ] [[package]] name = "postgres-protocol" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64 0.21.1", + "base64 0.21.4", "byteorder", "bytes", "fallible-iterator", "hmac 0.12.1", "md-5", "memchr", - "rand 0.8.5", - "sha2 0.10.6", + "rand", + "sha2 0.10.7", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" dependencies = [ "bytes", "fallible-iterator", @@ -6068,13 +5890,20 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +name = "pretty_reqwest_error" +version = "0.1.0" dependencies = [ - "proc-macro2", - "syn 1.0.109", + "reqwest", + "sensitive_url", +] + +[[package]] +name = "primeorder" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +dependencies = [ + "elliptic-curve 0.13.5", ] [[package]] @@ -6111,7 +5940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", - "toml", + "toml 0.5.11", ] [[package]] @@ -6145,24 +5974,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] -name = "proc-macro2" -version = "1.0.58" +name = "proc-macro-warning" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] -name = "procinfo" -version = "0.4.2" +name = "procfs" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab1427f3d2635891f842892dda177883dca0639e05fe66796a62c9d2f23b49c" +checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f" dependencies = [ + "bitflags 1.3.2", "byteorder", - "libc", - "nom 2.2.1", - "rustc_version 0.2.3", + "chrono", + "flate2", + "hex", + "lazy_static", + "rustix 0.36.15", ] [[package]] @@ -6182,92 +6025,25 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.18.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83cd1b99916654a69008fd66b4f9397fbe08e6e51dfe23d4417acf5d3b8cb87c" +checksum = "3c99afa9a01501019ac3a14d71d9f94050346f55ca471ce90c799a15c58f61e2" dependencies = [ "dtoa", "itoa", "parking_lot 0.12.1", - "prometheus-client-derive-text-encode", + "prometheus-client-derive-encode", ] [[package]] -name = "prometheus-client-derive-text-encode" -version = "0.3.0" +name = "prometheus-client-derive-encode" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a455fbcb954c1a7decf3c586e860fd7889cddf4b8e164be736dbac95a953cd" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" -dependencies = [ - "bytes", - "heck", - "itertools", - "lazy_static", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 1.0.109", - "tempfile", - "which", -] - -[[package]] -name = "prost-codec" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc34979ff898b6e141106178981ce2596c387ea6e62533facfc61a37fc879c0" -dependencies = [ - "asynchronous-codec", - "bytes", - "prost", - "thiserror", - "unsigned-varint 0.7.1", -] - -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost", + "syn 2.0.37", ] [[package]] @@ -6280,7 +6056,7 @@ dependencies = [ "serde", "serde_derive", "serde_yaml", - "superstruct 0.5.0", + "superstruct", "types", ] @@ -6325,22 +6101,34 @@ dependencies = [ ] [[package]] -name = "quickcheck" -version = "0.9.2" +name = "quick-protobuf-codec" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +checksum = "f8ededb1cd78531627244d51dd0c7139fbe736c7d57af0092a76f0ffb2f56e98" dependencies = [ - "env_logger 0.7.1", + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror", + "unsigned-varint 0.7.2", +] + +[[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.7.3", - "rand_core 0.5.1", + "rand", ] [[package]] name = "quickcheck_macros" -version = "0.9.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", @@ -6348,39 +6136,58 @@ dependencies = [ ] [[package]] -name = "quicksink" -version = "0.1.2" +name = "quinn" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ - "futures-core", - "futures-sink", - "pin-project-lite 0.1.12", + "bytes", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", ] [[package]] name = "quinn-proto" -version = "0.9.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c10f662eee9c94ddd7135043e544f3c82fa839a1e7b865911331961b53186c" +checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1" dependencies = [ "bytes", - "rand 0.8.5", + "rand", "ring", "rustc-hash", - "rustls 0.20.8", + "rustls", "slab", "thiserror", "tinyvec", "tracing", - "webpki 0.22.0", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.4", + "tracing", + "windows-sys 0.48.0", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -6418,19 +6225,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - [[package]] name = "rand" version = "0.8.5" @@ -6438,20 +6232,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - [[package]] name = "rand_chacha" version = "0.3.1" @@ -6477,16 +6261,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.9", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", + "getrandom 0.2.10", ] [[package]] @@ -6520,19 +6295,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rcgen" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6413f3de1edee53342e6138e75b56d32e7bc6e332b3bd62d497b1929d4cfbcdd" -dependencies = [ - "pem", - "ring", - "time 0.3.21", - "x509-parser 0.13.2", - "yasna", -] - [[package]] name = "rcgen" version = "0.10.0" @@ -6541,7 +6303,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.21", + "time", "yasna", ] @@ -6551,7 +6313,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -6560,7 +6322,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -6569,20 +6331,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] @@ -6594,6 +6357,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -6602,17 +6376,17 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.1", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -6630,15 +6404,15 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.9", - "rustls 0.21.1", + "pin-project-lite", + "rustls", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.0", + "tokio-rustls", "tokio-util 0.7.8", "tower-service", "url", @@ -6666,11 +6440,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac 0.12.1", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -6723,17 +6507,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "rtcp" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1919efd6d4a6a85d13388f9487549bb8e359f17198cc03ffd72f79b553873691" -dependencies = [ - "bytes", - "thiserror", - "webrtc-util", -] - [[package]] name = "rtnetlink" version = "0.10.1" @@ -6749,32 +6522,18 @@ dependencies = [ "tokio", ] -[[package]] -name = "rtp" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a095411ff00eed7b12e4c6a118ba984d113e1079582570d56a5ee723f11f80" -dependencies = [ - "async-trait", - "bytes", - "rand 0.8.5", - "serde", - "thiserror", - "webrtc-util", -] - [[package]] name = "rusqlite" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.8.2", + "hashlink 0.8.4", "libsqlite3-sys", - "smallvec", + "smallvec 1.11.0", ] [[package]] @@ -6795,22 +6554,13 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.17", + "semver", ] [[package]] @@ -6819,74 +6569,76 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] name = "rustix" -version = "0.37.19" +version = "0.36.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "c37f1bd5ef1b5422177b7646cba67430579cfe2ace80f284fee876bca52ad941" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.7", "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.19.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64 0.13.1", - "log", - "ring", - "sct 0.6.1", - "webpki 0.21.4", -] - -[[package]] -name = "rustls" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct 0.7.0", - "webpki 0.22.0", -] - -[[package]] -name = "rustls" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", "rustls-webpki", - "sct 0.7.0", + "sct", ] [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.1", + "base64 0.21.4", ] [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.101.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" dependencies = [ "ring", "untrusted", @@ -6894,15 +6646,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rw-stream-sink" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26338f5e09bb721b85b135ea05af7767c90b52f6de4f087d4f4a3a9d64e7dc04" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" dependencies = [ "futures", "pin-project", @@ -6911,20 +6663,14 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safe_arith" version = "0.1.0" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "salsa20" version = "0.8.1" @@ -6945,21 +6691,21 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b569c32c806ec3abdf3b5869fb8bf1e0d275a7c1c9b0b05603d9464632649edf" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" dependencies = [ "cfg-if", "derive_more", - "parity-scale-codec 3.5.0", + "parity-scale-codec 3.6.5", "scale-info-derive", ] [[package]] name = "scale-info-derive" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53012eae69e5aa5c14671942a5dd47de59d4cdcff8532a6dd0e081faf1119482" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6969,11 +6715,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -6993,9 +6739,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scrypt" @@ -7009,16 +6755,6 @@ dependencies = [ "sha2 0.9.9", ] -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sct" version = "0.7.0" @@ -7029,39 +6765,41 @@ dependencies = [ "untrusted", ] -[[package]] -name = "sdp" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d22a5ef407871893fd72b4562ee15e4742269b173959db4b8df6f538c414e13" -dependencies = [ - "rand 0.8.5", - "substring", - "thiserror", - "url", -] - [[package]] name = "sec1" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "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.8", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -7070,9 +6808,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -7080,28 +6818,13 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" dependencies = [ "serde", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "send_wrapper" version = "0.6.0" @@ -7118,13 +6841,24 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" +dependencies = [ + "array-init", + "serde", + "smallvec 0.6.14", +] + [[package]] name = "serde_array_query" version = "0.1.0" @@ -7147,20 +6881,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -7168,14 +6902,33 @@ dependencies = [ ] [[package]] -name = "serde_repr" -version = "0.1.12" +name = "serde_path_to_error" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", ] [[package]] @@ -7206,7 +6959,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling 0.13.4", + "darling", "proc-macro2", "quote", "syn 1.0.109", @@ -7218,36 +6971,12 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", ] -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.10.5" @@ -7274,9 +7003,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -7316,9 +7045,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signal-hook-registry" @@ -7339,6 +7068,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -7348,7 +7087,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.21", + "time", ] [[package]] @@ -7371,17 +7110,17 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -7403,7 +7142,7 @@ dependencies = [ "lru 0.7.8", "maplit", "parking_lot 0.12.1", - "rand 0.8.5", + "rand", "rayon", "safe_arith", "serde", @@ -7461,9 +7200,9 @@ checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" dependencies = [ "crossbeam-channel", "slog", @@ -7480,7 +7219,7 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.21", + "time", ] [[package]] @@ -7525,14 +7264,14 @@ dependencies = [ "slog", "term", "thread_local", - "time 0.3.21", + "time", ] [[package]] name = "sloggers" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e20d36cb80da75a9c5511872f15247ddad14ead8c1dd97a86b56d1be9f5d4a0e" +checksum = "7a0a4d8569a69ee56f277bffc2f6eee637b98ed468448e8a5a84fa63efe4de9d" dependencies = [ "chrono", "libc", @@ -7564,9 +7303,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "snap" @@ -7576,18 +7324,18 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "snow" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "0c9d1425eb528a21de2755c75af4c9b5d57f50a0d4c3b7f1828a4cd03f8ba155" dependencies = [ - "aes-gcm 0.9.4", + "aes-gcm", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.1.0", "rand_core 0.6.4", "ring", - "rustc_version 0.4.0", - "sha2 0.10.6", + "rustc_version", + "sha2 0.10.7", "subtle", ] @@ -7603,30 +7351,14 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys 0.48.0", ] -[[package]] -name = "soketto" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" -dependencies = [ - "base64 0.13.1", - "bytes", - "flate2", - "futures", - "httparse", - "log", - "rand 0.8.5", - "sha-1 0.9.8", -] - [[package]] name = "spin" version = "0.5.2" @@ -7640,27 +7372,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", ] [[package]] -name = "ssz-rs" -version = "0.8.0" -source = "git+https://github.com/ralexstokes//ssz-rs?rev=adf1a0b14cef90b9536f28ef89da1fab316465e1#adf1a0b14cef90b9536f28ef89da1fab316465e1" +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.8", +] + +[[package]] +name = "ssz_rs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057291e5631f280978fa9c8009390663ca4613359fc1318e36a8c24c392f6d1f" dependencies = [ "bitvec 1.0.1", "hex", "num-bigint", "serde", "sha2 0.9.9", - "ssz-rs-derive", - "thiserror", + "ssz_rs_derive", ] [[package]] -name = "ssz-rs-derive" -version = "0.8.0" -source = "git+https://github.com/ralexstokes//ssz-rs?rev=adf1a0b14cef90b9536f28ef89da1fab316465e1#adf1a0b14cef90b9536f28ef89da1fab316465e1" +name = "ssz_rs_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07d54c4d01a1713eb363b55ba51595da15f6f1211435b71466460da022aa140" dependencies = [ "proc-macro2", "quote", @@ -7669,9 +7412,9 @@ dependencies = [ [[package]] name = "ssz_types" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8052a1004e979c0be24b9e55940195553103cc57d0b34f7e2c4e32793325e402" +checksum = "382939886cb24ee8ac885d09116a60f6262d827c7a9e36012b4f6d3d0116d0b3" dependencies = [ "arbitrary", "derivative", @@ -7680,7 +7423,7 @@ dependencies = [ "itertools", "serde", "serde_derive", - "smallvec", + "smallvec 1.11.0", "tree_hash", "typenum", ] @@ -7705,7 +7448,7 @@ dependencies = [ "merkle_proof", "rayon", "safe_arith", - "smallvec", + "smallvec 1.11.0", "ssz_types", "tokio", "tree_hash", @@ -7757,10 +7500,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -7799,65 +7543,23 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "stun" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e94b1ec00bad60e6410e058b52f1c66de3dc5fe4d62d09b3e52bb7d3b73e25" -dependencies = [ - "base64 0.13.1", - "crc", - "lazy_static", - "md-5", - "rand 0.8.5", - "ring", - "subtle", - "thiserror", - "tokio", - "url", - "webrtc-util", -] - -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "superstruct" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a99807a055ff4ff5d249bb84c80d9eabb55ca3c452187daae43fd5b51ef695" -dependencies = [ - "darling 0.13.4", - "itertools", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - [[package]] name = "superstruct" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b9e5728aa1a87141cefd4e7509903fc01fa0dcb108022b1e841a67c5159fc5" dependencies = [ - "darling 0.13.4", + "darling", "itertools", "proc-macro2", "quote", - "smallvec", + "smallvec 1.11.0", "syn 1.0.109", ] @@ -7883,9 +7585,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -7931,7 +7633,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] @@ -7999,15 +7701,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.38.13", + "windows-sys 0.48.0", ] [[package]] @@ -8049,18 +7751,17 @@ dependencies = [ [[package]] name = "testcontainers" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" +source = "git+https://github.com/testcontainers/testcontainers-rs/?rev=0f2c9851#0f2c985160e51a200cfc847097c15b8d85ed7df1" dependencies = [ "bollard-stubs", "futures", "hex", "hmac 0.12.1", "log", - "rand 0.8.5", + "rand", "serde", "serde_json", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -8074,22 +7775,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -8113,21 +7814,11 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ + "deranged", "itoa", "libc", "num_threads", @@ -8144,9 +7835,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -8164,17 +7855,17 @@ dependencies = [ [[package]] name = "tiny-bip39" -version = "0.8.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" dependencies = [ "anyhow", - "hmac 0.8.1", + "hmac 0.12.1", "once_cell", - "pbkdf2 0.4.0", - "rand 0.7.3", + "pbkdf2 0.11.0", + "rand", "rustc-hash", - "sha2 0.9.9", + "sha2 0.10.7", "thiserror", "unicode-normalization", "wasm-bindgen", @@ -8217,19 +7908,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg 1.1.0", + "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.9", + "pin-project-lite", "signal-hook-registry", - "socket2 0.4.9", + "socket2 0.5.4", "tokio-macros", "windows-sys 0.48.0", ] @@ -8240,7 +7931,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", ] @@ -8252,7 +7943,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -8267,9 +7958,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e89f6234aa8fd43779746012fcf53603cdb91fdd8399aa0de868c2d56b6dde1" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" dependencies = [ "async-trait", "byteorder", @@ -8281,43 +7972,23 @@ dependencies = [ "parking_lot 0.12.1", "percent-encoding", "phf", - "pin-project-lite 0.2.9", + "pin-project-lite", "postgres-protocol", "postgres-types", - "socket2 0.5.3", + "rand", + "socket2 0.5.4", "tokio", "tokio-util 0.7.8", + "whoami", ] [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.19.1", - "tokio", - "webpki 0.21.4", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki 0.22.0", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" -dependencies = [ - "rustls 0.21.1", + "rustls", "tokio", ] @@ -8328,40 +7999,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tokio-util 0.7.8", ] -[[package]] -name = "tokio-tungstenite" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" -dependencies = [ - "futures-util", - "log", - "pin-project", - "tokio", - "tungstenite 0.14.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" -dependencies = [ - "futures-util", - "log", - "rustls 0.20.8", - "tokio", - "tokio-rustls 0.23.4", - "tungstenite 0.17.3", - "webpki 0.22.0", - "webpki-roots", -] - [[package]] name = "tokio-util" version = "0.6.10" @@ -8373,7 +8015,7 @@ dependencies = [ "futures-io", "futures-sink", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "slab", "tokio", ] @@ -8386,9 +8028,8 @@ checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", - "pin-project-lite 0.2.9", + "pin-project-lite", "slab", "tokio", "tracing", @@ -8403,6 +8044,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -8412,32 +8087,13 @@ dependencies = [ "futures-core", "futures-util", "pin-project", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite 0.2.9", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.2" @@ -8458,20 +8114,20 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -8516,7 +8172,7 @@ dependencies = [ "once_cell", "regex", "sharded-slab", - "smallvec", + "smallvec 1.11.0", "thread_local", "tracing", "tracing-core", @@ -8544,22 +8200,22 @@ dependencies = [ [[package]] name = "tree_hash" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8488e272d45adc36db8f6c99d09613f58a7cd06c7b347546c87d9a29ca11e8" +checksum = "5c998ac5fe2b07c025444bdd522e6258110b63861c6698eedc610c071980238d" dependencies = [ "ethereum-types 0.14.1", "ethereum_hashing", - "smallvec", + "smallvec 1.11.0", ] [[package]] name = "tree_hash_derive" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83baa26594d96889e5fef7638dfb0f41e16070301a5cf6da99b9a6a0804cec89" +checksum = "84303a9c7cda5f085a3ed9cd241d1e95e04d88aab1d679b02f212e653537ba86" dependencies = [ - "darling 0.13.4", + "darling", "quote", "syn 1.0.109", ] @@ -8590,8 +8246,8 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "rand 0.8.5", - "smallvec", + "rand", + "smallvec 1.11.0", "socket2 0.4.9", "thiserror", "tinyvec", @@ -8613,7 +8269,7 @@ dependencies = [ "lru-cache", "parking_lot 0.12.1", "resolv-conf", - "smallvec", + "smallvec 1.11.0", "thiserror", "tokio", "tracing", @@ -8626,79 +8282,11 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "tungstenite" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" -dependencies = [ - "base64 0.13.1", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand 0.8.5", - "sha-1 0.9.8", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" -dependencies = [ - "base64 0.13.1", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand 0.8.5", - "rustls 0.20.8", - "sha-1 0.10.1", - "thiserror", - "url", - "utf-8", - "webpki 0.22.0", -] - -[[package]] -name = "turn" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4712ee30d123ec7ae26d1e1b218395a16c87cdbaf4b3925d170d684af62ea5e8" -dependencies = [ - "async-trait", - "base64 0.13.1", - "futures", - "log", - "md-5", - "rand 0.8.5", - "ring", - "stun", - "thiserror", - "tokio", - "webrtc-util", -] - -[[package]] -name = "twoway" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" -dependencies = [ - "memchr", -] - [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "types" @@ -8727,7 +8315,8 @@ dependencies = [ "merkle_proof", "metastruct", "parking_lot 0.12.1", - "rand 0.8.5", + "paste", + "rand", "rand_xorshift", "rayon", "regex", @@ -8739,10 +8328,11 @@ dependencies = [ "serde_with", "serde_yaml", "slog", - "smallvec", + "smallvec 1.11.0", "ssz_types", "state_processing", - "superstruct 0.6.0", + "strum", + "superstruct", "swap_or_not_shuffle", "tempfile", "test_random_derive", @@ -8772,9 +8362,9 @@ checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -8787,9 +8377,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -8822,16 +8412,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "unsigned-varint" version = "0.6.0" @@ -8844,9 +8424,9 @@ dependencies = [ [[package]] name = "unsigned-varint" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" dependencies = [ "asynchronous-codec", "bytes", @@ -8869,40 +8449,25 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna 0.4.0", "percent-encoding", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", "serde", ] -[[package]] -name = "uuid" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" -dependencies = [ - "getrandom 0.2.9", -] - [[package]] name = "validator_client" version = "0.3.5" @@ -8934,7 +8499,7 @@ dependencies = [ "malloc_utils", "monitoring_api", "parking_lot 0.12.1", - "rand 0.8.5", + "rand", "reqwest", "ring", "safe_arith", @@ -8966,16 +8531,42 @@ dependencies = [ "bls", "deposit_contract", "derivative", + "directory", "eth2_keystore", "filesystem", "hex", "lockfile", - "rand 0.8.5", + "rand", "tempfile", "tree_hash", "types", ] +[[package]] +name = "validator_manager" +version = "0.1.0" +dependencies = [ + "account_utils", + "bls", + "clap", + "clap_utils", + "environment", + "eth2", + "eth2_keystore", + "eth2_network_config", + "eth2_wallet", + "ethereum_serde_utils", + "hex", + "regex", + "serde", + "serde_json", + "tempfile", + "tokio", + "tree_hash", + "types", + "validator_client", +] + [[package]] name = "valuable" version = "0.1.0" @@ -9006,15 +8597,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "waitgroup" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" -dependencies = [ - "atomic-waker", -] - [[package]] name = "waker-fn" version = "1.1.0" @@ -9023,9 +8605,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -9033,18 +8615,17 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] [[package]] name = "warp" -version = "0.3.2" -source = "git+https://github.com/macladson/warp?rev=7e75acc368229a46a236a8c991bf251fe7fe50ef#7e75acc368229a46a236a8c991bf251fe7fe50ef" +version = "0.3.5" +source = "git+https://github.com/seanmonstar/warp.git#5ad8a9cb155f6485d13d591a564d8c70053a388a" dependencies = [ "bytes", "futures-channel", @@ -9055,18 +8636,17 @@ dependencies = [ "log", "mime", "mime_guess", - "multipart", "percent-encoding", "pin-project", + "rustls-pemfile", "scoped-tls", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.22.0", + "tokio-rustls", "tokio-stream", - "tokio-tungstenite 0.15.0", - "tokio-util 0.6.10", + "tokio-util 0.7.8", "tower-service", "tracing", ] @@ -9095,12 +8675,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9109,9 +8683,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -9119,24 +8693,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -9146,9 +8720,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9156,28 +8730,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" dependencies = [ "futures-util", "js-sys", @@ -9219,13 +8793,15 @@ dependencies = [ "http_api", "hyper", "log", + "logging", "network", "r2d2", - "rand 0.7.3", + "rand", "reqwest", "serde", "serde_json", "serde_yaml", + "task_executor", "testcontainers", "tokio", "tokio-postgres", @@ -9236,9 +8812,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -9258,7 +8834,6 @@ dependencies = [ "parking_lot 0.12.1", "reqwest", "serde", - "serde_derive", "serde_json", "serde_yaml", "slot_clock", @@ -9271,253 +8846,20 @@ dependencies = [ "zip", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki 0.22.0", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] -name = "webrtc" -version = "0.6.0" +name = "whoami" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3bc9049bdb2cea52f5fd4f6f728184225bdb867ed0dc2410eab6df5bdd67bb" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "hex", - "interceptor", - "lazy_static", - "log", - "rand 0.8.5", - "rcgen 0.9.3", - "regex", - "ring", - "rtcp", - "rtp", - "rustls 0.19.1", - "sdp", - "serde", - "serde_json", - "sha2 0.10.6", - "stun", - "thiserror", - "time 0.3.21", - "tokio", - "turn", - "url", - "waitgroup", - "webrtc-data", - "webrtc-dtls", - "webrtc-ice", - "webrtc-mdns", - "webrtc-media", - "webrtc-sctp", - "webrtc-srtp", - "webrtc-util", -] - -[[package]] -name = "webrtc-data" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef36a4d12baa6e842582fe9ec16a57184ba35e1a09308307b67d43ec8883100" -dependencies = [ - "bytes", - "derive_builder", - "log", - "thiserror", - "tokio", - "webrtc-sctp", - "webrtc-util", -] - -[[package]] -name = "webrtc-dtls" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942be5bd85f072c3128396f6e5a9bfb93ca8c1939ded735d177b7bcba9a13d05" -dependencies = [ - "aes 0.6.0", - "aes-gcm 0.10.2", - "async-trait", - "bincode", - "block-modes", - "byteorder", - "ccm", - "curve25519-dalek 3.2.0", - "der-parser 8.2.0", - "elliptic-curve", - "hkdf", - "hmac 0.12.1", - "log", - "oid-registry 0.6.1", - "p256", - "p384", - "rand 0.8.5", - "rand_core 0.6.4", - "rcgen 0.9.3", - "ring", - "rustls 0.19.1", - "sec1", - "serde", - "sha1", - "sha2 0.10.6", - "signature", - "subtle", - "thiserror", - "tokio", - "webpki 0.21.4", - "webrtc-util", - "x25519-dalek 2.0.0-pre.1", - "x509-parser 0.13.2", -] - -[[package]] -name = "webrtc-ice" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a03cc11e9a7d7b4f9f99870558fe37a102b65b93f8045392fef7c67b39e80" -dependencies = [ - "arc-swap", - "async-trait", - "crc", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "stun", - "thiserror", - "tokio", - "turn", - "url", - "uuid 1.3.3", - "waitgroup", - "webrtc-mdns", - "webrtc-util", -] - -[[package]] -name = "webrtc-mdns" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08dfd7a6e3987e255c4dbe710dde5d94d0f0574f8a21afa95d171376c143106" -dependencies = [ - "log", - "socket2 0.4.9", - "thiserror", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-media" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72e1650a8ae006017d1a5280efb49e2610c19ccc3c0905b03b648aee9554991" -dependencies = [ - "byteorder", - "bytes", - "rand 0.8.5", - "rtp", - "thiserror", -] - -[[package]] -name = "webrtc-sctp" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d47adcd9427eb3ede33d5a7f3424038f63c965491beafcc20bc650a2f6679c0" -dependencies = [ - "arc-swap", - "async-trait", - "bytes", - "crc", - "log", - "rand 0.8.5", - "thiserror", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-srtp" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6183edc4c1c6c0175f8812eefdce84dfa0aea9c3ece71c2bf6ddd3c964de3da5" -dependencies = [ - "aead 0.4.3", - "aes 0.7.5", - "aes-gcm 0.9.4", - "async-trait", - "byteorder", - "bytes", - "ctr 0.8.0", - "hmac 0.11.0", - "log", - "rtcp", - "rtp", - "sha-1 0.9.8", - "subtle", - "thiserror", - "tokio", - "webrtc-util", -] - -[[package]] -name = "webrtc-util" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f1db1727772c05cf7a2cfece52c3aca8045ca1e176cd517d323489aa3c6d87" -dependencies = [ - "async-trait", - "bitflags", - "bytes", - "cc", - "ipnet", - "lazy_static", - "libc", - "log", - "nix 0.24.3", - "rand 0.8.5", - "thiserror", - "tokio", - "winapi", -] - -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -9528,9 +8870,9 @@ checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" [[package]] name = "widestring" -version = "0.5.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[package]] name = "wildmatch" @@ -9588,7 +8930,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -9603,21 +8945,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -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", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -9633,7 +8960,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -9653,17 +8980,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9674,9 +9001,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9692,9 +9019,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9710,9 +9037,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9728,9 +9055,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9746,9 +9073,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9758,9 +9085,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9776,17 +9103,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -9800,7 +9137,7 @@ dependencies = [ "js-sys", "log", "pharos", - "rustc_version 0.4.0", + "rustc_version", "send_wrapper", "thiserror", "wasm-bindgen", @@ -9834,59 +9171,28 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x25519-dalek" -version = "2.0.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" -dependencies = [ - "curve25519-dalek 3.2.0", - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "x509-parser" -version = "0.13.2" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" dependencies = [ - "asn1-rs 0.3.1", - "base64 0.13.1", + "asn1-rs", "data-encoding", - "der-parser 7.0.0", + "der-parser", "lazy_static", - "nom 7.1.3", - "oid-registry 0.4.0", - "ring", + "nom", + "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.21", -] - -[[package]] -name = "x509-parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" -dependencies = [ - "asn1-rs 0.5.2", - "base64 0.13.1", - "data-encoding", - "der-parser 8.2.0", - "lazy_static", - "nom 7.1.3", - "oid-registry 0.6.1", - "rusticata-macros", - "thiserror", - "time 0.3.21", + "time", ] [[package]] name = "xml-rs" -version = "0.8.11" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" +checksum = "bab77e97b50aee93da431f2cee7cd0f43b4d1da3c408042f2d7d164187774f0a" [[package]] name = "xmltree" @@ -9908,15 +9214,16 @@ dependencies = [ [[package]] name = "yamux" -version = "0.10.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d9ba232399af1783a58d8eb26f6b5006fbefe2dc9ef36bd283324792d03ea5" +checksum = "0329ef377816896f014435162bb3711ea7a07729c23d0960e6f8048b21b8fe91" dependencies = [ "futures", "log", "nohash-hasher", "parking_lot 0.12.1", - "rand 0.8.5", + "pin-project", + "rand", "static_assertions", ] @@ -9926,7 +9233,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.21", + "time", ] [[package]] @@ -9946,19 +9253,55 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "zip" -version = "0.5.13" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ + "aes 0.8.3", "byteorder", "bzip2", + "constant_time_eq", "crc32fast", + "crossbeam-utils", "flate2", - "thiserror", - "time 0.1.45", + "hmac 0.12.1", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index bbe77d209..62c0e7bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "beacon_node", "beacon_node/beacon_chain", + "beacon_node/beacon_processor", "beacon_node/builder_client", "beacon_node/client", "beacon_node/eth1", @@ -35,6 +36,7 @@ members = [ "common/lru_cache", "common/malloc_utils", "common/oneshot_broadcast", + "common/pretty_reqwest_error", "common/sensitive_url", "common/slot_clock", "common/system_health", @@ -81,21 +83,144 @@ members = [ "validator_client", "validator_client/slashing_protection", + "validator_manager", + "watch", ] resolver = "2" -[patch] -[patch.crates-io] -warp = { git = "https://github.com/macladson/warp", rev="7e75acc368229a46a236a8c991bf251fe7fe50ef" } -arbitrary = { git = "https://github.com/michaelsproul/arbitrary", rev="f002b99989b561ddce62e4cf2887b0f8860ae991" } +[workspace.package] +edition = "2021" -[patch."https://github.com/ralexstokes/mev-rs"] -mev-rs = { git = "https://github.com/ralexstokes//mev-rs", rev = "7813d4a4a564e0754e9aaab2d95520ba437c3889" } -[patch."https://github.com/ralexstokes/ethereum-consensus"] -ethereum-consensus = { git = "https://github.com/ralexstokes//ethereum-consensus", rev = "9b0ee0a8a45b968c8df5e7e64ea1c094e16f053d" } -[patch."https://github.com/ralexstokes/ssz-rs"] -ssz-rs = { git = "https://github.com/ralexstokes//ssz-rs", rev = "adf1a0b14cef90b9536f28ef89da1fab316465e1" } +[workspace.dependencies] +arbitrary = { version = "1", features = ["derive"] } +bincode = "1" +bitvec = "1" +byteorder = "1" +bytes = "1" +clap = "2" +compare_fields_derive = { path = "common/compare_fields_derive" } +criterion = "0.3" +delay_map = "0.3" +derivative = "2" +dirs = "3" +discv5 = { version = "0.3", features = ["libp2p"] } +env_logger = "0.9" +error-chain = "0.12" +ethereum-types = "0.14" +ethereum_hashing = "1.0.0-beta.2" +ethereum_serde_utils = "0.5" +ethereum_ssz = "0.5" +ethereum_ssz_derive = "0.5" +ethers-core = "1" +ethers-providers = { version = "1", default-features = false } +exit-future = "0.2" +fnv = "1" +fs2 = "0.4" +futures = "0.3" +hex = "0.4" +hyper = "0.14" +itertools = "0.10" +lazy_static = "1" +libsecp256k1 = "0.7" +log = "0.4" +lru = "0.7" +maplit = "1" +num_cpus = "1" +parking_lot = "0.12" +paste = "1" +quickcheck = "1" +quickcheck_macros = "1" +quote = "1" +r2d2 = "0.8" +rand = "0.8" +rayon = "1.7" +regex = "1" +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] } +ring = "0.16" +rusqlite = { version = "0.28", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_repr = "0.1" +serde_yaml = "0.8" +sha2 = "0.9" +slog = { version = "2", features = ["max_level_trace", "release_max_level_trace"] } +slog-async = "2" +slog-term = "2" +sloggers = { version = "2", features = ["json"] } +smallvec = "1" +snap = "1" +ssz_types = "0.5" +strum = { version = "0.24", features = ["derive"] } +superstruct = "0.6" +syn = "1" +sysinfo = "0.26" +tempfile = "3" +tokio = { version = "1", features = ["rt-multi-thread", "sync"] } +tokio-stream = { version = "0.1", features = ["sync"] } +tokio-util = { version = "0.6", features = ["codec", "compat", "time"] } +tree_hash = "0.5" +tree_hash_derive = "0.5" +url = "2" +uuid = { version = "0.8", features = ["serde", "v4"] } +# TODO update to warp 0.3.6 after released. +warp = { git = "https://github.com/seanmonstar/warp.git", default-features = false, features = ["tls"] } +zeroize = { version = "1", features = ["zeroize_derive"] } +zip = "0.6" + +# Local crates. +account_utils = { path = "common/account_utils" } +beacon_chain = { path = "beacon_node/beacon_chain" } +beacon_node = { path = "beacon_node" } +beacon_processor = { path = "beacon_node/beacon_processor" } +bls = { path = "crypto/bls" } +cached_tree_hash = { path = "consensus/cached_tree_hash" } +clap_utils = { path = "common/clap_utils" } +compare_fields = { path = "common/compare_fields" } +deposit_contract = { path = "common/deposit_contract" } +directory = { path = "common/directory" } +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" } +filesystem = { path = "common/filesystem" } +fork_choice = { path = "consensus/fork_choice" } +genesis = { path = "beacon_node/genesis" } +http_api = { path = "beacon_node/http_api" } +int_to_bytes = { path = "consensus/int_to_bytes" } +lighthouse_metrics = { path = "common/lighthouse_metrics" } +lighthouse_network = { path = "beacon_node/lighthouse_network" } +lighthouse_version = { path = "common/lighthouse_version" } +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" } +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" } +slasher = { path = "slasher" } +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" } +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" } +warp_utils = { path = "common/warp_utils" } [profile.maxperf] inherits = "release" diff --git a/Cross.toml b/Cross.toml index 9c3e441cb..d5f7a5d50 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,5 +1,5 @@ [target.x86_64-unknown-linux-gnu] -dockerfile = './scripts/cross/Dockerfile' +pre-build = ["apt-get install -y cmake clang-3.9"] [target.aarch64-unknown-linux-gnu] -dockerfile = './scripts/cross/Dockerfile' +pre-build = ["apt-get install -y cmake clang-3.9"] diff --git a/Dockerfile b/Dockerfile index be01ad7c5..bcddef8a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM rust:1.68.2-bullseye AS builder -RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev protobuf-compiler +FROM rust:1.69.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 @@ -13,4 +13,4 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-reco ca-certificates \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /usr/local/cargo/bin/lighthouse /usr/local/bin/lighthouse +COPY --from=builder /usr/local/cargo/bin/lighthouse /usr/local/bin/lighthouse \ No newline at end of file diff --git a/Makefile b/Makefile index 89362d12d..7bed5732b 100644 --- a/Makefile +++ b/Makefile @@ -71,13 +71,13 @@ install-lcli: # optimized CPU functions that may not be available on some systems. This # results in a more portable binary with ~20% slower BLS verification. build-x86_64: - cross build --bin lighthouse --target x86_64-unknown-linux-gnu --features "modern,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" + cross build --bin lighthouse --target x86_64-unknown-linux-gnu --features "modern,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-x86_64-portable: - cross build --bin lighthouse --target x86_64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" + cross build --bin lighthouse --target x86_64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-aarch64: - cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" + cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-aarch64-portable: - cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" + cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary @@ -145,8 +145,9 @@ test-op-pool-%: # Run the tests in the `slasher` crate for all supported database backends. test-slasher: - cargo test --release -p slasher --features mdbx - cargo test --release -p slasher --no-default-features --features lmdb + cargo test --release -p slasher --features lmdb + cargo test --release -p slasher --no-default-features --features mdbx + cargo test --release -p slasher --features lmdb,mdbx # both backends enabled # Runs only the tests/state_transition_vectors tests. run-state-transition-tests: @@ -169,7 +170,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - cargo clippy --workspace --tests -- \ + cargo clippy --workspace --tests $(EXTRA_CLIPPY_OPTS) -- \ -D clippy::fn_to_numeric_cast_any \ -D warnings \ -A clippy::derive_partial_eq_without_eq \ @@ -179,6 +180,10 @@ lint: -A clippy::question-mark \ -A clippy::uninlined-format-args +# Lints the code using Clippy and automatically fix some simple compiler warnings. +lint-fix: + EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint + nightly-lint: cp .github/custom/clippy.toml . cargo +$(CLIPPY_PINNED_NIGHTLY) clippy --workspace --tests --release -- \ @@ -202,7 +207,7 @@ arbitrary-fuzz: # Runs cargo audit (Audit Cargo.lock files for crates with security vulnerabilities reported to the RustSec Advisory Database) audit: cargo install --force cargo-audit - cargo audit --ignore RUSTSEC-2020-0071 + cargo audit # Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose. vendor: diff --git a/README.md b/README.md index 3565882d6..ade3bc2ab 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An open-source Ethereum consensus client, written in Rust and maintained by Sigm [Book Link]: https://lighthouse-book.sigmaprime.io [stable]: https://github.com/sigp/lighthouse/tree/stable [unstable]: https://github.com/sigp/lighthouse/tree/unstable -[blog]: https://lighthouse.sigmaprime.io +[blog]: https://lighthouse-blog.sigmaprime.io [Documentation](https://lighthouse-book.sigmaprime.io) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index ce863f914..0fab7b31f 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -1,29 +1,35 @@ [package] name = "account_manager" version = "0.3.5" -authors = ["Paul Hauner ", "Luke Anderson "] -edition = "2021" +authors = [ + "Paul Hauner ", + "Luke Anderson ", +] +edition = { workspace = true } [dependencies] -bls = { path = "../crypto/bls" } -clap = "2.33.3" -types = { path = "../consensus/types" } -environment = { path = "../lighthouse/environment" } -eth2_network_config = { path = "../common/eth2_network_config" } -clap_utils = { path = "../common/clap_utils" } -directory = { path = "../common/directory" } -eth2_wallet = { path = "../crypto/eth2_wallet" } +bls = { workspace = true } +clap = { workspace = true } +types = { workspace = true } +environment = { workspace = true } +eth2_network_config = { workspace = true } +clap_utils = { workspace = true } +directory = { workspace = true } +eth2_wallet = { workspace = true } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } -validator_dir = { path = "../common/validator_dir" } -tokio = { version = "1.14.0", features = ["full"] } -eth2_keystore = { path = "../crypto/eth2_keystore" } -account_utils = { path = "../common/account_utils" } -slashing_protection = { path = "../validator_client/slashing_protection" } -eth2 = {path = "../common/eth2"} -safe_arith = {path = "../consensus/safe_arith"} -slot_clock = { path = "../common/slot_clock" } -filesystem = { path = "../common/filesystem" } -sensitive_url = { path = "../common/sensitive_url" } +validator_dir = { workspace = true } +tokio = { workspace = true } +eth2_keystore = { workspace = true } +account_utils = { workspace = true } +slashing_protection = { workspace = true } +eth2 = { workspace = true } +safe_arith = { workspace = true } +slot_clock = { workspace = true } +filesystem = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +slog = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index ce42615e5..0764db21f 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,55 +1,7 @@ -use account_utils::PlainText; -use account_utils::{read_input_from_user, strip_off_newlines}; -use eth2_wallet::bip39::{Language, Mnemonic}; -use std::fs; -use std::path::PathBuf; -use std::str::from_utf8; -use std::thread::sleep; -use std::time::Duration; +use account_utils::read_input_from_user; -pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; pub const WALLET_NAME_PROMPT: &str = "Enter wallet name:"; -pub fn read_mnemonic_from_cli( - mnemonic_path: Option, - stdin_inputs: bool, -) -> Result { - let mnemonic = match mnemonic_path { - Some(path) => fs::read(&path) - .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) - .and_then(|bytes| { - let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); - let phrase = from_utf8(bytes_no_newlines.as_ref()) - .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; - Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { - format!( - "Unable to derive mnemonic from string {:?}: {:?}", - phrase, e - ) - }) - })?, - None => loop { - eprintln!(); - eprintln!("{}", MNEMONIC_PROMPT); - - let mnemonic = read_input_from_user(stdin_inputs)?; - - match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { - Ok(mnemonic_m) => { - eprintln!("Valid mnemonic provided."); - eprintln!(); - sleep(Duration::from_secs(1)); - break mnemonic_m; - } - Err(_) => { - eprintln!("Invalid mnemonic"); - } - } - }, - }; - Ok(mnemonic) -} - /// Reads in a wallet name from the user. If the `--wallet-name` flag is provided, use it. Otherwise /// read from an interactive prompt using tty unless the `--stdin-inputs` flag is provided. pub fn read_wallet_name_from_cli( diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 5755a355f..602390556 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -123,10 +123,8 @@ async fn publish_voluntary_exit( ) -> Result<(), String> { let genesis_data = get_geneisis_data(client).await?; let testnet_genesis_root = eth2_network_config - .beacon_state::() - .as_ref() - .expect("network should have valid genesis state") - .genesis_validators_root(); + .genesis_validators_root::()? + .ok_or("Genesis state is unknown")?; // Verify that the beacon node and validator being exited are on the same network. if genesis_data.genesis_validators_root != testnet_genesis_root { diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 8dc50a9df..339d9a291 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -4,8 +4,8 @@ use account_utils::{ eth2_keystore::Keystore, read_password_from_user, validator_definitions::{ - recursively_find_voting_keystores, ValidatorDefinition, ValidatorDefinitions, - CONFIG_FILENAME, + recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition, + ValidatorDefinitions, CONFIG_FILENAME, }, ZeroizeString, }; @@ -277,7 +277,9 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let suggested_fee_recipient = None; let validator_def = ValidatorDefinition::new_keystore_with_password( &dest_keystore, - password_opt, + password_opt + .map(PasswordStorage::ValidatorDefinitions) + .unwrap_or(PasswordStorage::None), graffiti, suggested_fee_recipient, None, diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index d9b05e775..33d3b1892 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,10 +1,9 @@ use super::create::STORE_WITHDRAW_FLAG; -use crate::common::read_mnemonic_from_cli; use crate::validator::create::COUNT_FLAG; use crate::wallet::create::STDIN_INPUTS_FLAG; use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; -use account_utils::random_password; +use account_utils::{random_password, read_mnemonic_from_cli}; use clap::{App, Arg, ArgMatches}; use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index f25bbd815..c6d81275a 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -7,7 +7,7 @@ use slashing_protection::{ use std::fs::File; use std::path::PathBuf; use std::str::FromStr; -use types::{BeaconState, Epoch, EthSpec, PublicKeyBytes, Slot}; +use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; pub const CMD: &str = "slashing-protection"; pub const IMPORT_CMD: &str = "import"; @@ -81,20 +81,13 @@ pub fn cli_run( validator_base_dir: PathBuf, ) -> Result<(), String> { let slashing_protection_db_path = validator_base_dir.join(SLASHING_PROTECTION_FILENAME); - let eth2_network_config = env .eth2_network_config .ok_or("Unable to get testnet configuration from the environment")?; let genesis_validators_root = eth2_network_config - .beacon_state::() - .map(|state: BeaconState| state.genesis_validators_root()) - .map_err(|e| { - format!( - "Unable to get genesis state, has genesis occurred? Detail: {:?}", - e - ) - })?; + .genesis_validators_root::()? + .ok_or_else(|| "Unable to get genesis state, has genesis occurred?".to_string())?; match matches.subcommand() { (IMPORT_CMD, Some(matches)) => { diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index f107c3638..6e047aca8 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,6 +1,6 @@ -use crate::common::read_mnemonic_from_cli; use crate::wallet::create::{create_wallet_from_mnemonic, STDIN_INPUTS_FLAG}; use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use account_utils::read_mnemonic_from_cli; use clap::{App, Arg, ArgMatches}; use std::path::PathBuf; diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index acf373070..073a0d170 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,8 +1,11 @@ [package] name = "beacon_node" -version = "4.2.0" -authors = ["Paul Hauner ", "Age Manning ", + "Age Manning ", "Age Manning "] -edition = "2021" +edition = { workspace = true } autotests = false # using a single test binary compiles faster [features] @@ -12,59 +12,59 @@ participation_metrics = [] # Exposes validator participation metrics to Prometh fork_from_env = [] # Initialise the harness chain spec from the FORK_NAME env variable [dev-dependencies] -maplit = "1.0.2" -environment = { path = "../../lighthouse/environment" } -serde_json = "1.0.58" +maplit = { workspace = true } +environment = { workspace = true } +serde_json = { workspace = true } [dependencies] -merkle_proof = { path = "../../consensus/merkle_proof" } -store = { path = "../store" } -parking_lot = "0.12.0" -lazy_static = "1.4.0" -smallvec = "1.6.1" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -operation_pool = { path = "../operation_pool" } -rayon = "1.4.1" -serde = "1.0.116" -serde_derive = "1.0.116" -slog = { version = "2.5.2", features = ["max_level_trace"] } -sloggers = { version = "2.1.1", features = ["json"] } -slot_clock = { path = "../../common/slot_clock" } -ethereum_hashing = "1.0.0-beta.2" -ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" -ethereum_ssz_derive = "0.5.0" -state_processing = { path = "../../consensus/state_processing" } -tree_hash = "0.5.0" -types = { path = "../../consensus/types" } -tokio = "1.14.0" -tokio-stream = "0.1.3" -eth1 = { path = "../eth1" } -futures = "0.3.7" -genesis = { path = "../genesis" } -int_to_bytes = { path = "../../consensus/int_to_bytes" } -rand = "0.8.5" -proto_array = { path = "../../consensus/proto_array" } -lru = "0.7.1" -tempfile = "3.1.0" -bitvec = "0.20.4" -bls = { path = "../../crypto/bls" } -safe_arith = { path = "../../consensus/safe_arith" } -fork_choice = { path = "../../consensus/fork_choice" } -task_executor = { path = "../../common/task_executor" } -derivative = "2.1.1" -itertools = "0.10.0" -slasher = { path = "../../slasher", default-features = false } -eth2 = { path = "../../common/eth2" } -strum = { version = "0.24.0", features = ["derive"] } -logging = { path = "../../common/logging" } -execution_layer = { path = "../execution_layer" } -sensitive_url = { path = "../../common/sensitive_url" } -superstruct = "0.5.0" -hex = "0.4.2" -exit-future = "0.2.0" -unused_port = {path = "../../common/unused_port"} -oneshot_broadcast = { path = "../../common/oneshot_broadcast" } +merkle_proof = { workspace = true } +store = { workspace = true } +parking_lot = { workspace = true } +lazy_static = { workspace = true } +smallvec = { workspace = true } +lighthouse_metrics = { workspace = true } +operation_pool = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +ethereum_serde_utils = { workspace = true } +slog = { workspace = true } +sloggers = { workspace = true } +slot_clock = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum_ssz = { workspace = true } +ssz_types = { workspace = true } +ethereum_ssz_derive = { workspace = true } +state_processing = { workspace = true } +tree_hash_derive = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +eth1 = { workspace = true } +futures = { workspace = true } +genesis = { workspace = true } +int_to_bytes = { workspace = true } +rand = { workspace = true } +proto_array = { workspace = true } +lru = { workspace = true } +tempfile = { workspace = true } +bitvec = { workspace = true } +bls = { workspace = true } +safe_arith = { workspace = true } +fork_choice = { workspace = true } +task_executor = { workspace = true } +derivative = { workspace = true } +itertools = { workspace = true } +slasher = { workspace = true } +eth2 = { workspace = true } +strum = { workspace = true } +logging = { workspace = true } +execution_layer = { workspace = true } +sensitive_url = { workspace = true } +superstruct = { workspace = true } +hex = { workspace = true } +exit-future = { workspace = true } +oneshot_broadcast = { path = "../../common/oneshot_broadcast/" } [[test]] name = "beacon_chain_tests" diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index a4a661197..94bd28f98 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -3,7 +3,8 @@ use eth2::lighthouse::attestation_rewards::{IdealAttestationRewards, TotalAttest use eth2::lighthouse::StandardAttestationRewards; use participation_cache::ParticipationCache; use safe_arith::SafeArith; -use slog::{debug, Logger}; +use serde_utils::quoted_u64::Quoted; +use slog::debug; use state_processing::{ common::altair::BaseRewardPerIncrement, per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight}, @@ -15,32 +16,111 @@ use store::consts::altair::{ }; use types::consts::altair::WEIGHT_DENOMINATOR; -use types::{Epoch, EthSpec}; +use types::{BeaconState, Epoch, EthSpec}; use eth2::types::ValidatorId; +use state_processing::common::base::get_base_reward_from_effective_balance; +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, +}; +use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo; +use state_processing::per_epoch_processing::base::{ + TotalBalances, ValidatorStatus, ValidatorStatuses, +}; impl BeaconChain { pub fn compute_attestation_rewards( &self, epoch: Epoch, validators: Vec, - log: Logger, ) -> Result { - debug!(log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len()); + debug!(self.log, "computing attestation rewards"; "epoch" => epoch, "validator_count" => validators.len()); // Get state - let spec = &self.spec; - let state_slot = (epoch + 1).end_slot(T::EthSpec::slots_per_epoch()); let state_root = self .state_root_at_slot(state_slot)? .ok_or(BeaconChainError::NoStateForSlot(state_slot))?; - let mut state = self + let state = self .get_state(&state_root, Some(state_slot))? .ok_or(BeaconChainError::MissingBeaconState(state_root))?; + match state { + BeaconState::Base(_) => self.compute_attestation_rewards_base(state, validators), + BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => { + self.compute_attestation_rewards_altair(state, validators) + } + } + } + + fn compute_attestation_rewards_base( + &self, + mut state: BeaconState, + validators: Vec, + ) -> Result { + let spec = &self.spec; + let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; + validator_statuses.process_attestations(&state)?; + + let ideal_rewards = + self.compute_ideal_rewards_base(&state, &validator_statuses.total_balances)?; + + let indices_to_attestation_delta = if validators.is_empty() { + get_attestation_deltas_all(&state, &validator_statuses, spec)? + .into_iter() + .enumerate() + .collect() + } else { + let validator_indices = Self::validators_ids_to_indices(&mut state, validators)?; + get_attestation_deltas_subset(&state, &validator_statuses, &validator_indices, spec)? + }; + + let mut total_rewards = vec![]; + + for (index, delta) in indices_to_attestation_delta.into_iter() { + let head_delta = delta.head_delta; + let head = (head_delta.rewards as i64).safe_sub(head_delta.penalties as i64)?; + + let target_delta = delta.target_delta; + let target = (target_delta.rewards as i64).safe_sub(target_delta.penalties as i64)?; + + let source_delta = delta.source_delta; + let source = (source_delta.rewards as i64).safe_sub(source_delta.penalties as i64)?; + + // No penalties associated with inclusion delay + let inclusion_delay = delta.inclusion_delay_delta.rewards; + let inactivity = delta.inactivity_penalty_delta.penalties.wrapping_neg() as i64; + + let rewards = TotalAttestationRewards { + validator_index: index as u64, + head, + target, + source, + inclusion_delay: Some(Quoted { + value: inclusion_delay, + }), + inactivity, + }; + + total_rewards.push(rewards); + } + + Ok(StandardAttestationRewards { + ideal_rewards, + total_rewards, + }) + } + + fn compute_attestation_rewards_altair( + &self, + mut state: BeaconState, + validators: Vec, + ) -> Result { + let spec = &self.spec; + // Calculate ideal_rewards let participation_cache = ParticipationCache::new(&state, spec)?; @@ -71,7 +151,7 @@ impl BeaconChain { let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?; - for effective_balance_eth in 0..=32 { + for effective_balance_eth in 1..=self.max_effective_balance_increment_steps()? { let effective_balance = effective_balance_eth.safe_mul(spec.effective_balance_increment)?; let base_reward = @@ -86,7 +166,7 @@ impl BeaconChain { let ideal_reward = reward_numerator .safe_div(active_increments)? .safe_div(WEIGHT_DENOMINATOR)?; - if !state.is_in_inactivity_leak(previous_epoch, spec) { + if !state.is_in_inactivity_leak(previous_epoch, spec)? { ideal_rewards_hashmap .insert((flag_index, effective_balance), (ideal_reward, penalty)); } else { @@ -101,20 +181,12 @@ impl BeaconChain { let validators = if validators.is_empty() { participation_cache.eligible_validator_indices().to_vec() } else { - validators - .into_iter() - .map(|validator| match validator { - ValidatorId::Index(i) => Ok(i as usize), - ValidatorId::PublicKey(pubkey) => state - .get_validator_index(&pubkey)? - .ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)), - }) - .collect::, _>>()? + Self::validators_ids_to_indices(&mut state, validators)? }; for validator_index in &validators { let eligible = state.is_eligible_validator(previous_epoch, *validator_index)?; - let mut head_reward = 0u64; + let mut head_reward = 0i64; let mut target_reward = 0i64; let mut source_reward = 0i64; @@ -132,7 +204,7 @@ impl BeaconChain { .map_err(|_| BeaconChainError::AttestationRewardsError)?; if voted_correctly { if flag_index == TIMELY_HEAD_FLAG_INDEX { - head_reward += ideal_reward; + head_reward += *ideal_reward as i64; } else if flag_index == TIMELY_TARGET_FLAG_INDEX { target_reward += *ideal_reward as i64; } else if flag_index == TIMELY_SOURCE_FLAG_INDEX { @@ -152,6 +224,9 @@ impl BeaconChain { head: head_reward, target: target_reward, source: source_reward, + inclusion_delay: None, + // TODO: altair calculation logic needs to be updated to include inactivity penalty + inactivity: 0, }); } @@ -173,6 +248,9 @@ impl BeaconChain { head: 0, target: 0, source: 0, + inclusion_delay: None, + // TODO: altair calculation logic needs to be updated to include inactivity penalty + inactivity: 0, }); match *flag_index { TIMELY_SOURCE_FLAG_INDEX => entry.source += ideal_reward, @@ -192,4 +270,126 @@ impl BeaconChain { total_rewards, }) } + + fn max_effective_balance_increment_steps(&self) -> Result { + let spec = &self.spec; + let max_steps = spec + .max_effective_balance + .safe_div(spec.effective_balance_increment)?; + Ok(max_steps) + } + + fn validators_ids_to_indices( + state: &mut BeaconState, + validators: Vec, + ) -> Result, BeaconChainError> { + let indices = validators + .into_iter() + .map(|validator| match validator { + ValidatorId::Index(i) => Ok(i as usize), + ValidatorId::PublicKey(pubkey) => state + .get_validator_index(&pubkey)? + .ok_or(BeaconChainError::ValidatorPubkeyUnknown(pubkey)), + }) + .collect::, _>>()?; + Ok(indices) + } + + fn compute_ideal_rewards_base( + &self, + state: &BeaconState, + total_balances: &TotalBalances, + ) -> Result, BeaconChainError> { + let spec = &self.spec; + let previous_epoch = state.previous_epoch(); + let finality_delay = previous_epoch + .safe_sub(state.finalized_checkpoint().epoch)? + .as_u64(); + + let ideal_validator_status = ValidatorStatus { + is_previous_epoch_attester: true, + is_slashed: false, + inclusion_info: Some(InclusionInfo { + delay: 1, + ..Default::default() + }), + ..Default::default() + }; + + let mut ideal_attestation_rewards_list = Vec::new(); + + for effective_balance_step in 1..=self.max_effective_balance_increment_steps()? { + let effective_balance = + effective_balance_step.safe_mul(spec.effective_balance_increment)?; + let base_reward = get_base_reward_from_effective_balance::( + effective_balance, + total_balances.current_epoch(), + spec, + )?; + + // compute ideal head rewards + let head = get_attestation_component_delta( + true, + total_balances.previous_epoch_head_attesters(), + total_balances, + base_reward, + finality_delay, + spec, + )? + .rewards; + + // compute ideal target rewards + let target = get_attestation_component_delta( + true, + total_balances.previous_epoch_target_attesters(), + total_balances, + base_reward, + finality_delay, + spec, + )? + .rewards; + + // compute ideal source rewards + let source = get_attestation_component_delta( + true, + total_balances.previous_epoch_attesters(), + total_balances, + base_reward, + finality_delay, + spec, + )? + .rewards; + + // compute ideal inclusion delay rewards + let inclusion_delay = + get_inclusion_delay_delta(&ideal_validator_status, base_reward, spec)? + .0 + .rewards; + + // compute inactivity penalty + let inactivity = get_inactivity_penalty_delta( + &ideal_validator_status, + base_reward, + finality_delay, + spec, + )? + .penalties + .wrapping_neg() as i64; + + let ideal_attestation_rewards = IdealAttestationRewards { + effective_balance, + head, + target, + source, + inclusion_delay: Some(Quoted { + value: inclusion_delay, + }), + inactivity, + }; + + ideal_attestation_rewards_list.push(ideal_attestation_rewards); + } + + Ok(ideal_attestation_rewards_list) + } } diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 04f601fad..5535fec37 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -35,10 +35,8 @@ mod batch; use crate::{ - beacon_chain::{MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT}, - metrics, - observed_aggregates::ObserveOutcome, - observed_attesters::Error as ObservedAttestersError, + beacon_chain::VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, metrics, + observed_aggregates::ObserveOutcome, observed_attesters::Error as ObservedAttestersError, BeaconChain, BeaconChainError, BeaconChainTypes, }; use bls::verify_signature_sets; @@ -57,8 +55,8 @@ use std::borrow::Cow; use strum::AsRefStr; use tree_hash::TreeHash; use types::{ - Attestation, BeaconCommittee, CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, - SelectionProof, SignedAggregateAndProof, Slot, SubnetId, + Attestation, BeaconCommittee, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, + IndexedAttestation, SelectionProof, SignedAggregateAndProof, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -117,14 +115,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The attestation has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The attestation or a superset of this attestation's aggregations bits for the same data + /// has been seen before; either in a block, on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed it and do not /// need to observe it again. - AttestationAlreadyKnown(Hash256), + AttestationSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,7 +266,7 @@ enum CheckAttestationSignature { struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { signed_aggregate: &'a SignedAggregateAndProof, indexed_attestation: IndexedAttestation, - attestation_root: Hash256, + attestation_data_root: Hash256, } /// Wraps a `Attestation` that has been verified up until the point that an `IndexedAttestation` can @@ -454,7 +452,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. - verify_propagation_slot_range(&chain.slot_clock, attestation)?; + verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?; // Check the attestation's epoch matches its target. if attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()) @@ -467,14 +465,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } // Ensure the valid aggregated attestation has not already been seen locally. - let attestation_root = attestation.tree_hash_root(); + let attestation_data = &attestation.data; + let attestation_data_root = attestation_data.tree_hash_root(); + if chain .observed_attestations .write() - .is_known(attestation, attestation_root) + .is_known_subset(attestation, attestation_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } let aggregator_index = signed_aggregate.message.aggregator_index; @@ -520,7 +521,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { if attestation.aggregation_bits.is_zero() { Err(Error::EmptyAggregationBitfield) } else { - Ok(attestation_root) + Ok(attestation_data_root) } } @@ -533,7 +534,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { let attestation = &signed_aggregate.message.aggregate; let aggregator_index = signed_aggregate.message.aggregator_index; - let attestation_root = match Self::verify_early_checks(signed_aggregate, chain) { + let attestation_data_root = match Self::verify_early_checks(signed_aggregate, chain) { Ok(root) => root, Err(e) => return Err(SignatureNotChecked(&signed_aggregate.message.aggregate, e)), }; @@ -568,7 +569,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, }) } } @@ -577,7 +578,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { /// Run the checks that happen after the indexed attestation and signature have been checked. fn verify_late_checks( signed_aggregate: &SignedAggregateAndProof, - attestation_root: Hash256, + attestation_data_root: Hash256, chain: &BeaconChain, ) -> Result<(), Error> { let attestation = &signed_aggregate.message.aggregate; @@ -587,13 +588,14 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { // // It's important to double check that the attestation is not already known, otherwise two // attestations processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_attestations .write() - .observe_item(attestation, Some(attestation_root)) + .observe_item(attestation, Some(attestation_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::AttestationAlreadyKnown(attestation_root)); + metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); + return Err(Error::AttestationSupersetKnown(attestation_data_root)); } // Observe the aggregator so we don't process another aggregate from them. @@ -653,7 +655,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { let IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_root, + attestation_data_root, } = signed_aggregate; match check_signature { @@ -677,7 +679,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { CheckAttestationSignature::No => (), }; - if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_root, chain) { + if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_data_root, chain) { return Err(SignatureValid(indexed_attestation, e)); } @@ -718,7 +720,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. - verify_propagation_slot_range(&chain.slot_clock, attestation)?; + verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?; // Check to ensure that the attestation is "unaggregated". I.e., it has exactly one // aggregation bit set. @@ -1033,11 +1035,11 @@ fn verify_head_block_is_known( pub fn verify_propagation_slot_range( slot_clock: &S, attestation: &Attestation, + spec: &ChainSpec, ) -> Result<(), Error> { let attestation_slot = attestation.data.slot; - let latest_permissible_slot = slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; if attestation_slot > latest_permissible_slot { return Err(Error::FutureSlot { @@ -1048,7 +1050,7 @@ pub fn verify_propagation_slot_range( // Taking advantage of saturating subtraction on `Slot`. let earliest_permissible_slot = slot_clock - .now_with_past_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)? - E::slots_per_epoch(); if attestation_slot < earliest_permissible_slot { diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2fa04304f..958778350 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -63,7 +63,6 @@ use execution_layer::{ BlockProposalContents, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; -pub use fork_choice::CountUnrealized; use fork_choice::{ AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, InvalidationOperation, PayloadVerificationStatus, ResetPayloadStatuses, @@ -165,7 +164,7 @@ pub enum WhenSlotSkipped { /// /// This is how the HTTP API behaves. None, - /// If the slot it a skip slot, return the previous non-skipped block. + /// If the slot is a skip slot, return the previous non-skipped block. /// /// This is generally how the specification behaves. Prev, @@ -198,6 +197,17 @@ pub struct PrePayloadAttributes { pub parent_block_number: u64, } +/// Information about a state/block at a specific slot. +#[derive(Debug, Clone, Copy)] +pub struct FinalizationAndCanonicity { + /// True if the slot of the state or block is finalized. + /// + /// This alone DOES NOT imply that the state/block is finalized, use `self.is_finalized()`. + pub slot_is_finalized: bool, + /// True if the state or block is canonical at its slot. + pub canonical: bool, +} + /// Define whether a forkchoiceUpdate needs to be checked for an override (`Yes`) or has already /// been checked (`AlreadyApplied`). It is safe to specify `Yes` even if re-orgs are disabled. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -207,11 +217,6 @@ pub enum OverrideForkchoiceUpdate { AlreadyApplied, } -/// The accepted clock drift for nodes gossiping blocks and attestations. See: -/// -/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#configuration -pub const MAXIMUM_GOSSIP_CLOCK_DISPARITY: Duration = Duration::from_millis(500); - #[derive(Debug, PartialEq)] pub enum AttestationProcessingOutcome { Processed, @@ -427,6 +432,12 @@ pub struct BeaconChain { type BeaconBlockAndState = (BeaconBlock, BeaconState); +impl FinalizationAndCanonicity { + pub fn is_finalized(self) -> bool { + self.slot_is_finalized && self.canonical + } +} + impl BeaconChain { /// Checks if a block is finalized. /// The finalization check is done with the block slot. The block root is used to verify that @@ -456,16 +467,30 @@ impl BeaconChain { state_root: &Hash256, state_slot: Slot, ) -> Result { + self.state_finalization_and_canonicity(state_root, state_slot) + .map(FinalizationAndCanonicity::is_finalized) + } + + /// Fetch the finalization and canonicity status of the state with `state_root`. + pub fn state_finalization_and_canonicity( + &self, + state_root: &Hash256, + state_slot: Slot, + ) -> Result { let finalized_slot = self .canonical_head .cached_head() .finalized_checkpoint() .epoch .start_slot(T::EthSpec::slots_per_epoch()); - let is_canonical = self + let slot_is_finalized = state_slot <= finalized_slot; + let canonical = self .state_root_at_slot(state_slot)? .map_or(false, |canonical_root| state_root == &canonical_root); - Ok(state_slot <= finalized_slot && is_canonical) + Ok(FinalizationAndCanonicity { + slot_is_finalized, + canonical, + }) } /// Persists the head tracker and fork choice. @@ -784,10 +809,10 @@ impl BeaconChain { /// /// May return a database error. pub fn state_root_at_slot(&self, request_slot: Slot) -> Result, Error> { - if request_slot > self.slot()? { - return Ok(None); - } else if request_slot == self.spec.genesis_slot { + if request_slot == self.spec.genesis_slot { return Ok(Some(self.genesis_state_root)); + } else if request_slot > self.slot()? { + return Ok(None); } // Check limits w.r.t historic state bounds. @@ -864,10 +889,10 @@ impl BeaconChain { /// /// May return a database error. fn block_root_at_slot_skips_none(&self, request_slot: Slot) -> Result, Error> { - if request_slot > self.slot()? { - return Ok(None); - } else if request_slot == self.spec.genesis_slot { + if request_slot == self.spec.genesis_slot { return Ok(Some(self.genesis_block_root)); + } else if request_slot > self.slot()? { + return Ok(None); } let prev_slot = request_slot.saturating_sub(1_u64); @@ -927,10 +952,10 @@ impl BeaconChain { /// /// May return a database error. fn block_root_at_slot_skips_prev(&self, request_slot: Slot) -> Result, Error> { - if request_slot > self.slot()? { - return Ok(None); - } else if request_slot == self.spec.genesis_slot { + if request_slot == self.spec.genesis_slot { return Ok(Some(self.genesis_block_root)); + } else if request_slot > self.slot()? { + return Ok(None); } // Try an optimized path of reading the root directly from the head state. @@ -2510,7 +2535,6 @@ impl BeaconChain { pub async fn process_chain_segment( self: &Arc, chain_segment: Vec>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { let mut imported_blocks = 0; @@ -2579,8 +2603,8 @@ impl BeaconChain { .process_block( signature_verified_block.block_root(), signature_verified_block, - count_unrealized, notify_execution_layer, + || Ok(()), ) .await { @@ -2668,8 +2692,8 @@ impl BeaconChain { self: &Arc, block_root: Hash256, unverified_block: B, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, + publish_fn: impl FnOnce() -> Result<(), BlockError> + Send + 'static, ) -> Result> { // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); @@ -2688,8 +2712,9 @@ impl BeaconChain { &chain, notify_execution_layer, )?; + publish_fn()?; chain - .import_execution_pending_block(execution_pending, count_unrealized) + .import_execution_pending_block(execution_pending) .await }; @@ -2729,7 +2754,7 @@ impl BeaconChain { } // The block failed verification. Err(other) => { - trace!( + debug!( self.log, "Beacon block rejected"; "reason" => other.to_string(), @@ -2744,10 +2769,9 @@ 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). - async fn import_execution_pending_block( + pub async fn import_execution_pending_block( self: Arc, execution_pending_block: ExecutionPendingBlock, - count_unrealized: CountUnrealized, ) -> Result> { let ExecutionPendingBlock { block, @@ -2808,7 +2832,6 @@ impl BeaconChain { state, confirmed_state_roots, payload_verification_status, - count_unrealized, parent_block, parent_eth1_finalization_data, consensus_context, @@ -2834,7 +2857,6 @@ impl BeaconChain { mut state: BeaconState, confirmed_state_roots: Vec, payload_verification_status: PayloadVerificationStatus, - count_unrealized: CountUnrealized, parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, @@ -2902,8 +2924,9 @@ impl BeaconChain { block_delay, &state, payload_verification_status, + self.config.progressive_balances_mode, &self.spec, - count_unrealized, + &self.log, ) .map_err(|e| BlockError::BeaconChainError(e.into()))?; } @@ -4633,6 +4656,7 @@ impl BeaconChain { self.log, "Produced block on state"; "block_size" => block_size, + "slot" => block.slot(), ); metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); @@ -5548,14 +5572,16 @@ impl BeaconChain { let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { (state, state_root) } else { - let state_root = head_block.state_root; - let state = self + let block_state_root = head_block.state_root; + let max_slot = shuffling_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let (state_root, state) = self .store .get_inconsistent_state_for_attestation_verification_only( - &state_root, - Some(head_block.slot), + &head_block_root, + max_slot, + block_state_root, )? - .ok_or(Error::MissingBeaconState(head_block.state_root))?; + .ok_or(Error::MissingBeaconState(block_state_root))?; (state, state_root) }; @@ -5707,13 +5733,9 @@ impl BeaconChain { /// Since we are likely calling this during the slot we are going to propose in, don't take into /// account the current slot when accounting for skips. pub fn is_healthy(&self, parent_root: &Hash256) -> Result { + let cached_head = self.canonical_head.cached_head(); // Check if the merge has been finalized. - if let Some(finalized_hash) = self - .canonical_head - .cached_head() - .forkchoice_update_parameters() - .finalized_hash - { + if let Some(finalized_hash) = cached_head.forkchoice_update_parameters().finalized_hash { if ExecutionBlockHash::zero() == finalized_hash { return Ok(ChainHealth::PreMerge); } @@ -5740,17 +5762,13 @@ impl BeaconChain { // Check slots at the head of the chain. let prev_slot = current_slot.saturating_sub(Slot::new(1)); - let head_skips = prev_slot.saturating_sub(self.canonical_head.cached_head().head_slot()); + let head_skips = prev_slot.saturating_sub(cached_head.head_slot()); let head_skips_check = head_skips.as_usize() <= self.config.builder_fallback_skips; // Check if finalization is advancing. let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - let epochs_since_finalization = current_epoch.saturating_sub( - self.canonical_head - .cached_head() - .finalized_checkpoint() - .epoch, - ); + let epochs_since_finalization = + current_epoch.saturating_sub(cached_head.finalized_checkpoint().epoch); let finalization_check = epochs_since_finalization.as_usize() <= self.config.builder_fallback_epochs_since_finalization; 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 9b2edbd8b..2a42b49b4 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -321,9 +321,17 @@ where .deconstruct() .0; - let state = self + let max_slot = self + .justified_checkpoint + .epoch + .start_slot(E::slots_per_epoch()); + let (_, state) = self .store - .get_state(&justified_block.state_root(), Some(justified_block.slot())) + .get_advanced_hot_state( + self.justified_checkpoint.root, + max_slot, + justified_block.state_root(), + ) .map_err(Error::FailedToReadState)? .ok_or_else(|| Error::MissingState(justified_block.state_root()))?; diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index e76a5a805..eae71bd63 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -135,7 +135,7 @@ impl BeaconProposerCache { /// Compute the proposer duties using the head state without cache. pub fn compute_proposer_duties_from_head( - current_epoch: Epoch, + request_epoch: Epoch, chain: &BeaconChain, ) -> Result<(Vec, Hash256, ExecutionStatus, Fork), BeaconChainError> { // Atomically collect information about the head whilst holding the canonical head `Arc` as @@ -159,7 +159,7 @@ 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, current_epoch, &chain.spec)?; + ensure_state_is_in_epoch(&mut state, head_state_root, request_epoch, &chain.spec)?; let indices = state .get_beacon_proposer_indices(&chain.spec) diff --git a/beacon_node/beacon_chain/src/beacon_snapshot.rs b/beacon_node/beacon_chain/src/beacon_snapshot.rs index 7d89df982..afb132477 100644 --- a/beacon_node/beacon_chain/src/beacon_snapshot.rs +++ b/beacon_node/beacon_chain/src/beacon_snapshot.rs @@ -1,4 +1,4 @@ -use serde_derive::Serialize; +use serde::Serialize; use std::sync::Arc; use types::{ beacon_state::CloneConfig, AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index ca4df864d..ef7f1b339 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -52,13 +52,14 @@ use crate::execution_payload::{ is_optimistic_candidate_block, validate_execution_payload_for_gossip, validate_merge_block, AllowOptimisticImport, NotifyExecutionLayer, PayloadNotifier, }; +use crate::observed_block_producers::SeenBlock; use crate::snapshot_cache::PreProcessingSnapshot; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ beacon_chain::{ BeaconForkChoice, ForkChoiceError, BLOCK_PROCESSING_CACHE_LOCK_TIMEOUT, - MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, + VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, }, metrics, BeaconChain, BeaconChainError, BeaconChainTypes, }; @@ -141,8 +142,6 @@ pub enum BlockError { /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. ParentUnknown(Arc>), - /// The block skips too many slots and is a DoS risk. - TooManySkippedSlots { parent_slot: Slot, block_slot: Slot }, /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -183,13 +182,6 @@ pub enum BlockError { /// /// The block is valid and we have already imported a block with this hash. BlockIsAlreadyKnown, - /// A block for this proposer and slot has already been observed. - /// - /// ## Peer scoring - /// - /// The `proposer` has already proposed a block at this slot. The existing block may or may not - /// be equal to the given block. - RepeatProposal { proposer: u64, slot: Slot }, /// The block slot exceeds the MAXIMUM_BLOCK_SLOT_NUMBER. /// /// ## Peer scoring @@ -285,6 +277,13 @@ pub enum BlockError { /// problems to worry about than losing peers, and we're doing the network a favour by /// disconnecting. ParentExecutionPayloadInvalid { parent_root: Hash256 }, + /// The block is a slashable equivocation from the proposer. + /// + /// ## Peer scoring + /// + /// Honest peers shouldn't forward more than 1 equivocating block from the same proposer, so + /// we penalise them with a mid-tolerance error. + Slashable, } /// Returned when block validation failed due to some issue verifying @@ -633,6 +632,40 @@ pub struct ExecutionPendingBlock { pub payload_verification_handle: PayloadVerificationHandle, } +pub trait IntoGossipVerifiedBlock: Sized { + fn into_gossip_verified_block( + self, + chain: &BeaconChain, + ) -> Result, BlockError>; + fn inner(&self) -> Arc>; +} + +impl IntoGossipVerifiedBlock for GossipVerifiedBlock { + fn into_gossip_verified_block( + self, + _chain: &BeaconChain, + ) -> Result, BlockError> { + Ok(self) + } + + fn inner(&self) -> Arc> { + self.block.clone() + } +} + +impl IntoGossipVerifiedBlock for Arc> { + fn into_gossip_verified_block( + self, + chain: &BeaconChain, + ) -> Result, BlockError> { + GossipVerifiedBlock::new(self, chain) + } + + fn inner(&self) -> Arc> { + self.clone() + } +} + /// Implemented on types that can be converted into a `ExecutionPendingBlock`. /// /// Used to allow functions to accept blocks at various stages of verification. @@ -697,7 +730,7 @@ impl GossipVerifiedBlock { // Do not gossip or process blocks from future slots. let present_slot_with_tolerance = chain .slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; if block.slot() > present_slot_with_tolerance { return Err(BlockError::FutureSlot { @@ -721,35 +754,16 @@ impl GossipVerifiedBlock { // reboot if the `observed_block_producers` cache is empty. In that case, without this // check, we will load the parent and state from disk only to find out later that we // already know this block. - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { + let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock(); + if fork_choice_read_lock.contains_block(&block_root) { return Err(BlockError::BlockIsAlreadyKnown); } - // Check that we have not already received a block with a valid signature for this slot. - if chain - .observed_block_producers - .read() - .proposer_has_been_observed(block.message()) - .map_err(|e| BlockError::BeaconChainError(e.into()))? - { - return Err(BlockError::RepeatProposal { - proposer: block.message().proposer_index(), - slot: block.slot(), - }); - } - // Do not process a block that doesn't descend from the finalized root. // // We check this *before* we load the parent so that we can return a more detailed error. - check_block_is_finalized_checkpoint_or_descendant( - chain, - &chain.canonical_head.fork_choice_write_lock(), - &block, - )?; + check_block_is_finalized_checkpoint_or_descendant(chain, &fork_choice_read_lock, &block)?; + drop(fork_choice_read_lock); let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = verify_parent_block_is_known(chain, block)?; @@ -786,9 +800,6 @@ impl GossipVerifiedBlock { parent_block.root }; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent_block.slot, block.message())?; - // 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 @@ -860,17 +871,16 @@ impl GossipVerifiedBlock { // // It's important to double-check that the proposer still hasn't been observed so we don't // have a race-condition when verifying two blocks simultaneously. - if chain + match chain .observed_block_producers .write() - .observe_proposer(block.message()) + .observe_proposal(block_root, block.message()) .map_err(|e| BlockError::BeaconChainError(e.into()))? { - return Err(BlockError::RepeatProposal { - proposer: block.message().proposer_index(), - slot: block.slot(), - }); - } + SeenBlock::Slashable => return Err(BlockError::Slashable), + SeenBlock::Duplicate => return Err(BlockError::BlockIsAlreadyKnown), + SeenBlock::UniqueNonSlashable => {} + }; if block.message().proposer_index() != expected_proposer as u64 { return Err(BlockError::IncorrectBlockProposer { @@ -942,9 +952,6 @@ impl SignatureVerifiedBlock { let (mut parent, block) = load_parent(block_root, block, chain)?; - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - let state = cheap_state_advance_to_obtain_committees( &mut parent.pre_state, parent.beacon_state_root, @@ -1109,6 +1116,12 @@ impl ExecutionPendingBlock { chain: &Arc>, notify_execution_layer: NotifyExecutionLayer, ) -> Result> { + chain + .observed_block_producers + .write() + .observe_proposal(block_root, block.message()) + .map_err(|e| BlockError::BeaconChainError(e.into()))?; + if let Some(parent) = chain .canonical_head .fork_choice_read_lock() @@ -1135,9 +1148,6 @@ impl ExecutionPendingBlock { return Err(BlockError::ParentUnknown(block)); } - // Reject any block that exceeds our limit on skipped slots. - check_block_skip_slots(chain, parent.beacon_block.slot(), block.message())?; - /* * Perform cursory checks to see if the block is even worth processing. */ @@ -1245,7 +1255,7 @@ impl ExecutionPendingBlock { // Perform a sanity check on the pre-state. let parent_slot = parent.beacon_block.slot(); - if state.slot() < parent_slot || state.slot() > parent_slot + 1 { + if state.slot() < parent_slot || state.slot() > block.slot() { return Err(BeaconChainError::BadPreState { parent_root: parent.beacon_block_root, parent_slot, @@ -1492,30 +1502,6 @@ impl ExecutionPendingBlock { } } -/// Check that the count of skip slots between the block and its parent does not exceed our maximum -/// value. -/// -/// Whilst this is not part of the specification, we include this to help prevent us from DoS -/// attacks. In times of dire network circumstance, the user can configure the -/// `import_max_skip_slots` value. -fn check_block_skip_slots( - chain: &BeaconChain, - parent_slot: Slot, - block: BeaconBlockRef<'_, T::EthSpec>, -) -> Result<(), BlockError> { - // Reject any block that exceeds our limit on skipped slots. - if let Some(max_skip_slots) = chain.config.import_max_skip_slots { - if block.slot() > parent_slot + max_skip_slots { - return Err(BlockError::TooManySkippedSlots { - parent_slot, - block_slot: block.slot(), - }); - } - } - - Ok(()) -} - /// Returns `Ok(())` if the block's slot is greater than the anchor block's slot (if any). fn check_block_against_anchor_slot( block: BeaconBlockRef<'_, T::EthSpec>, @@ -1768,13 +1754,18 @@ fn load_parent( BlockError::from(BeaconChainError::MissingBeaconBlock(block.parent_root())) })?; - // Load the parent blocks state from the database, returning an error if it is not found. + // Load the parent block's state from the database, returning an error if it is not found. // It is an error because if we know the parent block we should also know the parent state. - let parent_state_root = parent_block.state_root(); - let parent_state = chain - .get_state(&parent_state_root, Some(parent_block.slot()))? + // Retrieve any state that is advanced through to at most `block.slot()`: this is + // particularly important if `block` descends from the finalized/split block, but at a slot + // prior to the finalized slot (which is invalid and inaccessible in our DB schema). + let (parent_state_root, parent_state) = chain + .store + .get_advanced_hot_state(root, block.slot(), parent_block.state_root())? .ok_or_else(|| { - BeaconChainError::DBInconsistent(format!("Missing state {:?}", parent_state_root)) + BeaconChainError::DBInconsistent( + format!("Missing state for parent block {root:?}",), + ) })?; metrics::inc_counter(&metrics::BLOCK_PROCESSING_SNAPSHOT_CACHE_MISSES); diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b0f0015b9..54739f2b8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -18,14 +18,15 @@ use crate::{ }; use eth1::Config as Eth1Config; use execution_layer::ExecutionLayer; -use fork_choice::{CountUnrealized, ForkChoice, ResetPayloadStatuses}; +use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::RwLock; use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use slasher::Slasher; -use slog::{crit, error, info, Logger}; +use slog::{crit, debug, error, info, Logger}; use slot_clock::{SlotClock, TestingSlotClock}; +use state_processing::per_slot_processing; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; @@ -287,7 +288,7 @@ where let genesis_state = store .get_state(&genesis_block.state_root(), Some(genesis_block.slot())) .map_err(|e| descriptive_db_error("genesis state", &e))? - .ok_or("Genesis block not found in store")?; + .ok_or("Genesis state not found in store")?; self.genesis_time = Some(genesis_state.genesis_time()); @@ -338,7 +339,7 @@ where let beacon_block = genesis_block(&mut beacon_state, &self.spec)?; beacon_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Failed to build genesis state caches: {:?}", e))?; let beacon_state_root = beacon_block.message().state_root(); @@ -382,6 +383,16 @@ where 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))?, + ); + let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis) .map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?; let current_slot = None; @@ -408,46 +419,48 @@ where weak_subj_block: SignedBeaconBlock, genesis_state: BeaconState, ) -> Result { - let store = self.store.clone().ok_or("genesis_state requires a store")?; + let store = self + .store + .clone() + .ok_or("weak_subjectivity_state requires a store")?; + let log = self + .log + .as_ref() + .ok_or("weak_subjectivity_state requires a log")?; - let weak_subj_slot = weak_subj_state.slot(); - let weak_subj_block_root = weak_subj_block.canonical_root(); - let weak_subj_state_root = weak_subj_block.state_root(); - - // Check that the given block lies on an epoch boundary. Due to the database only storing - // full states on epoch boundaries and at restore points it would be difficult to support - // starting from a mid-epoch state. - if weak_subj_slot % TEthSpec::slots_per_epoch() != 0 { - return Err(format!( - "Checkpoint block at slot {} is not aligned to epoch start. \ - Please supply an aligned checkpoint with block.slot % 32 == 0", - weak_subj_block.slot(), - )); - } - - // Check that the block and state have consistent slots and state roots. - if weak_subj_state.slot() != weak_subj_block.slot() { - return Err(format!( - "Slot of snapshot block ({}) does not match snapshot state ({})", - weak_subj_block.slot(), - weak_subj_state.slot(), - )); + // Ensure the state is advanced to an epoch boundary. + let slots_per_epoch = TEthSpec::slots_per_epoch(); + if weak_subj_state.slot() % slots_per_epoch != 0 { + debug!( + log, + "Advancing checkpoint state to boundary"; + "state_slot" => weak_subj_state.slot(), + "block_slot" => weak_subj_block.slot(), + ); + while weak_subj_state.slot() % slots_per_epoch != 0 { + per_slot_processing(&mut weak_subj_state, None, &self.spec) + .map_err(|e| format!("Error advancing state: {e:?}"))?; + } } // Prime all caches before storing the state in the database and computing the tree hash // root. weak_subj_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Error building caches on checkpoint state: {e:?}"))?; - - let computed_state_root = weak_subj_state + let weak_subj_state_root = weak_subj_state .update_tree_hash_cache() .map_err(|e| format!("Error computing checkpoint state root: {:?}", e))?; - if weak_subj_state_root != computed_state_root { + let weak_subj_slot = weak_subj_state.slot(); + let weak_subj_block_root = weak_subj_block.canonical_root(); + + // Validate the state's `latest_block_header` against the checkpoint block. + let state_latest_block_root = weak_subj_state.get_latest_block_root(weak_subj_state_root); + if weak_subj_block_root != state_latest_block_root { return Err(format!( - "Snapshot state root does not match block, expected: {:?}, got: {:?}", - weak_subj_state_root, computed_state_root + "Snapshot state's most recent block root does not match block, expected: {:?}, got: {:?}", + weak_subj_block_root, state_latest_block_root )); } @@ -464,10 +477,25 @@ where // Set the store's split point *before* storing genesis so that genesis is stored // immediately in the freezer DB. - store.set_split(weak_subj_slot, weak_subj_state_root); + store.set_split(weak_subj_slot, weak_subj_state_root, weak_subj_block_root); let (_, updated_builder) = self.set_genesis_state(genesis_state)?; self = updated_builder; + // Fill in the linear block roots between the checkpoint block's slot and the aligned + // state's slot. All slots less than the block's slot will be handled by block backfill, + // while states greater or equal to the checkpoint state will be handled by `migrate_db`. + let block_root_batch = store + .store_frozen_block_root_at_skip_slots( + weak_subj_block.slot(), + weak_subj_state.slot(), + weak_subj_block_root, + ) + .map_err(|e| format!("Error writing frozen block roots: {e:?}"))?; + store + .cold_db + .do_atomically(block_root_batch) + .map_err(|e| format!("Error writing frozen block roots: {e:?}"))?; + // Write the state and block non-atomically, it doesn't matter if they're forgotten // about on a crash restart. store @@ -480,10 +508,11 @@ 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()) + .init_anchor_info(weak_subj_block.message(), retain_historic_states) .map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?, ); @@ -503,13 +532,12 @@ where let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot) .map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?; - let current_slot = Some(snapshot.beacon_block.slot()); let fork_choice = ForkChoice::from_anchor( fc_store, snapshot.beacon_block_root, &snapshot.beacon_block, &snapshot.beacon_state, - current_slot, + Some(weak_subj_slot), &self.spec, ) .map_err(|e| format!("Unable to initialize ForkChoice: {:?}", e))?; @@ -672,9 +700,8 @@ where Err(e) => return Err(descriptive_db_error("head block", &e)), }; - let head_state_root = head_block.state_root(); - let head_state = store - .get_state(&head_state_root, Some(head_block.slot())) + let (_head_state_root, head_state) = store + .get_advanced_hot_state(head_block_root, current_slot, head_block.state_root()) .map_err(|e| descriptive_db_error("head state", &e))? .ok_or("Head state not found in store")?; @@ -687,7 +714,8 @@ where store.clone(), Some(current_slot), &self.spec, - CountUnrealized::True, + self.chain_config.progressive_balances_mode, + &log, )?; } @@ -701,7 +729,7 @@ where head_snapshot .beacon_state - .build_all_caches(&self.spec) + .build_caches(&self.spec) .map_err(|e| format!("Failed to build state caches: {:?}", e))?; // Perform a check to ensure that the finalization points of the head and fork choice are @@ -827,7 +855,6 @@ where observed_sync_aggregators: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), - // TODO: allow for persisting and loading the pool from disk. observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 2b1f71436..7fa5b0152 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -47,7 +47,8 @@ use crate::{ }; use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead}; use fork_choice::{ - ExecutionStatus, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock, ResetPayloadStatuses, + ExecutionStatus, ForkChoiceStore, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock, + ResetPayloadStatuses, }; use itertools::process_results; use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -298,10 +299,10 @@ impl CanonicalHead { let beacon_block = store .get_full_block(&beacon_block_root)? .ok_or(Error::MissingBeaconBlock(beacon_block_root))?; - let beacon_state_root = beacon_block.state_root(); - let beacon_state = store - .get_state(&beacon_state_root, Some(beacon_block.slot()))? - .ok_or(Error::MissingBeaconState(beacon_state_root))?; + let current_slot = fork_choice.fc_store().get_current_slot(); + let (_, beacon_state) = store + .get_advanced_hot_state(beacon_block_root, current_slot, beacon_block.state_root())? + .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; let snapshot = BeaconSnapshot { beacon_block_root, @@ -669,10 +670,14 @@ impl BeaconChain { .get_full_block(&new_view.head_block_root)? .ok_or(Error::MissingBeaconBlock(new_view.head_block_root))?; - let beacon_state_root = beacon_block.state_root(); - let beacon_state: BeaconState = self - .get_state(&beacon_state_root, Some(beacon_block.slot()))? - .ok_or(Error::MissingBeaconState(beacon_state_root))?; + let (_, beacon_state) = self + .store + .get_advanced_hot_state( + new_view.head_block_root, + current_slot, + beacon_block.state_root(), + )? + .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; Ok(BeaconSnapshot { beacon_block: Arc::new(beacon_block), diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index a74fdced1..bccc3732c 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,7 +1,7 @@ pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use std::time::Duration; -use types::{Checkpoint, Epoch}; +use types::{Checkpoint, Epoch, ProgressiveBalancesMode}; pub const DEFAULT_RE_ORG_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); @@ -17,8 +17,7 @@ pub const FORK_CHOICE_LOOKAHEAD_FACTOR: u32 = 24; #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct ChainConfig { - /// Maximum number of slots to skip when importing a consensus message (e.g., block, - /// attestation, etc). + /// Maximum number of slots to skip when importing an attestation. /// /// If `None`, there is no limit. pub import_max_skip_slots: Option, @@ -80,8 +79,10 @@ pub struct ChainConfig { /// /// This is useful for block builders and testing. pub always_prepare_payload: bool, - /// Whether backfill sync processing should be rate-limited. - pub enable_backfill_rate_limiting: bool, + /// Whether to use `ProgressiveBalancesCache` in unrealized FFG progression calculation. + pub progressive_balances_mode: ProgressiveBalancesMode, + /// Number of epochs between each migration of data from the hot database to the freezer. + pub epochs_per_migration: u64, } impl Default for ChainConfig { @@ -111,7 +112,8 @@ impl Default for ChainConfig { shuffling_cache_size: crate::shuffling_cache::DEFAULT_CACHE_SIZE, genesis_backfill: false, always_prepare_payload: false, - enable_backfill_rate_limiting: true, + progressive_balances_mode: ProgressiveBalancesMode::Checked, + epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, } } } diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index e789b54a2..073761b0f 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -24,7 +24,7 @@ use state_processing::{ }, signature_sets::Error as SignatureSetError, state_advance::Error as StateAdvanceError, - BlockProcessingError, BlockReplayError, SlotProcessingError, + BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, }; use std::time::Duration; use task_executor::ShutdownReason; @@ -60,6 +60,7 @@ pub enum BeaconChainError { MissingBeaconBlock(Hash256), MissingBeaconState(Hash256), SlotProcessingError(SlotProcessingError), + EpochProcessingError(EpochProcessingError), StateAdvanceError(StateAdvanceError), UnableToAdvanceState(String), NoStateForAttestation { @@ -145,6 +146,8 @@ pub enum BeaconChainError { BlockVariantLacksExecutionPayload(Hash256), ExecutionLayerErrorPayloadReconstruction(ExecutionBlockHash, Box), EngineGetCapabilititesFailed(Box), + ExecutionLayerGetBlockByNumberFailed(Box), + ExecutionLayerGetBlockByHashFailed(Box), BlockHashMissingFromExecutionLayer(ExecutionBlockHash), InconsistentPayloadReconstructed { slot: Slot, @@ -213,9 +216,11 @@ pub enum BeaconChainError { BlsToExecutionConflictsWithPool, InconsistentFork(InconsistentFork), ProposerHeadForkChoiceError(fork_choice::Error), + UnableToPublish, } easy_from_to!(SlotProcessingError, BeaconChainError); +easy_from_to!(EpochProcessingError, BeaconChainError); easy_from_to!(AttestationValidationError, BeaconChainError); easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError); easy_from_to!(ExitValidationError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index fed050323..b267cc853 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -21,8 +21,11 @@ pub struct ServerSentEventHandler { } impl ServerSentEventHandler { - pub fn new(log: Logger) -> Self { - Self::new_with_capacity(log, DEFAULT_CHANNEL_CAPACITY) + pub fn new(log: Logger, capacity_multiplier: usize) -> Self { + Self::new_with_capacity( + log, + capacity_multiplier.saturating_mul(DEFAULT_CHANNEL_CAPACITY), + ) } pub fn new_with_capacity(log: Logger, capacity: usize) -> Self { diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index ccd17af24..dc0e34277 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,5 +1,5 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{CountUnrealized, ForkChoice, PayloadVerificationStatus}; +use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; use slog::{info, warn, Logger}; use state_processing::state_advance::complete_state_advance; @@ -10,7 +10,10 @@ use state_processing::{ use std::sync::Arc; use std::time::Duration; use store::{iter::ParentRootBlockIterator, HotColdDB, ItemStore}; -use types::{BeaconState, ChainSpec, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot}; +use types::{ + BeaconState, ChainSpec, EthSpec, ForkName, Hash256, ProgressiveBalancesMode, SignedBeaconBlock, + Slot, +}; const CORRUPT_DB_MESSAGE: &str = "The database could be corrupt. Check its file permissions or \ consider deleting it by running with the --purge-db flag."; @@ -100,7 +103,8 @@ pub fn reset_fork_choice_to_finalization, Cold: It store: Arc>, current_slot: Option, spec: &ChainSpec, - count_unrealized_config: CountUnrealized, + progressive_balances_mode: ProgressiveBalancesMode, + log: &Logger, ) -> Result, E>, String> { // Fetch finalized block. let finalized_checkpoint = head_state.finalized_checkpoint(); @@ -166,8 +170,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It .map_err(|e| format!("Error loading blocks to replay for fork choice: {:?}", e))?; let mut state = finalized_snapshot.beacon_state; - let blocks_len = blocks.len(); - for (i, block) in blocks.into_iter().enumerate() { + for block in blocks { complete_state_advance(&mut state, None, block.slot(), spec) .map_err(|e| format!("State advance failed: {:?}", e))?; @@ -190,15 +193,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It // This scenario is so rare that it seems OK to double-verify some blocks. let payload_verification_status = PayloadVerificationStatus::Optimistic; - // Because we are replaying a single chain of blocks, we only need to calculate unrealized - // justification for the last block in the chain. - let is_last_block = i + 1 == blocks_len; - let count_unrealized = if is_last_block { - count_unrealized_config - } else { - CountUnrealized::False - }; - fork_choice .on_block( block.slot(), @@ -208,8 +202,9 @@ pub fn reset_fork_choice_to_finalization, Cold: It Duration::from_secs(0), &state, payload_verification_status, + progressive_balances_mode, spec, - count_unrealized, + log, ) .map_err(|e| format!("Error applying replayed block to fork choice: {:?}", e))?; } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index be1522a3b..4ea1eeee0 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -52,9 +52,9 @@ pub mod validator_pubkey_cache; pub use self::beacon_chain::{ AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult, - CountUnrealized, ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, - StateSkipConfig, WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, MAXIMUM_GOSSIP_CLOCK_DISPARITY, + ForkChoiceError, 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; @@ -64,6 +64,7 @@ pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, GossipVerifiedBlock, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; @@ -72,6 +73,7 @@ pub use execution_layer::EngineState; pub use execution_payload::NotifyExecutionLayer; pub use fork_choice::{ExecutionStatus, ForkchoiceUpdateParameters}; pub use metrics::scrape_for_metrics; +pub use migrate::MigratorConfig; pub use parking_lot; pub use slot_clock; pub use state_processing::per_block_processing::errors::{ 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 7c431ebcc..638d2b401 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,6 +1,4 @@ -use crate::{ - beacon_chain::MAXIMUM_GOSSIP_CLOCK_DISPARITY, BeaconChain, BeaconChainError, BeaconChainTypes, -}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use derivative::Derivative; use slot_clock::SlotClock; use std::time::Duration; @@ -103,7 +101,8 @@ impl VerifiedLightClientFinalityUpdate { // verify that enough time has passed for the block to have been propagated match start_time { Some(time) => { - if seen_timestamp + MAXIMUM_GOSSIP_CLOCK_DISPARITY < time + one_third_slot_duration + if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() + < time + one_third_slot_duration { return Err(Error::TooEarly); } 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 20d718180..2d1a5cf97 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,6 +1,4 @@ -use crate::{ - beacon_chain::MAXIMUM_GOSSIP_CLOCK_DISPARITY, BeaconChain, BeaconChainError, BeaconChainTypes, -}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use derivative::Derivative; use eth2::types::Hash256; use slot_clock::SlotClock; @@ -103,7 +101,8 @@ impl VerifiedLightClientOptimisticUpdate { // verify that enough time has passed for the block to have been propagated match start_time { Some(time) => { - if seen_timestamp + MAXIMUM_GOSSIP_CLOCK_DISPARITY < time + one_third_slot_duration + if seen_timestamp + chain.spec.maximum_gossip_clock_disparity() + < time + one_third_slot_duration { return Err(Error::TooEarly); } diff --git a/beacon_node/beacon_chain/src/merge_readiness.rs b/beacon_node/beacon_chain/src/merge_readiness.rs index c66df39ee..52a5ea912 100644 --- a/beacon_node/beacon_chain/src/merge_readiness.rs +++ b/beacon_node/beacon_chain/src/merge_readiness.rs @@ -1,8 +1,10 @@ //! Provides tools for checking if a node is ready for the Bellatrix upgrade and following merge //! transition. -use crate::{BeaconChain, BeaconChainTypes}; +use crate::{BeaconChain, BeaconChainError as Error, BeaconChainTypes}; +use execution_layer::BlockByNumberQuery; use serde::{Deserialize, Serialize, Serializer}; +use slog::debug; use std::fmt; use std::fmt::Write; use types::*; @@ -86,9 +88,6 @@ pub enum MergeReadiness { #[serde(serialize_with = "serialize_uint256")] current_difficulty: Option, }, - /// The transition configuration with the EL failed, there might be a problem with - /// connectivity, authentication or a difference in configuration. - ExchangeTransitionConfigurationFailed { error: String }, /// The EL can be reached and has the correct configuration, however it's not yet synced. NotSynced, /// The user has not configured this node to use an execution endpoint. @@ -109,12 +108,6 @@ impl fmt::Display for MergeReadiness { params, current_difficulty ) } - MergeReadiness::ExchangeTransitionConfigurationFailed { error } => write!( - f, - "Could not confirm the transition configuration with the \ - execution endpoint: {:?}", - error - ), MergeReadiness::NotSynced => write!( f, "The execution endpoint is connected and configured, \ @@ -129,6 +122,25 @@ impl fmt::Display for MergeReadiness { } } +pub enum GenesisExecutionPayloadStatus { + Correct(ExecutionBlockHash), + BlockHashMismatch { + got: ExecutionBlockHash, + expected: ExecutionBlockHash, + }, + TransactionsRootMismatch { + got: Hash256, + expected: Hash256, + }, + WithdrawalsRootMismatch { + got: Hash256, + expected: Hash256, + }, + OtherMismatch, + Irrelevant, + AlreadyHappened, +} + impl BeaconChain { /// Returns `true` if user has an EL configured, or if the Bellatrix fork has occurred or will /// occur within `MERGE_READINESS_PREPARATION_SECONDS`. @@ -153,17 +165,9 @@ impl BeaconChain { } /// Attempts to connect to the EL and confirm that it is ready for the merge. - pub async fn check_merge_readiness(&self) -> MergeReadiness { + pub async fn check_merge_readiness(&self, current_slot: Slot) -> MergeReadiness { if let Some(el) = self.execution_layer.as_ref() { - if let Err(e) = el.exchange_transition_configuration(&self.spec).await { - // The EL was either unreachable, responded with an error or has a different - // configuration. - return MergeReadiness::ExchangeTransitionConfigurationFailed { - error: format!("{:?}", e), - }; - } - - if !el.is_synced_for_notifier().await { + if !el.is_synced_for_notifier(current_slot).await { // The EL is not synced. return MergeReadiness::NotSynced; } @@ -178,6 +182,91 @@ impl BeaconChain { MergeReadiness::NoExecutionEndpoint } } + + /// Check that the execution payload embedded in the genesis state matches the EL's genesis + /// block. + pub async fn check_genesis_execution_payload_is_correct( + &self, + ) -> Result { + let head_snapshot = self.head_snapshot(); + let genesis_state = &head_snapshot.beacon_state; + + if genesis_state.slot() != 0 { + return Ok(GenesisExecutionPayloadStatus::AlreadyHappened); + } + + let Ok(latest_execution_payload_header) = genesis_state.latest_execution_payload_header() + else { + return Ok(GenesisExecutionPayloadStatus::Irrelevant); + }; + let fork = self.spec.fork_name_at_epoch(Epoch::new(0)); + + let execution_layer = self + .execution_layer + .as_ref() + .ok_or(Error::ExecutionLayerMissing)?; + let exec_block_hash = latest_execution_payload_header.block_hash(); + + // Use getBlockByNumber(0) to check that the block hash matches. + // At present, Geth does not respond to engine_getPayloadBodiesByRange before genesis. + let execution_block = execution_layer + .get_block_by_number(BlockByNumberQuery::Tag("0x0")) + .await + .map_err(|e| Error::ExecutionLayerGetBlockByNumberFailed(Box::new(e)))? + .ok_or(Error::BlockHashMissingFromExecutionLayer(exec_block_hash))?; + + if execution_block.block_hash != exec_block_hash { + return Ok(GenesisExecutionPayloadStatus::BlockHashMismatch { + got: execution_block.block_hash, + expected: exec_block_hash, + }); + } + + // Double-check the block by reconstructing it. + let execution_payload = execution_layer + .get_payload_by_hash_legacy(exec_block_hash, fork) + .await + .map_err(|e| Error::ExecutionLayerGetBlockByHashFailed(Box::new(e)))? + .ok_or(Error::BlockHashMissingFromExecutionLayer(exec_block_hash))?; + + // Verify payload integrity. + let header_from_payload = ExecutionPayloadHeader::from(execution_payload.to_ref()); + + let got_transactions_root = header_from_payload.transactions_root(); + let expected_transactions_root = latest_execution_payload_header.transactions_root(); + let got_withdrawals_root = header_from_payload.withdrawals_root().ok(); + let expected_withdrawals_root = latest_execution_payload_header.withdrawals_root().ok(); + + if got_transactions_root != expected_transactions_root { + return Ok(GenesisExecutionPayloadStatus::TransactionsRootMismatch { + got: got_transactions_root, + expected: expected_transactions_root, + }); + } + + if let Some(&expected) = expected_withdrawals_root { + if let Some(&got) = got_withdrawals_root { + if got != expected { + return Ok(GenesisExecutionPayloadStatus::WithdrawalsRootMismatch { + got, + expected, + }); + } + } + } + + if header_from_payload.to_ref() != latest_execution_payload_header { + debug!( + self.log, + "Genesis execution payload reconstruction failure"; + "consensus_node_header" => ?latest_execution_payload_header, + "execution_node_header" => ?header_from_payload + ); + return Ok(GenesisExecutionPayloadStatus::OtherMismatch); + } + + Ok(GenesisExecutionPayloadStatus::Correct(exec_block_hash)) + } } /// Utility function to serialize a Uint256 as a decimal string. diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index d0f695062..dff663ded 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -998,6 +998,17 @@ lazy_static! { "light_client_optimistic_update_verification_success_total", "Number of light client optimistic updates verified for gossip" ); + /* + * Aggregate subset metrics + */ + pub static ref SYNC_CONTRIBUTION_SUBSETS: Result = try_create_int_counter( + "beacon_sync_contribution_subsets_total", + "Count of new sync contributions that are subsets of already known aggregates" + ); + pub static ref AGGREGATED_ATTESTATION_SUBSETS: Result = try_create_int_counter( + "beacon_aggregated_attestation_subsets_total", + "Count of new aggregated attestations that are subsets of already known aggregates" + ); } /// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot, diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 66f082742..6353a64e0 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -25,10 +25,15 @@ const MIN_COMPACTION_PERIOD_SECONDS: u64 = 7200; /// Compact after a large finality gap, if we respect `MIN_COMPACTION_PERIOD_SECONDS`. const COMPACTION_FINALITY_DISTANCE: u64 = 1024; +/// Default number of epochs to wait between finalization migrations. +pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; + /// The background migrator runs a thread to perform pruning and migrate state from the hot /// to the cold database. pub struct BackgroundMigrator, Cold: ItemStore> { db: Arc>, + /// Record of when the last migration ran, for enforcing `epochs_per_migration`. + prev_migration: Arc>, #[allow(clippy::type_complexity)] tx_thread: Option, thread::JoinHandle<()>)>>, /// Genesis block root, for persisting the `PersistedBeaconChain`. @@ -36,9 +41,22 @@ pub struct BackgroundMigrator, Cold: ItemStore> log: Logger, } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MigratorConfig { pub blocking: bool, + /// Run migrations at most once per `epochs_per_migration`. + /// + /// If set to 0 or 1, then run every finalization. + pub epochs_per_migration: u64, +} + +impl Default for MigratorConfig { + fn default() -> Self { + Self { + blocking: false, + epochs_per_migration: DEFAULT_EPOCHS_PER_MIGRATION, + } + } } impl MigratorConfig { @@ -46,6 +64,19 @@ impl MigratorConfig { self.blocking = true; self } + + pub fn epochs_per_migration(mut self, epochs_per_migration: u64) -> Self { + self.epochs_per_migration = epochs_per_migration; + self + } +} + +/// Record of when the last migration ran. +pub struct PrevMigration { + /// The epoch at which the last finalization migration ran. + epoch: Epoch, + /// The number of epochs to wait between runs. + epochs_per_migration: u64, } /// Pruning can be successful, or in rare cases deferred to a later point. @@ -92,6 +123,7 @@ pub struct FinalizationNotification { finalized_state_root: BeaconStateHash, finalized_checkpoint: Checkpoint, head_tracker: Arc, + prev_migration: Arc>, genesis_block_root: Hash256, } @@ -103,6 +135,11 @@ impl, Cold: ItemStore> BackgroundMigrator Self { + // Estimate last migration run from DB split slot. + let prev_migration = Arc::new(Mutex::new(PrevMigration { + epoch: db.get_split_slot().epoch(E::slots_per_epoch()), + epochs_per_migration: config.epochs_per_migration, + })); let tx_thread = if config.blocking { None } else { @@ -111,6 +148,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator prev_migration.epoch, + "new_finalized_epoch" => epoch, + "epochs_per_migration" => prev_migration.epochs_per_migration, + ); + return; + } + + // Update the previous migration epoch immediately to avoid holding the lock. If the + // migration doesn't succeed then the next migration will be retried at the next scheduled + // run. + prev_migration.epoch = epoch; + drop(prev_migration); + debug!(log, "Database consolidation started"); let finalized_state_root = notif.finalized_state_root; + let finalized_block_root = notif.finalized_checkpoint.root; let finalized_state = match db.get_state(&finalized_state_root.into(), None) { Ok(Some(state)) => state, @@ -260,7 +320,12 @@ impl, Cold: ItemStore> BackgroundMigrator {} Err(Error::HotColdDBError(HotColdDBError::FreezeSlotUnaligned(slot))) => { debug!( diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index bb0132f5f..18a761e29 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -1,7 +1,9 @@ //! Provides an `ObservedAggregates` struct which allows us to reject aggregated attestations or //! sync committee contributions if we've already seen them. -use std::collections::HashSet; +use crate::sync_committee_verification::SyncCommitteeData; +use ssz_types::{BitList, BitVector}; +use std::collections::HashMap; use std::marker::PhantomData; use tree_hash::TreeHash; use types::consts::altair::{ @@ -10,8 +12,16 @@ use types::consts::altair::{ use types::slot_data::SlotData; use types::{Attestation, EthSpec, Hash256, Slot, SyncCommitteeContribution}; -pub type ObservedSyncContributions = ObservedAggregates, E>; -pub type ObservedAggregateAttestations = ObservedAggregates, E>; +pub type ObservedSyncContributions = ObservedAggregates< + SyncCommitteeContribution, + E, + BitVector<::SyncSubcommitteeSize>, +>; +pub type ObservedAggregateAttestations = ObservedAggregates< + Attestation, + E, + BitList<::MaxValidatorsPerCommittee>, +>; /// A trait use to associate capacity constants with the type being stored in `ObservedAggregates`. pub trait Consts { @@ -69,10 +79,81 @@ impl Consts for SyncCommitteeContribution { } } +/// A trait for types that implement a behaviour where one object of that type +/// can be a subset/superset of another. +/// This trait allows us to be generic over the aggregate item that we store in the cache that +/// we want to prevent duplicates/subsets for. +pub trait SubsetItem { + /// The item that is stored for later comparison with new incoming aggregate items. + type Item; + + /// Returns `true` if `self` is a non-strict subset of `other` and `false` otherwise. + fn is_subset(&self, other: &Self::Item) -> bool; + + /// Returns `true` if `self` is a non-strict superset of `other` and `false` otherwise. + fn is_superset(&self, other: &Self::Item) -> bool; + + /// Returns the item that gets stored in `ObservedAggregates` for later subset + /// comparison with incoming aggregates. + fn get_item(&self) -> Self::Item; + + /// Returns a unique value that keys the object to the item that is being stored + /// in `ObservedAggregates`. + fn root(&self) -> Hash256; +} + +impl SubsetItem for Attestation { + type Item = BitList; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the attestation data. + fn root(&self) -> Hash256 { + self.data.tree_hash_root() + } +} + +impl SubsetItem for SyncCommitteeContribution { + type Item = BitVector; + fn is_subset(&self, other: &Self::Item) -> bool { + self.aggregation_bits.is_subset(other) + } + + fn is_superset(&self, other: &Self::Item) -> bool { + other.is_subset(&self.aggregation_bits) + } + + /// Returns the sync contribution aggregation bits. + fn get_item(&self) -> Self::Item { + self.aggregation_bits.clone() + } + + /// Returns the hash tree root of the root, slot and subcommittee index + /// of the sync contribution. + fn root(&self) -> Hash256 { + SyncCommitteeData { + root: self.beacon_block_root, + slot: self.slot, + subcommittee_index: self.subcommittee_index, + } + .tree_hash_root() + } +} + #[derive(Debug, PartialEq)] pub enum ObserveOutcome { - /// This item was already known. - AlreadyKnown, + /// This item is a non-strict subset of an already known item. + Subset, /// This was the first time this item was observed. New, } @@ -94,26 +175,28 @@ pub enum Error { }, } -/// A `HashSet` that contains entries related to some `Slot`. -struct SlotHashSet { - set: HashSet, +/// A `HashMap` that contains entries related to some `Slot`. +struct SlotHashSet { + /// Contains a vector of maximally-sized aggregation bitfields/bitvectors + /// such that no bitfield/bitvector is a subset of any other in the list. + map: HashMap>, slot: Slot, max_capacity: usize, } -impl SlotHashSet { +impl SlotHashSet { pub fn new(slot: Slot, initial_capacity: usize, max_capacity: usize) -> Self { Self { slot, - set: HashSet::with_capacity(initial_capacity), + map: HashMap::with_capacity(initial_capacity), max_capacity, } } /// Store the items in self so future observations recognise its existence. - pub fn observe_item( + pub fn observe_item>( &mut self, - item: &T, + item: &S, root: Hash256, ) -> Result { if item.get_slot() != self.slot { @@ -123,29 +206,45 @@ impl SlotHashSet { }); } - if self.set.contains(&root) { - Ok(ObserveOutcome::AlreadyKnown) - } else { - // Here we check to see if this slot has reached the maximum observation count. - // - // The resulting behaviour is that we are no longer able to successfully observe new - // items, however we will continue to return `is_known` values. We could also - // disable `is_known`, however then we would stop forwarding items across the - // gossip network and I think that this is a worse case than sending some invalid ones. - // The underlying libp2p network is responsible for removing duplicate messages, so - // this doesn't risk a broadcast loop. - if self.set.len() >= self.max_capacity { - return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + if let Some(aggregates) = self.map.get_mut(&root) { + for existing in aggregates { + // Check if `item` is a subset of any of the observed aggregates + if item.is_subset(existing) { + return Ok(ObserveOutcome::Subset); + // Check if `item` is a superset of any of the observed aggregates + // If true, we replace the new item with its existing subset. This allows us + // to hold fewer items in the list. + } else if item.is_superset(existing) { + *existing = item.get_item(); + return Ok(ObserveOutcome::New); + } } - - self.set.insert(root); - - Ok(ObserveOutcome::New) } + + // Here we check to see if this slot has reached the maximum observation count. + // + // The resulting behaviour is that we are no longer able to successfully observe new + // items, however we will continue to return `is_known_subset` values. We could also + // disable `is_known_subset`, however then we would stop forwarding items across the + // gossip network and I think that this is a worse case than sending some invalid ones. + // The underlying libp2p network is responsible for removing duplicate messages, so + // this doesn't risk a broadcast loop. + if self.map.len() >= self.max_capacity { + return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); + } + + let item = item.get_item(); + self.map.entry(root).or_default().push(item); + Ok(ObserveOutcome::New) } - /// Indicates if `item` has been observed before. - pub fn is_known(&self, item: &T, root: Hash256) -> Result { + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. + pub fn is_known_subset>( + &self, + item: &S, + root: Hash256, + ) -> Result { if item.get_slot() != self.slot { return Err(Error::IncorrectSlot { expected: self.slot, @@ -153,25 +252,28 @@ impl SlotHashSet { }); } - Ok(self.set.contains(&root)) + Ok(self + .map + .get(&root) + .map_or(false, |agg| agg.iter().any(|val| item.is_subset(val)))) } /// The number of observed items in `self`. pub fn len(&self) -> usize { - self.set.len() + self.map.len() } } /// Stores the roots of objects for some number of `Slots`, so we can determine if /// these have previously been seen on the network. -pub struct ObservedAggregates { +pub struct ObservedAggregates { lowest_permissible_slot: Slot, - sets: Vec, + sets: Vec>, _phantom_spec: PhantomData, _phantom_tree_hash: PhantomData, } -impl Default for ObservedAggregates { +impl Default for ObservedAggregates { fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), @@ -182,17 +284,17 @@ impl Default for ObservedAggregates } } -impl ObservedAggregates { - /// Store the root of `item` in `self`. +impl, E: EthSpec, I> ObservedAggregates { + /// Store `item` in `self` keyed at `root`. /// - /// `root` must equal `item.tree_hash_root()`. + /// `root` must equal `item.root::()`. pub fn observe_item( &mut self, item: &T, root_opt: Option, ) -> Result { let index = self.get_set_index(item.get_slot())?; - let root = root_opt.unwrap_or_else(|| item.tree_hash_root()); + let root = root_opt.unwrap_or_else(|| item.root()); self.sets .get_mut(index) @@ -200,17 +302,18 @@ impl ObservedAggregates { .and_then(|set| set.observe_item(item, root)) } - /// Check to see if the `root` of `item` is in self. + /// Check if `item` is a non-strict subset of any of the already observed aggregates for + /// the given root and slot. /// - /// `root` must equal `a.tree_hash_root()`. + /// `root` must equal `item.root::()`. #[allow(clippy::wrong_self_convention)] - pub fn is_known(&mut self, item: &T, root: Hash256) -> Result { + pub fn is_known_subset(&mut self, item: &T, root: Hash256) -> Result { let index = self.get_set_index(item.get_slot())?; self.sets .get(index) .ok_or(Error::InvalidSetIndex(index)) - .and_then(|set| set.is_known(item, root)) + .and_then(|set| set.is_known_subset(item, root)) } /// The maximum number of slots that items are stored for. @@ -296,7 +399,6 @@ impl ObservedAggregates { #[cfg(not(debug_assertions))] mod tests { use super::*; - use tree_hash::TreeHash; use types::{test_utils::test_random_instance, Hash256}; type E = types::MainnetEthSpec; @@ -330,7 +432,7 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(false), "should indicate an unknown attestation is unknown" ); @@ -343,13 +445,13 @@ mod tests { for a in &items { assert_eq!( - store.is_known(a, a.tree_hash_root()), + store.is_known_subset(a, a.root()), Ok(true), "should indicate a known attestation is known" ); assert_eq!( - store.observe_item(a, Some(a.tree_hash_root())), - Ok(ObserveOutcome::AlreadyKnown), + store.observe_item(a, Some(a.root())), + Ok(ObserveOutcome::Subset), "should acknowledge an existing attestation" ); } diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index 59c67bd1b..605a13432 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -841,7 +841,7 @@ mod tests { let mut store = $type::default(); let max_cap = store.max_capacity(); - let to_skip = vec![1_u64, 3, 4, 5]; + let to_skip = [1_u64, 3, 4, 5]; let periods = (0..max_cap * 3) .into_iter() .filter(|i| !to_skip.contains(i)) @@ -1012,7 +1012,7 @@ mod tests { let mut store = $type::default(); let max_cap = store.max_capacity(); - let to_skip = vec![1_u64, 3, 4, 5]; + let to_skip = [1_u64, 3, 4, 5]; let periods = (0..max_cap * 3) .into_iter() .filter(|i| !to_skip.contains(i)) @@ -1121,7 +1121,7 @@ mod tests { let mut store = $type::default(); let max_cap = store.max_capacity(); - let to_skip = vec![1_u64, 3, 4, 5]; + let to_skip = [1_u64, 3, 4, 5]; let periods = (0..max_cap * 3) .into_iter() .filter(|i| !to_skip.contains(i)) diff --git a/beacon_node/beacon_chain/src/observed_block_producers.rs b/beacon_node/beacon_chain/src/observed_block_producers.rs index b5995121b..f76fc5379 100644 --- a/beacon_node/beacon_chain/src/observed_block_producers.rs +++ b/beacon_node/beacon_chain/src/observed_block_producers.rs @@ -1,9 +1,10 @@ //! Provides the `ObservedBlockProducers` struct which allows for rejecting gossip blocks from //! validators that have already produced a block. +use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{BeaconBlockRef, Epoch, EthSpec, Slot, Unsigned}; +use types::{BeaconBlockRef, Epoch, EthSpec, Hash256, Slot, Unsigned}; #[derive(Debug, PartialEq)] pub enum Error { @@ -14,6 +15,12 @@ pub enum Error { ValidatorIndexTooHigh(u64), } +#[derive(Eq, Hash, PartialEq, Debug, Default)] +struct ProposalKey { + slot: Slot, + proposer: u64, +} + /// Maintains a cache of observed `(block.slot, block.proposer)`. /// /// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you @@ -27,7 +34,7 @@ pub enum Error { /// known_distinct_shufflings` which is much smaller. pub struct ObservedBlockProducers { finalized_slot: Slot, - items: HashMap>, + items: HashMap>, _phantom: PhantomData, } @@ -42,6 +49,24 @@ impl Default for ObservedBlockProducers { } } +pub enum SeenBlock { + Duplicate, + Slashable, + UniqueNonSlashable, +} + +impl SeenBlock { + pub fn proposer_previously_observed(self) -> bool { + match self { + Self::Duplicate | Self::Slashable => true, + Self::UniqueNonSlashable => false, + } + } + pub fn is_slashable(&self) -> bool { + matches!(self, Self::Slashable) + } +} + impl ObservedBlockProducers { /// Observe that the `block` was produced by `block.proposer_index` at `block.slot`. This will /// update `self` so future calls to it indicate that this block is known. @@ -52,16 +77,44 @@ impl ObservedBlockProducers { /// /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. - pub fn observe_proposer(&mut self, block: BeaconBlockRef<'_, E>) -> Result { + pub fn observe_proposal( + &mut self, + block_root: Hash256, + block: BeaconBlockRef<'_, E>, + ) -> Result { self.sanitize_block(block)?; - let did_not_exist = self - .items - .entry(block.slot()) - .or_insert_with(|| HashSet::with_capacity(E::SlotsPerEpoch::to_usize())) - .insert(block.proposer_index()); + let key = ProposalKey { + slot: block.slot(), + proposer: block.proposer_index(), + }; - Ok(!did_not_exist) + let entry = self.items.entry(key); + + let slashable_proposal = match entry { + Entry::Occupied(mut occupied_entry) => { + let block_roots = occupied_entry.get_mut(); + let newly_inserted = block_roots.insert(block_root); + + let is_equivocation = block_roots.len() > 1; + + if is_equivocation { + SeenBlock::Slashable + } else if !newly_inserted { + SeenBlock::Duplicate + } else { + SeenBlock::UniqueNonSlashable + } + } + Entry::Vacant(vacant_entry) => { + let block_roots = HashSet::from([block_root]); + vacant_entry.insert(block_roots); + + SeenBlock::UniqueNonSlashable + } + }; + + Ok(slashable_proposal) } /// Returns `Ok(true)` if the `block` has been observed before, `Ok(false)` if not. Does not @@ -72,15 +125,33 @@ impl ObservedBlockProducers { /// /// - `block.proposer_index` is greater than `VALIDATOR_REGISTRY_LIMIT`. /// - `block.slot` is equal to or less than the latest pruned `finalized_slot`. - pub fn proposer_has_been_observed(&self, block: BeaconBlockRef<'_, E>) -> Result { + pub fn proposer_has_been_observed( + &self, + block: BeaconBlockRef<'_, E>, + block_root: Hash256, + ) -> Result { self.sanitize_block(block)?; - let exists = self - .items - .get(&block.slot()) - .map_or(false, |set| set.contains(&block.proposer_index())); + let key = ProposalKey { + slot: block.slot(), + proposer: block.proposer_index(), + }; - Ok(exists) + if let Some(block_roots) = self.items.get(&key) { + let block_already_known = block_roots.contains(&block_root); + let no_prev_known_blocks = + block_roots.difference(&HashSet::from([block_root])).count() == 0; + + if !no_prev_known_blocks { + Ok(SeenBlock::Slashable) + } else if block_already_known { + Ok(SeenBlock::Duplicate) + } else { + Ok(SeenBlock::UniqueNonSlashable) + } + } else { + Ok(SeenBlock::UniqueNonSlashable) + } } /// Returns `Ok(())` if the given `block` is sane. @@ -112,15 +183,15 @@ impl ObservedBlockProducers { } self.finalized_slot = finalized_slot; - self.items.retain(|slot, _set| *slot > finalized_slot); + self.items.retain(|key, _| key.slot > finalized_slot); } /// Returns `true` if the given `validator_index` has been stored in `self` at `epoch`. /// /// This is useful for doppelganger detection. pub fn index_seen_at_epoch(&self, validator_index: u64, epoch: Epoch) -> bool { - self.items.iter().any(|(slot, producers)| { - slot.epoch(E::slots_per_epoch()) == epoch && producers.contains(&validator_index) + self.items.iter().any(|(key, _)| { + key.slot.epoch(E::slots_per_epoch()) == epoch && key.proposer == validator_index }) } } @@ -148,9 +219,12 @@ mod tests { // Slot 0, proposer 0 let block_a = get_block(0, 0); + let block_root = block_a.canonical_root(); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer, indicates proposer unobserved" ); @@ -164,7 +238,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -182,7 +259,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -207,9 +287,12 @@ mod tests { // First slot of finalized epoch, proposer 0 let block_b = get_block(E::slots_per_epoch(), 0); + let block_root_b = block_b.canonical_root(); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Err(Error::FinalizedBlock { slot: E::slots_per_epoch().into(), finalized_slot: E::slots_per_epoch().into(), @@ -229,7 +312,9 @@ mod tests { let block_b = get_block(three_epochs, 0); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can insert non-finalized block" ); @@ -238,7 +323,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(three_epochs)) + .get(&ProposalKey { + slot: Slot::new(three_epochs), + proposer: 0 + }) .expect("the three epochs slot should be present") .len(), 1, @@ -262,7 +350,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(three_epochs)) + .get(&ProposalKey { + slot: Slot::new(three_epochs), + proposer: 0 + }) .expect("the three epochs slot should be present") .len(), 1, @@ -276,24 +367,33 @@ mod tests { // Slot 0, proposer 0 let block_a = get_block(0, 0); + let block_root_a = block_a.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_a.to_ref()), + cache + .proposer_has_been_observed(block_a.to_ref(), block_a.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation in empty cache" ); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root_a, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_a.to_ref()), + cache + .proposer_has_been_observed(block_a.to_ref(), block_a.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed block is indicated as true" ); assert_eq!( - cache.observe_proposer(block_a.to_ref()), + cache + .observe_proposal(block_root_a, block_a.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing again indicates true" ); @@ -303,7 +403,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -312,24 +415,33 @@ mod tests { // Slot 1, proposer 0 let block_b = get_block(1, 0); + let block_root_b = block_b.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_b.to_ref()), + cache + .proposer_has_been_observed(block_b.to_ref(), block_b.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation for new slot" ); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe proposer for new slot, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_b.to_ref()), + cache + .proposer_has_been_observed(block_b.to_ref(), block_b.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed block in slot 1 is indicated as true" ); assert_eq!( - cache.observe_proposer(block_b.to_ref()), + cache + .observe_proposal(block_root_b, block_b.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing slot 1 again indicates true" ); @@ -339,7 +451,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(0)) + .get(&ProposalKey { + slot: Slot::new(0), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -348,7 +463,10 @@ mod tests { assert_eq!( cache .items - .get(&Slot::new(1)) + .get(&ProposalKey { + slot: Slot::new(1), + proposer: 0 + }) .expect("slot zero should be present") .len(), 1, @@ -357,45 +475,54 @@ mod tests { // Slot 0, proposer 1 let block_c = get_block(0, 1); + let block_root_c = block_c.canonical_root(); assert_eq!( - cache.proposer_has_been_observed(block_c.to_ref()), + cache + .proposer_has_been_observed(block_c.to_ref(), block_c.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(false), "no observation for new proposer" ); assert_eq!( - cache.observe_proposer(block_c.to_ref()), + cache + .observe_proposal(block_root_c, block_c.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(false), "can observe new proposer, indicates proposer unobserved" ); assert_eq!( - cache.proposer_has_been_observed(block_c.to_ref()), + cache + .proposer_has_been_observed(block_c.to_ref(), block_c.canonical_root()) + .map(|x| x.proposer_previously_observed()), Ok(true), "observed new proposer block is indicated as true" ); assert_eq!( - cache.observe_proposer(block_c.to_ref()), + cache + .observe_proposal(block_root_c, block_c.to_ref()) + .map(SeenBlock::proposer_previously_observed), Ok(true), "observing new proposer again indicates true" ); assert_eq!(cache.finalized_slot, 0, "finalized slot is zero"); - assert_eq!(cache.items.len(), 2, "two slots should be present"); + assert_eq!(cache.items.len(), 3, "three slots should be present"); assert_eq!( cache .items - .get(&Slot::new(0)) - .expect("slot zero should be present") - .len(), + .iter() + .filter(|(k, _)| k.slot == cache.finalized_slot) + .count(), 2, "two proposers should be present in slot 0" ); assert_eq!( cache .items - .get(&Slot::new(1)) - .expect("slot zero should be present") - .len(), + .iter() + .filter(|(k, _)| k.slot == Slot::new(1)) + .count(), 1, "only one proposer should be present in slot 1" ); diff --git a/beacon_node/beacon_chain/src/sync_committee_verification.rs b/beacon_node/beacon_chain/src/sync_committee_verification.rs index 14cdc2400..5c6710bfd 100644 --- a/beacon_node/beacon_chain/src/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/src/sync_committee_verification.rs @@ -28,15 +28,14 @@ use crate::observed_attesters::SlotSubcommitteeIndex; use crate::{ - beacon_chain::{MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT}, - metrics, - observed_aggregates::ObserveOutcome, - BeaconChain, BeaconChainError, BeaconChainTypes, + beacon_chain::VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, metrics, + observed_aggregates::ObserveOutcome, BeaconChain, BeaconChainError, BeaconChainTypes, }; use bls::{verify_signature_sets, PublicKeyBytes}; use derivative::Derivative; use safe_arith::ArithError; use slot_clock::SlotClock; +use ssz_derive::{Decode, Encode}; use state_processing::per_block_processing::errors::SyncCommitteeMessageValidationError; use state_processing::signature_sets::{ signed_sync_aggregate_selection_proof_signature_set, signed_sync_aggregate_signature_set, @@ -47,9 +46,11 @@ use std::borrow::Cow; use std::collections::HashMap; use strum::AsRefStr; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; 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_contribution::Error as ContributionError, AggregateSignature, BeaconStateError, EthSpec, Hash256, SignedContributionAndProof, Slot, SyncCommitteeContribution, @@ -110,14 +111,14 @@ pub enum Error { /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), - /// The sync contribution has been seen before; either in a block, on the gossip network or from a - /// local validator. + /// The sync contribution or a superset of this sync contribution's aggregation bits for the same data + /// has been seen before; either in a block on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this sync contribution is valid, however we have already observed it and do not /// need to observe it again. - SyncContributionAlreadyKnown(Hash256), + SyncContributionSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// @@ -268,6 +269,14 @@ pub struct VerifiedSyncContribution { participant_pubkeys: Vec, } +/// The sync contribution data. +#[derive(Encode, Decode, TreeHash)] +pub struct SyncCommitteeData { + pub slot: Slot, + pub root: Hash256, + pub subcommittee_index: u64, +} + /// Wraps a `SyncCommitteeMessage` that has been verified for propagation on the gossip network. #[derive(Clone)] pub struct VerifiedSyncCommitteeMessage { @@ -287,7 +296,7 @@ impl VerifiedSyncContribution { let subcommittee_index = contribution.subcommittee_index as usize; // Ensure sync committee contribution is within the MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance. - verify_propagation_slot_range(&chain.slot_clock, contribution)?; + verify_propagation_slot_range(&chain.slot_clock, contribution, &chain.spec)?; // Validate subcommittee index. if contribution.subcommittee_index >= SYNC_COMMITTEE_SUBNET_COUNT { @@ -314,15 +323,22 @@ impl VerifiedSyncContribution { return Err(Error::AggregatorNotInCommittee { aggregator_index }); }; - // Ensure the valid sync contribution has not already been seen locally. - let contribution_root = contribution.tree_hash_root(); + // Ensure the valid sync contribution or its superset has not already been seen locally. + let contribution_data_root = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + } + .tree_hash_root(); + if chain .observed_sync_contributions .write() - .is_known(contribution, contribution_root) + .is_known_subset(contribution, contribution_data_root) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Ensure there has been no other observed aggregate for the given `aggregator_index`. @@ -376,13 +392,14 @@ impl VerifiedSyncContribution { // // It's important to double check that the contribution is not already known, otherwise two // contribution processed at the same time could be published. - if let ObserveOutcome::AlreadyKnown = chain + if let ObserveOutcome::Subset = chain .observed_sync_contributions .write() - .observe_item(contribution, Some(contribution_root)) + .observe_item(contribution, Some(contribution_data_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { - return Err(Error::SyncContributionAlreadyKnown(contribution_root)); + metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_SUBSETS); + return Err(Error::SyncContributionSupersetKnown(contribution_data_root)); } // Observe the aggregator so we don't process another aggregate from them. @@ -442,7 +459,7 @@ impl VerifiedSyncCommitteeMessage { // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future sync committee messages for later processing. - verify_propagation_slot_range(&chain.slot_clock, &sync_message)?; + verify_propagation_slot_range(&chain.slot_clock, &sync_message, &chain.spec)?; // Ensure the `subnet_id` is valid for the given validator. let pubkey = chain @@ -558,11 +575,11 @@ impl VerifiedSyncCommitteeMessage { pub fn verify_propagation_slot_range( slot_clock: &S, sync_contribution: &U, + spec: &ChainSpec, ) -> Result<(), Error> { let message_slot = sync_contribution.get_slot(); - let latest_permissible_slot = slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; if message_slot > latest_permissible_slot { return Err(Error::FutureSlot { @@ -572,7 +589,7 @@ pub fn verify_propagation_slot_range( } let earliest_permissible_slot = slot_clock - .now_with_past_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; if message_slot < earliest_permissible_slot { diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c5615b618..5e54b1194 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -17,12 +17,11 @@ use bls::get_withdrawal_credentials; use execution_layer::{ auth::JwtKey, test_utils::{ - ExecutionBlockGenerator, MockExecutionLayer, TestingBuilder, DEFAULT_JWT_SECRET, - DEFAULT_TERMINAL_BLOCK, + ExecutionBlockGenerator, MockBuilder, MockBuilderServer, MockExecutionLayer, + DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, }, ExecutionLayer, }; -use fork_choice::CountUnrealized; use futures::channel::mpsc::Receiver; pub use genesis::{interop_genesis_state_with_eth1, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; @@ -168,7 +167,6 @@ pub struct Builder { store_mutator: Option>, execution_layer: Option>, mock_execution_layer: Option>, - mock_builder: Option>, testing_slot_clock: Option, runtime: TestRuntime, log: Logger, @@ -302,7 +300,6 @@ where store_mutator: None, execution_layer: None, mock_execution_layer: None, - mock_builder: None, testing_slot_clock: None, runtime, log, @@ -434,7 +431,11 @@ where self } - pub fn mock_execution_layer(mut self) -> Self { + pub fn mock_execution_layer(self) -> Self { + self.mock_execution_layer_with_config(None) + } + + pub fn mock_execution_layer_with_config(mut self, builder_threshold: Option) -> Self { let spec = self.spec.clone().expect("cannot build without spec"); let shanghai_time = spec.capella_fork_epoch.map(|epoch| { HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() @@ -443,55 +444,15 @@ where self.runtime.task_executor.clone(), DEFAULT_TERMINAL_BLOCK, shanghai_time, - None, + builder_threshold, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec, - None, ); self.execution_layer = Some(mock.el.clone()); self.mock_execution_layer = Some(mock); self } - pub fn mock_execution_layer_with_builder( - mut self, - beacon_url: SensitiveUrl, - builder_threshold: Option, - ) -> Self { - // Get a random unused port - let port = unused_port::unused_tcp4_port().unwrap(); - let builder_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(); - - let spec = self.spec.clone().expect("cannot build without spec"); - let shanghai_time = spec.capella_fork_epoch.map(|epoch| { - HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() - }); - let mock_el = MockExecutionLayer::new( - self.runtime.task_executor.clone(), - DEFAULT_TERMINAL_BLOCK, - shanghai_time, - builder_threshold, - Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), - spec.clone(), - Some(builder_url.clone()), - ) - .move_to_terminal_block(); - - let mock_el_url = SensitiveUrl::parse(mock_el.server.url().as_str()).unwrap(); - - self.mock_builder = Some(TestingBuilder::new( - mock_el_url, - builder_url, - beacon_url, - spec, - self.runtime.task_executor.clone(), - )); - self.execution_layer = Some(mock_el.el.clone()); - self.mock_execution_layer = Some(mock_el); - - self - } - /// Instruct the mock execution engine to always return a "valid" response to any payload it is /// asked to execute. pub fn mock_execution_layer_all_payloads_valid(self) -> Self { @@ -517,18 +478,23 @@ where let validator_keypairs = self .validator_keypairs .expect("cannot build without validator keypairs"); + let chain_config = self.chain_config.unwrap_or_default(); let mut builder = BeaconChainBuilder::new(self.eth_spec_instance) .logger(log.clone()) .custom_spec(spec) .store(self.store.expect("cannot build without store")) - .store_migrator_config(MigratorConfig::default().blocking()) + .store_migrator_config( + MigratorConfig::default() + .blocking() + .epochs_per_migration(chain_config.epochs_per_migration), + ) .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(self.chain_config.unwrap_or_default()) + .chain_config(chain_config) .event_handler(Some(ServerSentEventHandler::new_with_capacity( log.clone(), 5, @@ -568,7 +534,7 @@ where shutdown_receiver: Arc::new(Mutex::new(shutdown_receiver)), runtime: self.runtime, mock_execution_layer: self.mock_execution_layer, - mock_builder: self.mock_builder.map(Arc::new), + mock_builder: None, rng: make_rng(), } } @@ -593,7 +559,7 @@ pub struct BeaconChainHarness { pub runtime: TestRuntime, pub mock_execution_layer: Option>, - pub mock_builder: Option>>, + pub mock_builder: Option>>, pub rng: Mutex, } @@ -629,6 +595,49 @@ where .execution_block_generator() } + pub fn set_mock_builder(&mut self, beacon_url: SensitiveUrl) -> MockBuilderServer { + let mock_el = self + .mock_execution_layer + .as_ref() + .expect("harness was not built with mock execution layer"); + + let mock_el_url = SensitiveUrl::parse(mock_el.server.url().as_str()).unwrap(); + + // Create the builder, listening on a free port. + let (mock_builder, mock_builder_server) = MockBuilder::new_for_testing( + mock_el_url, + beacon_url, + self.spec.clone(), + self.runtime.task_executor.clone(), + ); + + // Set the builder URL in the execution layer now that its port is known. + let builder_listen_addr = mock_builder_server.local_addr(); + let port = builder_listen_addr.port(); + mock_el + .el + .set_builder_url( + SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(), + None, + ) + .unwrap(); + + self.mock_builder = Some(Arc::new(mock_builder)); + + // Sanity check. + let el_builder = self + .chain + .execution_layer + .as_ref() + .unwrap() + .builder() + .unwrap(); + let mock_el_builder = mock_el.el.builder().unwrap(); + assert!(Arc::ptr_eq(&el_builder, &mock_el_builder)); + + mock_builder_server + } + pub fn get_all_validators(&self) -> Vec { (0..self.validator_keypairs.len()).collect() } @@ -734,6 +743,15 @@ where state.get_block_root(slot).unwrap() == state.get_block_root(slot - 1).unwrap() } + pub async fn make_blinded_block( + &self, + state: BeaconState, + slot: Slot, + ) -> (SignedBlindedBeaconBlock, BeaconState) { + let (unblinded, new_state) = self.make_block(state, slot).await; + (unblinded.into(), new_state) + } + /// Returns a newly created block, signed by the proposer for the given slot. pub async fn make_block( &self, @@ -746,9 +764,7 @@ where complete_state_advance(&mut state, None, slot, &self.spec) .expect("should be able to advance state to slot"); - state - .build_all_caches(&self.spec) - .expect("should build caches"); + state.build_caches(&self.spec).expect("should build caches"); let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); @@ -795,9 +811,7 @@ where complete_state_advance(&mut state, None, slot, &self.spec) .expect("should be able to advance state to slot"); - state - .build_all_caches(&self.spec) - .expect("should build caches"); + state.build_caches(&self.spec).expect("should build caches"); let proposer_index = state.get_beacon_proposer_index(slot, &self.spec).unwrap(); @@ -1515,6 +1529,36 @@ where .sign(sk, &fork, genesis_validators_root, &self.chain.spec) } + pub fn add_proposer_slashing(&self, validator_index: u64) -> Result<(), String> { + let propposer_slashing = self.make_proposer_slashing(validator_index); + if let ObservationOutcome::New(verified_proposer_slashing) = self + .chain + .verify_proposer_slashing_for_gossip(propposer_slashing) + .expect("should verify proposer slashing for gossip") + { + self.chain + .import_proposer_slashing(verified_proposer_slashing); + Ok(()) + } else { + Err("should observe new proposer slashing".to_string()) + } + } + + pub fn add_attester_slashing(&self, validator_indices: Vec) -> Result<(), String> { + let attester_slashing = self.make_attester_slashing(validator_indices); + if let ObservationOutcome::New(verified_attester_slashing) = self + .chain + .verify_attester_slashing_for_gossip(attester_slashing) + .expect("should verify attester slashing for gossip") + { + self.chain + .import_attester_slashing(verified_attester_slashing); + Ok(()) + } else { + Err("should observe new attester slashing".to_string()) + } + } + pub fn add_bls_to_execution_change( &self, validator_index: u64, @@ -1696,8 +1740,8 @@ where .process_block( block_root, Arc::new(block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await? .into(); @@ -1714,8 +1758,8 @@ where .process_block( block.canonical_root(), Arc::new(block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await? .into(); diff --git a/beacon_node/beacon_chain/src/timeout_rw_lock.rs b/beacon_node/beacon_chain/src/timeout_rw_lock.rs index 28e3f4c29..b2eea7626 100644 --- a/beacon_node/beacon_chain/src/timeout_rw_lock.rs +++ b/beacon_node/beacon_chain/src/timeout_rw_lock.rs @@ -5,7 +5,7 @@ use std::time::Duration; /// A simple wrapper around `parking_lot::RwLock` that only permits read/write access with a /// time-out (i.e., no indefinitely-blocking operations). /// -/// Timeouts can be optionally be disabled at runtime for all instances of this type by calling +/// Timeouts can be optionally disabled at runtime for all instances of this type by calling /// `TimeoutRwLock::disable_timeouts()`. pub struct TimeoutRwLock(RwLock); diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 1040521e5..7878fd14a 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -9,7 +9,7 @@ use beacon_chain::{ test_utils::{ test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }, - BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped, + BeaconChain, BeaconChainError, BeaconChainTypes, ChainConfig, WhenSlotSkipped, }; use genesis::{interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; @@ -47,6 +47,10 @@ fn get_harness(validator_count: usize) -> BeaconChainHarness Vec> { fn get_harness(validator_count: usize) -> BeaconChainHarness> { let harness = BeaconChainHarness::builder(MainnetEthSpec) .default_spec() + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() @@ -148,18 +154,14 @@ async fn chain_segment_full_segment() { // Sneak in a little check to ensure we can process empty chain segments. harness .chain - .process_chain_segment(vec![], CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(vec![], NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import empty chain segment"); harness .chain - .process_chain_segment( - blocks.clone(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(blocks.clone(), NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import chain segment"); @@ -188,11 +190,7 @@ async fn chain_segment_varying_chunk_size() { for chunk in blocks.chunks(*chunk_size) { harness .chain - .process_chain_segment( - chunk.to_vec(), - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(chunk.to_vec(), NotifyExecutionLayer::Yes) .await .into_block_error() .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); @@ -228,7 +226,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -248,7 +246,7 @@ async fn chain_segment_non_linear_parent_roots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearParentRoots) @@ -279,7 +277,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -300,7 +298,7 @@ async fn chain_segment_non_linear_slots() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::NonLinearSlots) @@ -326,7 +324,7 @@ async fn assert_invalid_signature( matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -348,11 +346,7 @@ async fn assert_invalid_signature( // imported prior to this test. let _ = harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await; harness.chain.recompute_head_at_current_slot().await; @@ -361,8 +355,8 @@ async fn assert_invalid_signature( .process_block( snapshots[block_index].beacon_block.canonical_root(), snapshots[block_index].beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await; assert!( @@ -414,11 +408,7 @@ async fn invalid_signature_gossip_block() { .collect(); harness .chain - .process_chain_segment( - ancestor_blocks, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(ancestor_blocks, NotifyExecutionLayer::Yes) .await .into_block_error() .expect("should import all blocks prior to the one being tested"); @@ -430,8 +420,8 @@ async fn invalid_signature_gossip_block() { .process_block( signed_block.canonical_root(), Arc::new(signed_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await, Err(BlockError::InvalidSignature) @@ -465,7 +455,7 @@ async fn invalid_signature_block_proposal() { matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -663,7 +653,7 @@ async fn invalid_signature_deposit() { !matches!( harness .chain - .process_chain_segment(blocks, CountUnrealized::True, NotifyExecutionLayer::Yes) + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), Err(BlockError::InvalidSignature) @@ -743,8 +733,8 @@ async fn block_gossip_verification() { .process_block( gossip_verified.block_root, gossip_verified, - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .expect("should import valid gossip verified block"); @@ -941,11 +931,7 @@ async fn block_gossip_verification() { assert!( matches!( unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(block.clone())).await), - BlockError::RepeatProposal { - proposer, - slot, - } - if proposer == other_proposer && slot == block.message().slot() + BlockError::BlockIsAlreadyKnown, ), "should register any valid signature against the proposer, even if the block failed later verification" ); @@ -974,11 +960,7 @@ async fn block_gossip_verification() { .await .err() .expect("should error when processing known block"), - BlockError::RepeatProposal { - proposer, - slot, - } - if proposer == block.message().proposer_index() && slot == block.message().slot() + BlockError::BlockIsAlreadyKnown ), "the second proposal by this validator should be rejected" ); @@ -1015,8 +997,8 @@ async fn verify_block_for_gossip_slashing_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1055,8 +1037,8 @@ async fn verify_block_for_gossip_doppelganger_detection() { .process_block( verified_block.block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1203,8 +1185,8 @@ async fn add_base_block_to_altair_chain() { .process_block( base_block.canonical_root(), Arc::new(base_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .err() @@ -1219,11 +1201,7 @@ async fn add_base_block_to_altair_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(base_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_chain_segment(vec![Arc::new(base_block)], NotifyExecutionLayer::Yes,) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1342,8 +1320,8 @@ async fn add_altair_block_to_base_chain() { .process_block( altair_block.canonical_root(), Arc::new(altair_block.clone()), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .err() @@ -1358,11 +1336,7 @@ async fn add_altair_block_to_base_chain() { assert!(matches!( harness .chain - .process_chain_segment( - vec![Arc::new(altair_block)], - CountUnrealized::True, - NotifyExecutionLayer::Yes - ) + .process_chain_segment(vec![Arc::new(altair_block)], NotifyExecutionLayer::Yes) .await, ChainSegmentResult::Failed { imported_blocks: 0, @@ -1373,3 +1347,100 @@ async fn add_altair_block_to_base_chain() { } )); } + +#[tokio::test] +async fn import_duplicate_block_unrealized_justification() { + let spec = MainnetEthSpec::default_spec(); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let chain = &harness.chain; + + // Move out of the genesis slot. + harness.advance_slot(); + + // Build the chain out to the first justification opportunity 2/3rds of the way through epoch 2. + let num_slots = E::slots_per_epoch() as usize * 8 / 3; + harness + .extend_chain( + num_slots, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Move into the next empty slot. + harness.advance_slot(); + + // The store's justified checkpoint must still be at epoch 0, while unrealized justification + // must be at epoch 1. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); + drop(fc); + + // Produce a block to justify epoch 2. + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block, _) = harness.make_block(state.clone(), slot).await; + let block = Arc::new(block); + let block_root = block.canonical_root(); + + // Create two verified variants of the block, representing the same block being processed in + // parallel. + let notify_execution_layer = NotifyExecutionLayer::Yes; + let verified_block1 = block + .clone() + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + let verified_block2 = block + .into_execution_pending_block(block_root, &chain, notify_execution_layer) + .unwrap(); + + // Import the first block, simulating a block processed via a finalized chain segment. + chain + .clone() + .import_execution_pending_block(verified_block1) + .await + .unwrap(); + + // Unrealized justification should NOT have updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + let unrealized_justification = fc.unrealized_justified_checkpoint(); + assert_eq!(unrealized_justification.epoch, 2); + + // The fork choice node for the block should have unrealized justification. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); + drop(fc); + + // Import the second verified block, simulating a block processed via RPC. + chain + .clone() + .import_execution_pending_block(verified_block2) + .await + .unwrap(); + + // Unrealized justification should still be updated. + let fc = chain.canonical_head.fork_choice_read_lock(); + assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!( + fc.unrealized_justified_checkpoint(), + unrealized_justification + ); + + // The fork choice node for the block should still have the unrealized justified checkpoint. + let fc_block = fc.get_block(&block_root).unwrap(); + assert_eq!( + fc_block.unrealized_justified_checkpoint, + Some(unrealized_justification) + ); +} diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index e910e8134..f0b799ec9 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -133,13 +133,8 @@ async fn base_altair_merge_capella() { for _ in (merge_fork_slot.as_u64() + 3)..capella_fork_slot.as_u64() { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload = block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(); + let full_payload: FullPayload = + block.message().body().execution_payload().unwrap().into(); // pre-capella shouldn't have withdrawals assert!(full_payload.withdrawals_root().is_err()); execution_payloads.push(full_payload); @@ -151,13 +146,8 @@ async fn base_altair_merge_capella() { for _ in 0..16 { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - let full_payload: FullPayload = block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(); + let full_payload: FullPayload = + block.message().body().execution_payload().unwrap().into(); // post-capella should have withdrawals assert!(full_payload.withdrawals_root().is_ok()); execution_payloads.push(full_payload); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index f88c2ee6f..cd4351297 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -7,7 +7,7 @@ use beacon_chain::otb_verification_service::{ use beacon_chain::{ canonical_head::{CachedHead, CanonicalHead}, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - BeaconChainError, BlockError, ExecutionPayloadError, NotifyExecutionLayer, + BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, NotifyExecutionLayer, OverrideForkchoiceUpdate, StateSkipConfig, WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, @@ -17,9 +17,7 @@ use execution_layer::{ test_utils::ExecutionBlockGenerator, ExecutionLayer, ForkchoiceState, PayloadAttributes, }; -use fork_choice::{ - CountUnrealized, Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus, -}; +use fork_choice::{Error as ForkChoiceError, InvalidationOperation, PayloadVerificationStatus}; use logging::test_logger; use proto_array::{Error as ProtoArrayError, ExecutionStatus}; use slot_clock::SlotClock; @@ -61,6 +59,10 @@ impl InvalidPayloadRig { let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec) + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) .logger(test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .mock_execution_layer() @@ -698,8 +700,8 @@ async fn invalidates_all_descendants() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -795,8 +797,8 @@ async fn switches_heads() { .process_block( fork_block.canonical_root(), Arc::new(fork_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1050,7 +1052,9 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for import. assert!(matches!( - rig.harness.chain.process_block(block.canonical_root(), block.clone(), CountUnrealized::True, NotifyExecutionLayer::Yes).await, + rig.harness.chain.process_block(block.canonical_root(), block.clone(), NotifyExecutionLayer::Yes, + || Ok(()), + ).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); @@ -1064,8 +1068,9 @@ async fn invalid_parent() { Duration::from_secs(0), &state, PayloadVerificationStatus::Optimistic, + rig.harness.chain.config.progressive_balances_mode, &rig.harness.chain.spec, - CountUnrealized::True, + rig.harness.logger() ), Err(ForkChoiceError::ProtoArrayStringError(message)) if message.contains(&format!( @@ -1339,8 +1344,8 @@ async fn build_optimistic_chain( .process_block( block.canonical_root(), block, - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -1900,8 +1905,8 @@ async fn recover_from_invalid_head_by_importing_blocks() { .process_block( fork_block.canonical_root(), fork_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index b61bea124..be271804b 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -9,19 +9,22 @@ use beacon_chain::{ test_utils::{AttestationStrategy, BlockStrategy, RelativeSyncCommittee}, types::{Epoch, EthSpec, Keypair, MinimalEthSpec}, }; +use eth2::lighthouse::attestation_rewards::TotalAttestationRewards; +use eth2::lighthouse::StandardAttestationRewards; +use eth2::types::ValidatorId; use lazy_static::lazy_static; +use types::beacon_state::Error as BeaconStateError; +use types::{BeaconState, ChainSpec}; pub const VALIDATOR_COUNT: usize = 64; +type E = MinimalEthSpec; + lazy_static! { static ref KEYPAIRS: Vec = generate_deterministic_keypairs(VALIDATOR_COUNT); } -fn get_harness() -> BeaconChainHarness> { - let mut spec = E::default_spec(); - - spec.altair_fork_epoch = Some(Epoch::new(0)); // We use altair for all tests - +fn get_harness(spec: ChainSpec) -> BeaconChainHarness> { let harness = BeaconChainHarness::builder(E::default()) .spec(spec) .keypairs(KEYPAIRS.to_vec()) @@ -35,8 +38,11 @@ fn get_harness() -> BeaconChainHarness> { #[tokio::test] async fn test_sync_committee_rewards() { - let num_block_produced = MinimalEthSpec::slots_per_epoch(); - let harness = get_harness::(); + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + + let harness = get_harness(spec); + let num_block_produced = E::slots_per_epoch(); let latest_block_root = harness .extend_chain( @@ -119,3 +125,175 @@ async fn test_sync_committee_rewards() { mismatches.join(",") ); } + +#[tokio::test] +async fn test_verify_attestation_rewards_base() { + let harness = get_harness(E::default_spec()); + + // epoch 0 (N), only two thirds of validators vote. + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let two_thirds_validators: Vec = (0..two_thirds).collect(); + harness + .extend_chain( + E::slots_per_epoch() as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(two_thirds_validators), + ) + .await; + + let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + + // extend slots to beginning of epoch N + 2 + harness.extend_slots(E::slots_per_epoch() as usize).await; + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(0), vec![]) + .unwrap(); + + // assert no inactivity penalty for both ideal rewards and individual validators + assert!(ideal_rewards.iter().all(|reward| reward.inactivity == 0)); + assert!(total_rewards.iter().all(|reward| reward.inactivity == 0)); + + // apply attestation rewards to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().clone().into(); + assert_eq!(expected_balances, balances); +} + +#[tokio::test] +async fn test_verify_attestation_rewards_base_inactivity_leak() { + let spec = E::default_spec(); + let harness = get_harness(spec.clone()); + + let half = VALIDATOR_COUNT / 2; + let half_validators: Vec = (0..half).collect(); + // target epoch is the epoch where the chain enters inactivity leak + let target_epoch = &spec.min_epochs_to_inactivity_penalty + 1; + + // advance until beginning of epoch N + 1 and get balances + harness + .extend_chain( + (E::slots_per_epoch() * (target_epoch + 1)) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(half_validators.clone()), + ) + .await; + let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + + // extend slots to beginning of epoch N + 2 + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(half_validators), + ) + .await; + let _slot = harness.get_current_slot(); + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(target_epoch), vec![]) + .unwrap(); + + // assert inactivity penalty for both ideal rewards and individual validators + assert!(ideal_rewards.iter().all(|reward| reward.inactivity < 0)); + assert!(total_rewards.iter().all(|reward| reward.inactivity < 0)); + + // apply attestation rewards to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().clone().into(); + assert_eq!(expected_balances, balances); +} + +#[tokio::test] +async fn test_verify_attestation_rewards_base_subset_only() { + let harness = get_harness(E::default_spec()); + + // epoch 0 (N), only two thirds of validators vote. + let two_thirds = (VALIDATOR_COUNT / 3) * 2; + let two_thirds_validators: Vec = (0..two_thirds).collect(); + harness + .extend_chain( + E::slots_per_epoch() as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(two_thirds_validators), + ) + .await; + + // a small subset of validators to compute attestation rewards for + let validators_subset = [0, VALIDATOR_COUNT / 2, VALIDATOR_COUNT - 1]; + + // capture balances before transitioning to N + 2 + let initial_balances = get_validator_balances(harness.get_current_state(), &validators_subset); + + // extend slots to beginning of epoch N + 2 + harness.extend_slots(E::slots_per_epoch() as usize).await; + + let validators_subset_ids: Vec = validators_subset + .into_iter() + .map(|idx| ValidatorId::Index(idx as u64)) + .collect(); + + // compute reward deltas for the subset of validators in epoch N + let StandardAttestationRewards { + ideal_rewards: _, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(0), validators_subset_ids) + .unwrap(); + + // apply attestation rewards to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + + // verify expected balances against actual balances + let balances = get_validator_balances(harness.get_current_state(), &validators_subset); + assert_eq!(expected_balances, balances); +} + +/// Apply a vec of `TotalAttestationRewards` to initial balances, and return +fn apply_attestation_rewards( + initial_balances: &[u64], + attestation_rewards: Vec, +) -> Vec { + initial_balances + .iter() + .zip(attestation_rewards) + .map(|(&initial_balance, rewards)| { + let expected_balance = initial_balance as i64 + + rewards.head + + rewards.source + + rewards.target + + rewards.inclusion_delay.map(|q| q.value).unwrap_or(0) as i64 + + rewards.inactivity; + expected_balance as u64 + }) + .collect::>() +} + +fn get_validator_balances(state: BeaconState, validators: &[usize]) -> Vec { + validators + .iter() + .flat_map(|&id| { + state + .balances() + .get(id) + .cloned() + .ok_or(BeaconStateError::BalancesOutOfBounds(id)) + }) + .collect() +} diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 2f40443b9..ab54af42c 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -9,15 +9,15 @@ use beacon_chain::test_utils::{ use beacon_chain::validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD; use beacon_chain::{ historical_blocks::HistoricalBlockError, migrate::MigratorConfig, BeaconChain, - BeaconChainError, BeaconChainTypes, BeaconSnapshot, ChainConfig, NotifyExecutionLayer, - ServerSentEventHandler, WhenSlotSkipped, + BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig, + NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use logging::test_logger; use maplit::hashset; use rand::Rng; -use state_processing::BlockReplayer; +use slot_clock::{SlotClock, TestingSlotClock}; +use state_processing::{state_advance::complete_state_advance, BlockReplayer}; use std::collections::HashMap; use std::collections::HashSet; use std::convert::TryInto; @@ -66,6 +66,19 @@ fn get_store_with_spec( fn get_harness( store: Arc, LevelDB>>, validator_count: usize, +) -> TestHarness { + // Most tests expect to retain historic states, so we use this as the default. + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }; + get_harness_generic(store, validator_count, chain_config) +} + +fn get_harness_generic( + store: Arc, LevelDB>>, + validator_count: usize, + chain_config: ChainConfig, ) -> TestHarness { let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() @@ -73,6 +86,7 @@ fn get_harness( .logger(store.logger().clone()) .fresh_disk_store(store) .mock_execution_layer() + .chain_config(chain_config) .build(); harness.advance_slot(); harness @@ -407,7 +421,7 @@ async fn forwards_iter_block_and_state_roots_until() { // The last restore point slot is the point at which the hybrid forwards iterator behaviour // changes. - let last_restore_point_slot = store.get_latest_restore_point_slot(); + let last_restore_point_slot = store.get_latest_restore_point_slot().unwrap(); assert!(last_restore_point_slot > 0); let chain = &harness.chain; @@ -461,13 +475,15 @@ async fn block_replay_with_inaccurate_state_roots() { .await; // Slot must not be 0 mod 32 or else no blocks will be replayed. - let (mut head_state, head_root) = harness.get_current_state_and_root(); + let (mut head_state, head_state_root) = harness.get_current_state_and_root(); + let head_block_root = harness.head_block_root(); assert_ne!(head_state.slot() % 32, 0); - let mut fast_head_state = store + let (_, mut fast_head_state) = store .get_inconsistent_state_for_attestation_verification_only( - &head_root, - Some(head_state.slot()), + &head_block_root, + head_state.slot(), + head_state_root, ) .unwrap() .unwrap(); @@ -566,14 +582,7 @@ async fn block_replayer_hooks() { async fn delete_blocks_and_states() { let db_path = tempdir().unwrap(); let store = get_store(&db_path); - let validators_keypairs = - types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); - let harness = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_disk_store(store.clone()) - .mock_execution_layer() - .build(); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); let unforked_blocks: u64 = 4 * E::slots_per_epoch(); @@ -1016,18 +1025,14 @@ fn check_shuffling_compatible( // Ensure blocks from abandoned forks are pruned from the Hot DB #[tokio::test] async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { - const HONEST_VALIDATOR_COUNT: usize = 32 + 0; - const ADVERSARIAL_VALIDATOR_COUNT: usize = 16 - 0; + const HONEST_VALIDATOR_COUNT: usize = 32; + const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let slots_per_epoch = rig.slots_per_epoch(); let (mut state, state_root) = rig.get_current_state_and_root(); @@ -1126,18 +1131,14 @@ async fn prunes_abandoned_fork_between_two_finalized_checkpoints() { #[tokio::test] async fn pruning_does_not_touch_abandoned_block_shared_with_canonical_chain() { - const HONEST_VALIDATOR_COUNT: usize = 32 + 0; - const ADVERSARIAL_VALIDATOR_COUNT: usize = 16 - 0; + const HONEST_VALIDATOR_COUNT: usize = 32; + const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let slots_per_epoch = rig.slots_per_epoch(); let (state, state_root) = rig.get_current_state_and_root(); @@ -1261,15 +1262,11 @@ async fn pruning_does_not_touch_blocks_prior_to_finalization() { const HONEST_VALIDATOR_COUNT: usize = 32; const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let slots_per_epoch = rig.slots_per_epoch(); let (mut state, state_root) = rig.get_current_state_and_root(); @@ -1353,18 +1350,14 @@ async fn pruning_does_not_touch_blocks_prior_to_finalization() { #[tokio::test] async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { - const HONEST_VALIDATOR_COUNT: usize = 32 + 0; - const ADVERSARIAL_VALIDATOR_COUNT: usize = 16 - 0; + const HONEST_VALIDATOR_COUNT: usize = 32; + const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let (state, state_root) = rig.get_current_state_and_root(); // Fill up 0th epoch with canonical chain blocks @@ -1498,18 +1491,14 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { // This is to check if state outside of normal block processing are pruned correctly. #[tokio::test] async fn prunes_skipped_slots_states() { - const HONEST_VALIDATOR_COUNT: usize = 32 + 0; - const ADVERSARIAL_VALIDATOR_COUNT: usize = 16 - 0; + const HONEST_VALIDATOR_COUNT: usize = 32; + const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let (state, state_root) = rig.get_current_state_and_root(); let canonical_slots_zeroth_epoch: Vec = @@ -1627,18 +1616,14 @@ async fn prunes_skipped_slots_states() { // This is to check if state outside of normal block processing are pruned correctly. #[tokio::test] async fn finalizes_non_epoch_start_slot() { - const HONEST_VALIDATOR_COUNT: usize = 32 + 0; - const ADVERSARIAL_VALIDATOR_COUNT: usize = 16 - 0; + const HONEST_VALIDATOR_COUNT: usize = 32; + const ADVERSARIAL_VALIDATOR_COUNT: usize = 16; const VALIDATOR_COUNT: usize = HONEST_VALIDATOR_COUNT + ADVERSARIAL_VALIDATOR_COUNT; - let validators_keypairs = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); let honest_validators: Vec = (0..HONEST_VALIDATOR_COUNT).collect(); let adversarial_validators: Vec = (HONEST_VALIDATOR_COUNT..VALIDATOR_COUNT).collect(); - let rig = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .keypairs(validators_keypairs) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let rig = get_harness(store.clone(), VALIDATOR_COUNT); let (state, state_root) = rig.get_current_state_and_root(); let canonical_slots_zeroth_epoch: Vec = @@ -2054,39 +2039,82 @@ async fn garbage_collect_temp_states_from_failed_block() { } #[tokio::test] -async fn weak_subjectivity_sync() { +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 +} + +#[tokio::test] +async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { + 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) + .filter(|&slot| { + // Skip 3 slots leading up to the checkpoint slot. + slot <= checkpoint_slot - 3 || slot > checkpoint_slot + }) + .collect(); + weak_subjectivity_sync_test(slots, checkpoint_slot).await +} + +#[tokio::test] +async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { + 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| { + // Skip 3 slots after the checkpoint slot. + slot <= checkpoint_slot || slot > checkpoint_slot + 3 + }) + .collect(); + weak_subjectivity_sync_test(slots, checkpoint_slot).await +} + +async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Build an initial chain on one harness, representing a synced node with full history. - let num_initial_blocks = E::slots_per_epoch() * 11; let num_final_blocks = E::slots_per_epoch() * 2; let temp1 = tempdir().unwrap(); let full_store = get_store(&temp1); let harness = get_harness(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 - .extend_chain( - num_initial_blocks as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, + .add_attested_blocks_at_slots( + genesis_state.clone(), + genesis_state_root, + &slots, + &all_validators, ) .await; - let genesis_state = full_store - .get_state(&harness.chain.genesis_state_root, Some(Slot::new(0))) + let wss_block_root = harness + .chain + .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) .unwrap() .unwrap(); - let wss_checkpoint = harness.finalized_checkpoint(); + let wss_state_root = harness + .chain + .state_root_at_slot(checkpoint_slot) + .unwrap() + .unwrap(); + let wss_block = harness .chain .store - .get_full_block(&wss_checkpoint.root) + .get_full_block(&wss_block_root) .unwrap() .unwrap(); let wss_state = full_store - .get_state(&wss_block.state_root(), None) + .get_state(&wss_state_root, Some(checkpoint_slot)) .unwrap() .unwrap(); - let wss_slot = wss_block.slot(); // Add more blocks that advance finalization further. harness.advance_slot(); @@ -2105,20 +2133,26 @@ async fn weak_subjectivity_sync() { let spec = test_spec::(); let seconds_per_slot = spec.seconds_per_slot; - // Initialise a new beacon chain from the finalized checkpoint + // Initialise a new beacon chain from the finalized checkpoint. + // The slot clock must be set to a time ahead of the checkpoint state. + let slot_clock = TestingSlotClock::new( + Slot::new(0), + Duration::from_secs(harness.chain.genesis_time), + Duration::from_secs(seconds_per_slot), + ); + slot_clock.set_slot(harness.get_current_slot().as_u64()); let beacon_chain = Arc::new( BeaconChainBuilder::new(MinimalEthSpec) .store(store.clone()) .custom_spec(test_spec::()) .task_executor(harness.chain.task_executor.clone()) + .logger(log.clone()) .weak_subjectivity_state(wss_state, wss_block.clone(), genesis_state) .unwrap() - .logger(log.clone()) .store_migrator_config(MigratorConfig::default().blocking()) .dummy_eth1_backend() .expect("should build dummy backend") - .testing_slot_clock(Duration::from_secs(seconds_per_slot)) - .expect("should configure testing slot clock") + .slot_clock(slot_clock) .shutdown_sender(shutdown_tx) .chain_config(ChainConfig::default()) .event_handler(Some(ServerSentEventHandler::new_with_capacity( @@ -2132,9 +2166,9 @@ async fn weak_subjectivity_sync() { // Apply blocks forward to reach head. let chain_dump = harness.chain.chain_dump().unwrap(); - let new_blocks = &chain_dump[wss_slot.as_usize() + 1..]; - - assert_eq!(new_blocks[0].beacon_block.slot(), wss_slot + 1); + let new_blocks = chain_dump + .iter() + .filter(|snapshot| snapshot.beacon_block.slot() > checkpoint_slot); for snapshot in new_blocks { let full_block = harness @@ -2151,8 +2185,8 @@ async fn weak_subjectivity_sync() { .process_block( full_block.canonical_root(), Arc::new(full_block), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await .unwrap(); @@ -2220,13 +2254,17 @@ async fn weak_subjectivity_sync() { assert_eq!(forwards, expected); // All blocks can be loaded. + let mut prev_block_root = Hash256::zero(); for (block_root, slot) in beacon_chain .forwards_iter_block_roots(Slot::new(0)) .unwrap() .map(Result::unwrap) { let block = store.get_blinded_block(&block_root).unwrap().unwrap(); - assert_eq!(block.slot(), slot); + if block_root != prev_block_root { + assert_eq!(block.slot(), slot); + } + prev_block_root = block_root; } // All states from the oldest state slot can be loaded. @@ -2241,14 +2279,141 @@ async fn weak_subjectivity_sync() { assert_eq!(state.canonical_root(), state_root); } - // Anchor slot is still set to the starting slot. - assert_eq!(store.get_anchor_slot(), Some(wss_slot)); + // Anchor slot is still set to the slot of the checkpoint block. + assert_eq!(store.get_anchor_slot(), Some(wss_block.slot())); // Reconstruct states. store.clone().reconstruct_historic_states().unwrap(); assert_eq!(store.get_anchor_slot(), None); } +/// Test that blocks and attestations that refer to states around an unaligned split state are +/// processed correctly. +#[tokio::test] +async fn process_blocks_and_attestations_for_unaligned_checkpoint() { + let temp = tempdir().unwrap(); + let store = get_store(&temp); + let chain_config = ChainConfig { + reconstruct_historic_states: false, + ..ChainConfig::default() + }; + let harness = get_harness_generic(store.clone(), LOW_VALIDATOR_COUNT, chain_config); + + let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); + + let split_slot = Slot::new(E::slots_per_epoch() * 4); + let pre_skips = 1; + let post_skips = 1; + + // Build the chain up to the intended split slot, with 3 skips before the split. + let slots = (1..=split_slot.as_u64() - pre_skips) + .map(Slot::new) + .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; + + // Before the split slot becomes finalized, create two forking blocks that build on the split + // block: + // + // - one that is invalid because it conflicts with finalization (slot <= finalized_slot) + // - one that is valid because its slot is not finalized (slot > finalized_slot) + let (unadvanced_split_state, unadvanced_split_state_root) = + harness.get_current_state_and_root(); + + let (invalid_fork_block, _) = harness + .make_block(unadvanced_split_state.clone(), split_slot) + .await; + let (valid_fork_block, _) = harness + .make_block(unadvanced_split_state.clone(), split_slot + 1) + .await; + + // Advance the chain so that the intended split slot is finalized. + // Do not attest in the epoch boundary slot, to make attestation production later easier (no + // equivocations). + let finalizing_slot = split_slot + 2 * E::slots_per_epoch(); + for _ in 0..pre_skips + post_skips { + harness.advance_slot(); + } + harness.extend_to_slot(finalizing_slot - 1).await; + harness + .add_block_at_slot(finalizing_slot, harness.get_current_state()) + .await + .unwrap(); + + // Check that the split slot is as intended. + let split = store.get_split_info(); + assert_eq!(split.slot, split_slot); + assert_eq!(split.block_root, valid_fork_block.parent_root()); + assert_ne!(split.state_root, unadvanced_split_state_root); + + // Applying the invalid block should fail. + let err = harness + .chain + .process_block( + invalid_fork_block.canonical_root(), + Arc::new(invalid_fork_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap_err(); + assert!(matches!(err, BlockError::WouldRevertFinalizedSlot { .. })); + + // Applying the valid block should succeed, but it should not become head. + harness + .chain + .process_block( + valid_fork_block.canonical_root(), + Arc::new(valid_fork_block.clone()), + NotifyExecutionLayer::Yes, + || Ok(()), + ) + .await + .unwrap(); + harness.chain.recompute_head_at_current_slot().await; + assert_ne!(harness.head_block_root(), valid_fork_block.canonical_root()); + + // Attestations to the split block in the next 2 epochs should be processed successfully. + let attestation_start_slot = harness.get_current_slot(); + let attestation_end_slot = attestation_start_slot + 2 * E::slots_per_epoch(); + let (split_state_root, mut advanced_split_state) = harness + .chain + .store + .get_advanced_hot_state(split.block_root, split.slot, split.state_root) + .unwrap() + .unwrap(); + complete_state_advance( + &mut advanced_split_state, + Some(split_state_root), + attestation_start_slot, + &harness.chain.spec, + ) + .unwrap(); + advanced_split_state + .build_caches(&harness.chain.spec) + .unwrap(); + let advanced_split_state_root = advanced_split_state.update_tree_hash_cache().unwrap(); + for slot in (attestation_start_slot.as_u64()..attestation_end_slot.as_u64()).map(Slot::new) { + let attestations = harness.make_attestations( + &all_validators, + &advanced_split_state, + advanced_split_state_root, + split.block_root.into(), + slot, + ); + harness.advance_slot(); + harness.process_attestations(attestations); + } +} + #[tokio::test] async fn finalizes_after_resuming_from_db() { let validator_count = 16; @@ -2307,6 +2472,7 @@ async fn finalizes_after_resuming_from_db() { .default_spec() .keypairs(KEYPAIRS[0..validator_count].to_vec()) .resumed_disk_store(store) + .testing_slot_clock(original_chain.slot_clock.clone()) .mock_execution_layer() .build(); @@ -2560,6 +2726,9 @@ async fn schema_downgrade_to_min_version() { SchemaVersion(11) }; + // Save the slot clock so that the new harness doesn't revert in time. + let slot_clock = harness.chain.slot_clock.clone(); + // Close the database to ensure everything is written to disk. drop(store); drop(harness); @@ -2590,11 +2759,21 @@ async fn schema_downgrade_to_min_version() { ) .expect("schema upgrade from minimum version should work"); - // Rescreate the harness. + // Recreate the harness. + /* + 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 harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() .keypairs(KEYPAIRS[0..LOW_VALIDATOR_COUNT].to_vec()) .logger(store.logger().clone()) + .testing_slot_clock(slot_clock) .resumed_disk_store(store.clone()) .mock_execution_layer() .build(); diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index 4204a5121..0e4745ff6 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -1,6 +1,6 @@ #![cfg(not(debug_assertions))] -use beacon_chain::sync_committee_verification::Error as SyncCommitteeError; +use beacon_chain::sync_committee_verification::{Error as SyncCommitteeError, SyncCommitteeData}; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee}; use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; @@ -444,11 +444,17 @@ async fn aggregated_gossip_verification() { * subcommittee index contribution.subcommittee_index. */ + let contribution = &valid_aggregate.message.contribution; + let sync_committee_data = SyncCommitteeData { + slot: contribution.slot, + root: contribution.beacon_block_root, + subcommittee_index: contribution.subcommittee_index, + }; assert_invalid!( "aggregate that has already been seen", valid_aggregate.clone(), - SyncCommitteeError::SyncContributionAlreadyKnown(hash) - if hash == valid_aggregate.message.contribution.tree_hash_root() + SyncCommitteeError::SyncContributionSupersetKnown(hash) + if hash == sync_committee_data.tree_hash_root() ); /* diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index b4eabc809..8935c6992 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -6,9 +6,8 @@ use beacon_chain::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, OP_POOL_DB_KEY, }, - BeaconChain, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, + BeaconChain, ChainConfig, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, }; -use fork_choice::CountUnrealized; use lazy_static::lazy_static; use operation_pool::PersistedOperationPool; use state_processing::{ @@ -29,6 +28,10 @@ lazy_static! { fn get_harness(validator_count: usize) -> BeaconChainHarness> { let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() @@ -687,8 +690,8 @@ async fn run_skip_slot_test(skip_slots: u64) { .process_block( harness_a.chain.head_snapshot().beacon_block_root, harness_a.chain.head_snapshot().beacon_block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()) ) .await .unwrap(), diff --git a/beacon_node/beacon_processor/Cargo.toml b/beacon_node/beacon_processor/Cargo.toml new file mode 100644 index 000000000..723b09b58 --- /dev/null +++ b/beacon_node/beacon_processor/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "beacon_processor" +version = "0.1.0" +edition = { workspace = true } + +[dependencies] +slog = { workspace = true } +itertools = { workspace = true } +logging = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } +futures = { workspace = true } +fnv = { workspace = true } +strum = { workspace = true } +task_executor = { workspace = true } +slot_clock = { workspace = true } +lighthouse_network = { workspace = true } +hex = { workspace = true } +derivative = { workspace = true } +types = { workspace = true } +ethereum_ssz = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +parking_lot = { workspace = true } +num_cpus = { workspace = true } +serde = { workspace = true } \ No newline at end of file diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/beacon_processor/src/lib.rs similarity index 57% rename from beacon_node/network/src/beacon_processor/mod.rs rename to beacon_node/beacon_processor/src/lib.rs index 26d2c19b5..4c1da85fa 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -38,56 +38,40 @@ //! checks the queues to see if there are more parcels of work that can be spawned in a new worker //! task. -use crate::sync::manager::BlockProcessType; -use crate::{metrics, service::NetworkMessage, sync::SyncMessage}; -use beacon_chain::parking_lot::Mutex; -use beacon_chain::{BeaconChain, BeaconChainTypes, GossipVerifiedBlock, NotifyExecutionLayer}; -use derivative::Derivative; +use crate::work_reprocessing_queue::{ + spawn_reprocess_scheduler, QueuedAggregate, QueuedBackfillBatch, QueuedGossipBlock, + QueuedLightClientUpdate, QueuedRpcBlock, QueuedUnaggregate, ReadyWork, ReprocessQueueMessage, +}; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; -use lighthouse_network::rpc::LightClientBootstrapRequest; -use lighthouse_network::{ - rpc::{BlocksByRangeRequest, BlocksByRootRequest, StatusMessage}, - Client, MessageId, NetworkGlobals, PeerId, PeerRequestId, -}; +use lighthouse_network::NetworkGlobals; +use lighthouse_network::{MessageId, PeerId}; use logging::TimeLatch; +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; use slog::{crit, debug, error, trace, warn, Logger}; -use std::collections::VecDeque; +use slot_clock::SlotClock; +use std::cmp; +use std::collections::{HashSet, VecDeque}; +use std::fmt; use std::future::Future; -use std::path::PathBuf; use std::pin::Pin; -use std::sync::{Arc, Weak}; +use std::sync::Arc; use std::task::Context; use std::time::Duration; -use std::{cmp, collections::HashSet}; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tokio::sync::mpsc::error::TrySendError; -use types::{ - Attestation, AttesterSlashing, Hash256, LightClientFinalityUpdate, LightClientOptimisticUpdate, - ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId, -}; -use work_reprocessing_queue::{ - spawn_reprocess_scheduler, QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock, - QueuedUnaggregate, ReadyWork, -}; +use types::{Attestation, EthSpec, Hash256, SignedAggregateAndProof, Slot, SubnetId}; +use work_reprocessing_queue::IgnoredRpcBlock; -use worker::{Toolbox, Worker}; - -mod tests; -mod work_reprocessing_queue; -mod worker; - -use crate::beacon_processor::work_reprocessing_queue::{ - QueuedBackfillBatch, QueuedGossipBlock, ReprocessQueueMessage, -}; -pub use worker::{ChainSegmentProcessId, GossipAggregatePackage, GossipAttestationPackage}; +mod metrics; +pub mod work_reprocessing_queue; /// The maximum size of the channel for work events to the `BeaconProcessor`. /// /// Setting this too low will cause consensus messages to be dropped. -pub const MAX_WORK_EVENT_QUEUE_LEN: usize = 16_384; +const DEFAULT_MAX_WORK_EVENT_QUEUE_LEN: usize = 16_384; /// The maximum size of the channel for idle events to the `BeaconProcessor`. /// @@ -96,7 +80,7 @@ pub const MAX_WORK_EVENT_QUEUE_LEN: usize = 16_384; const MAX_IDLE_QUEUE_LEN: usize = 16_384; /// The maximum size of the channel for re-processing work events. -const MAX_SCHEDULED_WORK_QUEUE_LEN: usize = 3 * MAX_WORK_EVENT_QUEUE_LEN / 4; +const DEFAULT_MAX_SCHEDULED_WORK_QUEUE_LEN: usize = 3 * DEFAULT_MAX_WORK_EVENT_QUEUE_LEN / 4; /// The maximum number of queued `Attestation` objects that will be stored before we start dropping /// them. @@ -184,6 +168,14 @@ const MAX_BLS_TO_EXECUTION_CHANGE_QUEUE_LEN: usize = 16_384; /// will be stored before we start dropping them. const MAX_LIGHT_CLIENT_BOOTSTRAP_QUEUE_LEN: usize = 1_024; +/// The maximum number of priority-0 (highest priority) messages that will be queued before +/// they begin to be dropped. +const MAX_API_REQUEST_P0_QUEUE_LEN: usize = 1_024; + +/// The maximum number of priority-1 (second-highest priority) messages that will be queued before +/// they begin to be dropped. +const MAX_API_REQUEST_P1_QUEUE_LEN: usize = 1_024; + /// The name of the manager tokio task. const MANAGER_TASK_NAME: &str = "beacon_processor_manager"; @@ -201,8 +193,8 @@ const WORKER_TASK_NAME: &str = "beacon_processor_worker"; /// Poisoning occurs when an invalid signature is included in a batch of attestations. A single /// invalid signature causes the entire batch to fail. When a batch fails, we fall-back to /// individually verifying each attestation signature. -const MAX_GOSSIP_ATTESTATION_BATCH_SIZE: usize = 64; -const MAX_GOSSIP_AGGREGATE_BATCH_SIZE: usize = 64; +const DEFAULT_MAX_GOSSIP_ATTESTATION_BATCH_SIZE: usize = 64; +const DEFAULT_MAX_GOSSIP_AGGREGATE_BATCH_SIZE: usize = 64; /// Unique IDs used for metrics and testing. pub const WORKER_FREED: &str = "worker_freed"; @@ -221,6 +213,7 @@ pub const GOSSIP_SYNC_CONTRIBUTION: &str = "gossip_sync_contribution"; pub const GOSSIP_LIGHT_CLIENT_FINALITY_UPDATE: &str = "light_client_finality_update"; pub const GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic_update"; pub const RPC_BLOCK: &str = "rpc_block"; +pub const IGNORED_RPC_BLOCK: &str = "ignored_rpc_block"; pub const CHAIN_SEGMENT: &str = "chain_segment"; pub const CHAIN_SEGMENT_BACKFILL: &str = "chain_segment_backfill"; pub const STATUS_PROCESSING: &str = "status_processing"; @@ -231,6 +224,61 @@ pub const UNKNOWN_BLOCK_ATTESTATION: &str = "unknown_block_attestation"; pub const UNKNOWN_BLOCK_AGGREGATE: &str = "unknown_block_aggregate"; pub const UNKNOWN_LIGHT_CLIENT_UPDATE: &str = "unknown_light_client_update"; pub const GOSSIP_BLS_TO_EXECUTION_CHANGE: &str = "gossip_bls_to_execution_change"; +pub const API_REQUEST_P0: &str = "api_request_p0"; +pub const API_REQUEST_P1: &str = "api_request_p1"; + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct BeaconProcessorConfig { + pub max_workers: usize, + pub max_work_event_queue_len: usize, + pub max_scheduled_work_queue_len: usize, + pub max_gossip_attestation_batch_size: usize, + pub max_gossip_aggregate_batch_size: usize, + pub enable_backfill_rate_limiting: bool, +} + +impl Default for BeaconProcessorConfig { + fn default() -> Self { + Self { + max_workers: cmp::max(1, num_cpus::get()), + max_work_event_queue_len: DEFAULT_MAX_WORK_EVENT_QUEUE_LEN, + max_scheduled_work_queue_len: DEFAULT_MAX_SCHEDULED_WORK_QUEUE_LEN, + max_gossip_attestation_batch_size: DEFAULT_MAX_GOSSIP_ATTESTATION_BATCH_SIZE, + max_gossip_aggregate_batch_size: DEFAULT_MAX_GOSSIP_AGGREGATE_BATCH_SIZE, + enable_backfill_rate_limiting: true, + } + } +} + +// The channels necessary to instantiate a `BeaconProcessor`. +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_scheduled_work_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, + } + } +} + +impl Default for BeaconProcessorChannels { + fn default() -> Self { + Self::new(&BeaconProcessorConfig::default()) + } +} /// A simple first-in-first-out queue with a maximum length. struct FifoQueue { @@ -366,524 +414,194 @@ impl DuplicateCache { } /// An event to be processed by the manager task. -#[derive(Derivative)] -#[derivative(Debug(bound = "T: BeaconChainTypes"))] -pub struct WorkEvent { - drop_during_sync: bool, - work: Work, +#[derive(Debug)] +pub struct WorkEvent { + pub drop_during_sync: bool, + pub work: Work, } -impl WorkEvent { - /// Create a new `Work` event for some unaggregated attestation. - pub fn unaggregated_attestation( - message_id: MessageId, - peer_id: PeerId, - attestation: Attestation, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipAttestation { - message_id, - peer_id, - attestation: Box::new(attestation), - subnet_id, - should_import, - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some aggregated attestation. - pub fn aggregated_attestation( - message_id: MessageId, - peer_id: PeerId, - aggregate: SignedAggregateAndProof, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipAggregate { - message_id, - peer_id, - aggregate: Box::new(aggregate), - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some block. - pub fn gossip_beacon_block( - message_id: MessageId, - peer_id: PeerId, - peer_client: Client, - block: Arc>, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::GossipBlock { - message_id, - peer_id, - peer_client, - block, - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some sync committee signature. - pub fn gossip_sync_signature( - message_id: MessageId, - peer_id: PeerId, - sync_signature: SyncCommitteeMessage, - subnet_id: SyncSubnetId, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipSyncSignature { - message_id, - peer_id, - sync_signature: Box::new(sync_signature), - subnet_id, - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some sync committee contribution. - pub fn gossip_sync_contribution( - message_id: MessageId, - peer_id: PeerId, - sync_contribution: SignedContributionAndProof, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipSyncContribution { - message_id, - peer_id, - sync_contribution: Box::new(sync_contribution), - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some exit. - pub fn gossip_voluntary_exit( - message_id: MessageId, - peer_id: PeerId, - voluntary_exit: Box, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::GossipVoluntaryExit { - message_id, - peer_id, - voluntary_exit, - }, - } - } - - /// Create a new `Work` event for some proposer slashing. - pub fn gossip_proposer_slashing( - message_id: MessageId, - peer_id: PeerId, - proposer_slashing: Box, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::GossipProposerSlashing { - message_id, - peer_id, - proposer_slashing, - }, - } - } - - /// Create a new `Work` event for some light client finality update. - pub fn gossip_light_client_finality_update( - message_id: MessageId, - peer_id: PeerId, - light_client_finality_update: Box>, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipLightClientFinalityUpdate { - message_id, - peer_id, - light_client_finality_update, - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some light client optimistic update. - pub fn gossip_light_client_optimistic_update( - message_id: MessageId, - peer_id: PeerId, - light_client_optimistic_update: Box>, - seen_timestamp: Duration, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::GossipLightClientOptimisticUpdate { - message_id, - peer_id, - light_client_optimistic_update, - seen_timestamp, - }, - } - } - - /// Create a new `Work` event for some attester slashing. - pub fn gossip_attester_slashing( - message_id: MessageId, - peer_id: PeerId, - attester_slashing: Box>, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::GossipAttesterSlashing { - message_id, - peer_id, - attester_slashing, - }, - } - } - - /// Create a new `Work` event for some BLS to execution change. - pub fn gossip_bls_to_execution_change( - message_id: MessageId, - peer_id: PeerId, - bls_to_execution_change: Box, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::GossipBlsToExecutionChange { - message_id, - peer_id, - bls_to_execution_change, - }, - } - } - - /// Create a new `Work` event for some block, where the result from computation (if any) is - /// sent to the other side of `result_tx`. - pub fn rpc_beacon_block( - block_root: Hash256, - block: Arc>, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::RpcBlock { - block_root, - block, - seen_timestamp, - process_type, - should_process: true, - }, - } - } - - /// Create a new work event to import `blocks` as a beacon chain segment. - pub fn chain_segment( - process_id: ChainSegmentProcessId, - blocks: Vec>>, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::ChainSegment { process_id, blocks }, - } - } - - /// Create a new work event to process `StatusMessage`s from the RPC network. - pub fn status_message(peer_id: PeerId, message: StatusMessage) -> Self { - Self { - drop_during_sync: false, - work: Work::Status { peer_id, message }, - } - } - - /// Create a new work event to process `BlocksByRangeRequest`s from the RPC network. - pub fn blocks_by_range_request( - peer_id: PeerId, - request_id: PeerRequestId, - request: BlocksByRangeRequest, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::BlocksByRangeRequest { - peer_id, - request_id, - request, - }, - } - } - - /// Create a new work event to process `BlocksByRootRequest`s from the RPC network. - pub fn blocks_by_roots_request( - peer_id: PeerId, - request_id: PeerRequestId, - request: BlocksByRootRequest, - ) -> Self { - Self { - drop_during_sync: false, - work: Work::BlocksByRootsRequest { - peer_id, - request_id, - request, - }, - } - } - - /// Create a new work event to process `LightClientBootstrap`s from the RPC network. - pub fn lightclient_bootstrap_request( - peer_id: PeerId, - request_id: PeerRequestId, - request: LightClientBootstrapRequest, - ) -> Self { - Self { - drop_during_sync: true, - work: Work::LightClientBootstrapRequest { - peer_id, - request_id, - request, - }, - } - } - +impl WorkEvent { /// Get a `str` representation of the type of work this `WorkEvent` contains. pub fn work_type(&self) -> &'static str { self.work.str_id() } } -impl std::convert::From> for WorkEvent { - fn from(ready_work: ReadyWork) -> Self { +impl From for WorkEvent { + fn from(ready_work: ReadyWork) -> Self { match ready_work { ReadyWork::Block(QueuedGossipBlock { - peer_id, - block, - seen_timestamp, + beacon_block_slot, + beacon_block_root, + process_fn, }) => Self { drop_during_sync: false, work: Work::DelayedImportBlock { - peer_id, - block, - seen_timestamp, + beacon_block_slot, + beacon_block_root, + process_fn, }, }, ReadyWork::RpcBlock(QueuedRpcBlock { - block_root, - block, - seen_timestamp, - process_type, - should_process, + beacon_block_root: _, + process_fn, + ignore_fn: _, }) => Self { drop_during_sync: false, - work: Work::RpcBlock { - block_root, - block, - seen_timestamp, - process_type, - should_process, - }, + work: Work::RpcBlock { process_fn }, + }, + ReadyWork::IgnoredRpcBlock(IgnoredRpcBlock { process_fn }) => Self { + drop_during_sync: false, + work: Work::IgnoredRpcBlock { process_fn }, }, ReadyWork::Unaggregate(QueuedUnaggregate { - peer_id, - message_id, - attestation, - subnet_id, - should_import, - seen_timestamp, + beacon_block_root: _, + process_fn, }) => Self { drop_during_sync: true, - work: Work::UnknownBlockAttestation { - message_id, - peer_id, - attestation, - subnet_id, - should_import, - seen_timestamp, - }, + work: Work::UnknownBlockAttestation { process_fn }, }, ReadyWork::Aggregate(QueuedAggregate { - peer_id, - message_id, - attestation, - seen_timestamp, + process_fn, + beacon_block_root: _, }) => Self { drop_during_sync: true, - work: Work::UnknownBlockAggregate { - message_id, - peer_id, - aggregate: attestation, - seen_timestamp, - }, + work: Work::UnknownBlockAggregate { process_fn }, }, ReadyWork::LightClientUpdate(QueuedLightClientUpdate { - peer_id, - message_id, - light_client_optimistic_update, - seen_timestamp, - .. + parent_root, + process_fn, }) => Self { drop_during_sync: true, work: Work::UnknownLightClientOptimisticUpdate { - message_id, - peer_id, - light_client_optimistic_update, - seen_timestamp, + parent_root, + process_fn, }, }, - ReadyWork::BackfillSync(QueuedBackfillBatch { process_id, blocks }) => { - WorkEvent::chain_segment(process_id, blocks) + ReadyWork::BackfillSync(QueuedBackfillBatch(process_fn)) => Self { + drop_during_sync: false, + work: Work::ChainSegmentBackfill(process_fn), + }, + } + } +} + +/// Items required to verify a batch of unaggregated gossip attestations. +#[derive(Debug)] +pub struct GossipAttestationPackage { + pub message_id: MessageId, + pub peer_id: PeerId, + pub attestation: Box>, + pub subnet_id: SubnetId, + pub should_import: bool, + pub seen_timestamp: Duration, +} + +/// Items required to verify a batch of aggregated gossip attestations. +#[derive(Debug)] +pub struct GossipAggregatePackage { + pub message_id: MessageId, + pub peer_id: PeerId, + pub aggregate: Box>, + pub beacon_block_root: Hash256, + pub seen_timestamp: Duration, +} + +#[derive(Clone)] +pub struct BeaconProcessorSend(pub mpsc::Sender>); + +impl BeaconProcessorSend { + pub fn try_send(&self, message: WorkEvent) -> Result<(), TrySendError>> { + let work_type = message.work_type(); + match self.0.try_send(message) { + Ok(res) => Ok(res), + Err(e) => { + metrics::inc_counter_vec( + &metrics::BEACON_PROCESSOR_SEND_ERROR_PER_WORK_TYPE, + &[work_type], + ); + Err(e) } } } } -/// A consensus message (or multiple) from the network that requires processing. -#[derive(Derivative)] -#[derivative(Debug(bound = "T: BeaconChainTypes"))] -pub enum Work { - GossipAttestation { - message_id: MessageId, - peer_id: PeerId, - attestation: Box>, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, - }, - UnknownBlockAttestation { - message_id: MessageId, - peer_id: PeerId, - attestation: Box>, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, - }, - GossipAttestationBatch { - packages: Vec>, - }, - GossipAggregate { - message_id: MessageId, - peer_id: PeerId, - aggregate: Box>, - seen_timestamp: Duration, - }, - UnknownBlockAggregate { - message_id: MessageId, - peer_id: PeerId, - aggregate: Box>, - seen_timestamp: Duration, - }, - UnknownLightClientOptimisticUpdate { - message_id: MessageId, - peer_id: PeerId, - light_client_optimistic_update: Box>, - seen_timestamp: Duration, - }, - GossipAggregateBatch { - packages: Vec>, - }, - GossipBlock { - message_id: MessageId, - peer_id: PeerId, - peer_client: Client, - block: Arc>, - seen_timestamp: Duration, - }, - DelayedImportBlock { - peer_id: PeerId, - block: Box>, - seen_timestamp: Duration, - }, - GossipVoluntaryExit { - message_id: MessageId, - peer_id: PeerId, - voluntary_exit: Box, - }, - GossipProposerSlashing { - message_id: MessageId, - peer_id: PeerId, - proposer_slashing: Box, - }, - GossipAttesterSlashing { - message_id: MessageId, - peer_id: PeerId, - attester_slashing: Box>, - }, - GossipSyncSignature { - message_id: MessageId, - peer_id: PeerId, - sync_signature: Box, - subnet_id: SyncSubnetId, - seen_timestamp: Duration, - }, - GossipSyncContribution { - message_id: MessageId, - peer_id: PeerId, - sync_contribution: Box>, - seen_timestamp: Duration, - }, - GossipLightClientFinalityUpdate { - message_id: MessageId, - peer_id: PeerId, - light_client_finality_update: Box>, - seen_timestamp: Duration, - }, - GossipLightClientOptimisticUpdate { - message_id: MessageId, - peer_id: PeerId, - light_client_optimistic_update: Box>, - seen_timestamp: Duration, - }, - RpcBlock { - block_root: Hash256, - block: Arc>, - seen_timestamp: Duration, - process_type: BlockProcessType, - should_process: bool, - }, - ChainSegment { - process_id: ChainSegmentProcessId, - blocks: Vec>>, - }, - Status { - peer_id: PeerId, - message: StatusMessage, - }, - BlocksByRangeRequest { - peer_id: PeerId, - request_id: PeerRequestId, - request: BlocksByRangeRequest, - }, - BlocksByRootsRequest { - peer_id: PeerId, - request_id: PeerRequestId, - request: BlocksByRootRequest, - }, - GossipBlsToExecutionChange { - message_id: MessageId, - peer_id: PeerId, - bls_to_execution_change: Box, - }, - LightClientBootstrapRequest { - peer_id: PeerId, - request_id: PeerRequestId, - request: LightClientBootstrapRequest, - }, +pub type AsyncFn = Pin + Send + Sync>>; +pub type BlockingFn = Box; +pub type BlockingFnWithManualSendOnIdle = Box; +pub enum BlockingOrAsync { + Blocking(BlockingFn), + Async(AsyncFn), } -impl Work { +/// Indicates the type of work to be performed and therefore its priority and +/// queuing specifics. +pub enum Work { + GossipAttestation { + attestation: GossipAttestationPackage, + process_individual: Box) + Send + Sync>, + process_batch: Box>) + Send + Sync>, + }, + UnknownBlockAttestation { + process_fn: BlockingFn, + }, + GossipAttestationBatch { + attestations: Vec>, + process_batch: Box>) + Send + Sync>, + }, + GossipAggregate { + aggregate: GossipAggregatePackage, + process_individual: Box) + Send + Sync>, + process_batch: Box>) + Send + Sync>, + }, + UnknownBlockAggregate { + process_fn: BlockingFn, + }, + UnknownLightClientOptimisticUpdate { + parent_root: Hash256, + process_fn: BlockingFn, + }, + GossipAggregateBatch { + aggregates: Vec>, + process_batch: Box>) + Send + Sync>, + }, + GossipBlock(AsyncFn), + DelayedImportBlock { + beacon_block_slot: Slot, + beacon_block_root: Hash256, + process_fn: AsyncFn, + }, + GossipVoluntaryExit(BlockingFn), + GossipProposerSlashing(BlockingFn), + GossipAttesterSlashing(BlockingFn), + GossipSyncSignature(BlockingFn), + GossipSyncContribution(BlockingFn), + GossipLightClientFinalityUpdate(BlockingFn), + GossipLightClientOptimisticUpdate(BlockingFn), + RpcBlock { + process_fn: AsyncFn, + }, + IgnoredRpcBlock { + process_fn: BlockingFn, + }, + ChainSegment(AsyncFn), + ChainSegmentBackfill(AsyncFn), + Status(BlockingFn), + BlocksByRangeRequest(BlockingFnWithManualSendOnIdle), + BlocksByRootsRequest(BlockingFnWithManualSendOnIdle), + GossipBlsToExecutionChange(BlockingFn), + LightClientBootstrapRequest(BlockingFn), + ApiRequestP0(BlockingOrAsync), + ApiRequestP1(BlockingOrAsync), +} + +impl fmt::Debug for Work { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.str_id()) + } +} + +impl Work { /// Provides a `&str` that uniquely identifies each enum variant. fn str_id(&self) -> &'static str { match self { @@ -891,58 +609,58 @@ impl Work { Work::GossipAttestationBatch { .. } => GOSSIP_ATTESTATION_BATCH, Work::GossipAggregate { .. } => GOSSIP_AGGREGATE, Work::GossipAggregateBatch { .. } => GOSSIP_AGGREGATE_BATCH, - Work::GossipBlock { .. } => GOSSIP_BLOCK, + Work::GossipBlock(_) => GOSSIP_BLOCK, Work::DelayedImportBlock { .. } => DELAYED_IMPORT_BLOCK, - Work::GossipVoluntaryExit { .. } => GOSSIP_VOLUNTARY_EXIT, - Work::GossipProposerSlashing { .. } => GOSSIP_PROPOSER_SLASHING, - Work::GossipAttesterSlashing { .. } => GOSSIP_ATTESTER_SLASHING, - Work::GossipSyncSignature { .. } => GOSSIP_SYNC_SIGNATURE, - Work::GossipSyncContribution { .. } => GOSSIP_SYNC_CONTRIBUTION, - Work::GossipLightClientFinalityUpdate { .. } => GOSSIP_LIGHT_CLIENT_FINALITY_UPDATE, - Work::GossipLightClientOptimisticUpdate { .. } => GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE, + Work::GossipVoluntaryExit(_) => GOSSIP_VOLUNTARY_EXIT, + Work::GossipProposerSlashing(_) => GOSSIP_PROPOSER_SLASHING, + Work::GossipAttesterSlashing(_) => GOSSIP_ATTESTER_SLASHING, + Work::GossipSyncSignature(_) => GOSSIP_SYNC_SIGNATURE, + Work::GossipSyncContribution(_) => GOSSIP_SYNC_CONTRIBUTION, + Work::GossipLightClientFinalityUpdate(_) => GOSSIP_LIGHT_CLIENT_FINALITY_UPDATE, + Work::GossipLightClientOptimisticUpdate(_) => GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE, Work::RpcBlock { .. } => RPC_BLOCK, - Work::ChainSegment { - process_id: ChainSegmentProcessId::BackSyncBatchId { .. }, - .. - } => CHAIN_SEGMENT_BACKFILL, + Work::IgnoredRpcBlock { .. } => IGNORED_RPC_BLOCK, Work::ChainSegment { .. } => CHAIN_SEGMENT, - Work::Status { .. } => STATUS_PROCESSING, - Work::BlocksByRangeRequest { .. } => BLOCKS_BY_RANGE_REQUEST, - Work::BlocksByRootsRequest { .. } => BLOCKS_BY_ROOTS_REQUEST, - Work::LightClientBootstrapRequest { .. } => LIGHT_CLIENT_BOOTSTRAP_REQUEST, + Work::ChainSegmentBackfill(_) => CHAIN_SEGMENT_BACKFILL, + Work::Status(_) => STATUS_PROCESSING, + Work::BlocksByRangeRequest(_) => BLOCKS_BY_RANGE_REQUEST, + Work::BlocksByRootsRequest(_) => BLOCKS_BY_ROOTS_REQUEST, + Work::LightClientBootstrapRequest(_) => LIGHT_CLIENT_BOOTSTRAP_REQUEST, Work::UnknownBlockAttestation { .. } => UNKNOWN_BLOCK_ATTESTATION, Work::UnknownBlockAggregate { .. } => UNKNOWN_BLOCK_AGGREGATE, - Work::GossipBlsToExecutionChange { .. } => GOSSIP_BLS_TO_EXECUTION_CHANGE, + Work::GossipBlsToExecutionChange(_) => GOSSIP_BLS_TO_EXECUTION_CHANGE, Work::UnknownLightClientOptimisticUpdate { .. } => UNKNOWN_LIGHT_CLIENT_UPDATE, + Work::ApiRequestP0 { .. } => API_REQUEST_P0, + Work::ApiRequestP1 { .. } => API_REQUEST_P1, } } } /// Unifies all the messages processed by the `BeaconProcessor`. -enum InboundEvent { +enum InboundEvent { /// A worker has completed a task and is free. WorkerIdle, /// There is new work to be done. - WorkEvent(WorkEvent), + WorkEvent(WorkEvent), /// A work event that was queued for re-processing has become ready. - ReprocessingWork(WorkEvent), + ReprocessingWork(WorkEvent), } /// Combines the various incoming event streams for the `BeaconProcessor` into a single stream. /// /// This struct has a similar purpose to `tokio::select!`, however it allows for more fine-grained /// control (specifically in the ordering of event processing). -struct InboundEvents { +struct InboundEvents { /// Used by workers when they finish a task. idle_rx: mpsc::Receiver<()>, /// Used by upstream processes to send new work to the `BeaconProcessor`. - event_rx: mpsc::Receiver>, + event_rx: mpsc::Receiver>, /// Used internally for queuing work ready to be re-processed. - reprocess_work_rx: mpsc::Receiver>, + reprocess_work_rx: mpsc::Receiver, } -impl Stream for InboundEvents { - type Item = InboundEvent; +impl Stream for InboundEvents { + type Item = InboundEvent; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // Always check for idle workers before anything else. This allows us to ensure that a big @@ -983,47 +701,39 @@ impl Stream for InboundEvents { } } -/// Defines if and where we will store the SSZ files of invalid blocks. -#[derive(Clone)] -pub enum InvalidBlockStorage { - Enabled(PathBuf), - Disabled, -} - /// A mutli-threaded processor for messages received on the network /// that need to be processed by the `BeaconChain` /// /// See module level documentation for more information. -pub struct BeaconProcessor { - pub beacon_chain: Weak>, - pub network_tx: mpsc::UnboundedSender>, - pub sync_tx: mpsc::UnboundedSender>, - pub network_globals: Arc>, +pub struct BeaconProcessor { + pub network_globals: Arc>, pub executor: TaskExecutor, - pub max_workers: usize, pub current_workers: usize, - pub importing_blocks: DuplicateCache, - pub invalid_block_storage: InvalidBlockStorage, + pub config: BeaconProcessorConfig, pub log: Logger, } -impl BeaconProcessor { +impl BeaconProcessor { /// Spawns the "manager" task which checks the receiver end of the returned `Sender` for /// messages which contain some new work which will be: /// /// - Performed immediately, if a worker is available. /// - Queued for later processing, if no worker is currently available. /// - /// Only `self.max_workers` will ever be spawned at one time. Each worker is a `tokio` task + /// Only `self.config.max_workers` will ever be spawned at one time. Each worker is a `tokio` task /// started with `spawn_blocking`. /// /// The optional `work_journal_tx` allows for an outside process to receive a log of all work /// events processed by `self`. This should only be used during testing. - pub fn spawn_manager( + pub fn spawn_manager( mut self, - event_rx: mpsc::Receiver>, + 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, + ) -> Result<(), String> { // Used by workers to communicate that they are finished a task. let (idle_tx, idle_rx) = mpsc::channel::<()>(MAX_IDLE_QUEUE_LEN); @@ -1075,22 +785,21 @@ impl BeaconProcessor { let mut lcbootstrap_queue = FifoQueue::new(MAX_LIGHT_CLIENT_BOOTSTRAP_QUEUE_LEN); - let chain = match self.beacon_chain.upgrade() { - Some(chain) => chain, - // No need to proceed any further if the beacon chain has been dropped, the client - // is shutting down. - None => return, - }; + let mut api_request_p0_queue = FifoQueue::new(MAX_API_REQUEST_P0_QUEUE_LEN); + let mut api_request_p1_queue = FifoQueue::new(MAX_API_REQUEST_P1_QUEUE_LEN); // Channels for sending work to the re-process scheduler (`work_reprocessing_tx`) and to // receive them back once they are ready (`ready_work_rx`). - let (ready_work_tx, ready_work_rx) = mpsc::channel(MAX_SCHEDULED_WORK_QUEUE_LEN); - let work_reprocessing_tx = spawn_reprocess_scheduler( + let (ready_work_tx, ready_work_rx) = + mpsc::channel::(self.config.max_scheduled_work_queue_len); + spawn_reprocess_scheduler( ready_work_tx, + work_reprocessing_rx, &self.executor, - chain.slot_clock.clone(), + slot_clock, self.log.clone(), - ); + maximum_gossip_clock_disparity, + )?; let executor = self.executor.clone(); @@ -1103,7 +812,7 @@ impl BeaconProcessor { reprocess_work_rx: ready_work_rx, }; - let enable_backfill_rate_limiting = chain.config.enable_backfill_rate_limiting; + let enable_backfill_rate_limiting = self.config.enable_backfill_rate_limiting; loop { let work_event = match inbound_events.next().await { @@ -1186,49 +895,50 @@ impl BeaconProcessor { let _ = work_journal_tx.try_send(id); } - let can_spawn = self.current_workers < self.max_workers; + let can_spawn = self.current_workers < self.config.max_workers; let drop_during_sync = work_event .as_ref() .map_or(false, |event| event.drop_during_sync); + let idle_tx = idle_tx.clone(); match work_event { // There is no new work event, but we are able to spawn a new worker. // // We don't check the `work.drop_during_sync` here. We assume that if it made // it into the queue at any point then we should process it. None if can_spawn => { - let toolbox = Toolbox { - idle_tx: idle_tx.clone(), - work_reprocessing_tx: work_reprocessing_tx.clone(), - }; - // Check for chain segments first, they're the most efficient way to get // blocks into the system. if let Some(item) = chain_segment_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check sync blocks before gossip blocks, since we've already explicitly // requested these blocks. } else if let Some(item) = rpc_block_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check delayed blocks before gossip blocks, the gossip blocks might rely // on the delayed ones. } else if let Some(item) = delayed_block_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check gossip blocks before gossip attestations, since a block might be // required to verify some attestations. } else if let Some(item) = gossip_block_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); + // Check the priority 0 API requests after blocks, but before attestations. + } else if let Some(item) = api_request_p0_queue.pop() { + self.spawn_worker(item, idle_tx); // Check the aggregates, *then* the unaggregates since we assume that // aggregates are more valuable to local validators and effectively give us // more information with less signature verification time. } else if aggregate_queue.len() > 0 { - let batch_size = - cmp::min(aggregate_queue.len(), MAX_GOSSIP_AGGREGATE_BATCH_SIZE); + let batch_size = cmp::min( + aggregate_queue.len(), + self.config.max_gossip_aggregate_batch_size, + ); if batch_size < 2 { // One single aggregate is in the queue, process it individually. if let Some(item) = aggregate_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } } else { // Collect two or more aggregates into a batch, so they can take @@ -1236,32 +946,45 @@ impl BeaconProcessor { // // Note: this will convert the `Work::GossipAggregate` item into a // `Work::GossipAggregateBatch` item. - let mut packages = Vec::with_capacity(batch_size); + let mut aggregates = Vec::with_capacity(batch_size); + let mut process_batch_opt = None; for _ in 0..batch_size { if let Some(item) = aggregate_queue.pop() { match item { Work::GossipAggregate { - message_id, - peer_id, aggregate, - seen_timestamp, + process_individual: _, + process_batch, } => { - packages.push(GossipAggregatePackage::new( - message_id, - peer_id, - aggregate, - seen_timestamp, - )); + aggregates.push(aggregate); + if process_batch_opt.is_none() { + process_batch_opt = Some(process_batch); + } } _ => { - error!(self.log, "Invalid item in aggregate queue") + error!(self.log, "Invalid item in aggregate queue"); } } } } - // Process all aggregates with a single worker. - self.spawn_worker(Work::GossipAggregateBatch { packages }, toolbox) + if let Some(process_batch) = process_batch_opt { + // Process all aggregates with a single worker. + self.spawn_worker( + Work::GossipAggregateBatch { + aggregates, + process_batch, + }, + idle_tx, + ) + } else { + // There is no good reason for this to + // happen, it is a serious logic error. + // Since we only form batches when multiple + // work items exist, we should always have a + // work closure at this point. + crit!(self.log, "Missing aggregate work"); + } } // Check the unaggregated attestation queue. // @@ -1269,13 +992,13 @@ impl BeaconProcessor { } else if attestation_queue.len() > 0 { let batch_size = cmp::min( attestation_queue.len(), - MAX_GOSSIP_ATTESTATION_BATCH_SIZE, + self.config.max_gossip_attestation_batch_size, ); if batch_size < 2 { // One single attestation is in the queue, process it individually. if let Some(item) = attestation_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } } else { // Collect two or more attestations into a batch, so they can take @@ -1283,26 +1006,20 @@ impl BeaconProcessor { // // Note: this will convert the `Work::GossipAttestation` item into a // `Work::GossipAttestationBatch` item. - let mut packages = Vec::with_capacity(batch_size); + let mut attestations = Vec::with_capacity(batch_size); + let mut process_batch_opt = None; for _ in 0..batch_size { if let Some(item) = attestation_queue.pop() { match item { Work::GossipAttestation { - message_id, - peer_id, attestation, - subnet_id, - should_import, - seen_timestamp, + process_individual: _, + process_batch, } => { - packages.push(GossipAttestationPackage::new( - message_id, - peer_id, - attestation, - subnet_id, - should_import, - seen_timestamp, - )); + attestations.push(attestation); + if process_batch_opt.is_none() { + process_batch_opt = Some(process_batch); + } } _ => error!( self.log, @@ -1312,54 +1029,72 @@ impl BeaconProcessor { } } - // Process all attestations with a single worker. - self.spawn_worker( - Work::GossipAttestationBatch { packages }, - toolbox, - ) + if let Some(process_batch) = process_batch_opt { + // Process all attestations with a single worker. + self.spawn_worker( + Work::GossipAttestationBatch { + attestations, + process_batch, + }, + idle_tx, + ) + } else { + // There is no good reason for this to + // happen, it is a serious logic error. + // Since we only form batches when multiple + // work items exist, we should always have a + // work closure at this point. + crit!(self.log, "Missing attestations work"); + } } // Check sync committee messages after attestations as their rewards are lesser // and they don't influence fork choice. } else if let Some(item) = sync_contribution_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = sync_message_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Aggregates and unaggregates queued for re-processing are older and we // care about fresher ones, so check those first. } else if let Some(item) = unknown_block_aggregate_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = unknown_block_attestation_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check RPC methods next. Status messages are needed for sync so // prioritize them over syncing requests from other peers (BlocksByRange // and BlocksByRoot) } else if let Some(item) = status_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = bbrange_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = bbroots_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check slashings after all other consensus messages so we prioritize // following head. // // Check attester slashings before proposer slashings since they have the // potential to slash multiple validators at once. } else if let Some(item) = gossip_attester_slashing_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = gossip_proposer_slashing_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // Check exits and address changes late since our validators don't get // rewards from them. } else if let Some(item) = gossip_voluntary_exit_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else if let Some(item) = gossip_bls_to_execution_change_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); + // Check the priority 1 API requests after we've + // processed all the interesting things from the network + // and things required for us to stay in good repute + // with our P2P peers. + } else if let Some(item) = api_request_p1_queue.pop() { + self.spawn_worker(item, idle_tx); // Handle backfill sync chain segments. } else if let Some(item) = backfill_chain_segment.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); // This statement should always be the final else statement. } else if let Some(item) = lcbootstrap_queue.pop() { - self.spawn_worker(item, toolbox); + self.spawn_worker(item, idle_tx); } else { // Let the journal know that a worker is freed and there's nothing else // for it to do. @@ -1401,13 +1136,9 @@ impl BeaconProcessor { // it. Some(WorkEvent { work, .. }) => { let work_id = work.str_id(); - let toolbox = Toolbox { - idle_tx: idle_tx.clone(), - work_reprocessing_tx: work_reprocessing_tx.clone(), - }; match work { - _ if can_spawn => self.spawn_worker(work, toolbox), + _ if can_spawn => self.spawn_worker(work, idle_tx), Work::GossipAttestation { .. } => attestation_queue.push(work), // Attestation batches are formed internally within the // `BeaconProcessor`, they are not sent from external services. @@ -1449,16 +1180,15 @@ impl BeaconProcessor { Work::GossipLightClientOptimisticUpdate { .. } => { optimistic_update_queue.push(work, work_id, &self.log) } - Work::RpcBlock { .. } => rpc_block_queue.push(work, work_id, &self.log), - Work::ChainSegment { ref process_id, .. } => match process_id { - ChainSegmentProcessId::RangeBatchId { .. } - | ChainSegmentProcessId::ParentLookup { .. } => { - chain_segment_queue.push(work, work_id, &self.log) - } - ChainSegmentProcessId::BackSyncBatchId { .. } => { - backfill_chain_segment.push(work, work_id, &self.log) - } - }, + Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { + rpc_block_queue.push(work, work_id, &self.log) + } + Work::ChainSegment { .. } => { + chain_segment_queue.push(work, work_id, &self.log) + } + Work::ChainSegmentBackfill { .. } => { + backfill_chain_segment.push(work, work_id, &self.log) + } Work::Status { .. } => status_queue.push(work, work_id, &self.log), Work::BlocksByRangeRequest { .. } => { bbrange_queue.push(work, work_id, &self.log) @@ -1481,6 +1211,12 @@ impl BeaconProcessor { Work::UnknownLightClientOptimisticUpdate { .. } => { unknown_light_client_update_queue.push(work, work_id, &self.log) } + Work::ApiRequestP0 { .. } => { + api_request_p0_queue.push(work, work_id, &self.log) + } + Work::ApiRequestP1 { .. } => { + api_request_p1_queue.push(work, work_id, &self.log) + } } } } @@ -1537,6 +1273,14 @@ impl BeaconProcessor { &metrics::BEACON_PROCESSOR_BLS_TO_EXECUTION_CHANGE_QUEUE_TOTAL, gossip_bls_to_execution_change_queue.len() as i64, ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_API_REQUEST_P0_QUEUE_TOTAL, + api_request_p0_queue.len() as i64, + ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_API_REQUEST_P1_QUEUE_TOTAL, + api_request_p1_queue.len() as i64, + ); if aggregate_queue.is_full() && aggregate_debounce.elapsed() { error!( @@ -1560,15 +1304,13 @@ impl BeaconProcessor { // Spawn on the core executor. executor.spawn(manager_future, MANAGER_TASK_NAME); + Ok(()) } /// 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, toolbox: Toolbox) { - let idle_tx = toolbox.idle_tx; - let work_reprocessing_tx = toolbox.work_reprocessing_tx; - + fn spawn_worker(&mut self, work: Work, idle_tx: mpsc::Sender<()>) { let work_id = work.str_id(); let worker_timer = metrics::start_timer_vec(&metrics::BEACON_PROCESSOR_WORKER_TIME, &[work_id]); @@ -1591,27 +1333,8 @@ impl BeaconProcessor { let worker_id = self.current_workers; self.current_workers = self.current_workers.saturating_add(1); - let chain = if let Some(chain) = self.beacon_chain.upgrade() { - chain - } else { - debug!( - self.log, - "Beacon chain dropped, shutting down"; - ); - return; - }; - let executor = self.executor.clone(); - let worker = Worker { - chain, - network_tx: self.network_tx.clone(), - sync_tx: self.sync_tx.clone(), - log: self.log.clone(), - }; - - let duplicate_cache = self.importing_blocks.clone(); - trace!( self.log, "Spawning beacon processor worker"; @@ -1620,349 +1343,76 @@ impl BeaconProcessor { ); let task_spawner = TaskSpawner { - executor: executor.clone(), + executor, send_idle_on_drop, }; - let sub_executor = executor; match work { - /* - * Individual unaggregated attestation verification. - */ Work::GossipAttestation { - message_id, - peer_id, attestation, - subnet_id, - should_import, - seen_timestamp, + process_individual, + process_batch: _, } => task_spawner.spawn_blocking(move || { - worker.process_gossip_attestation( - message_id, - peer_id, - attestation, - subnet_id, - should_import, - Some(work_reprocessing_tx), - seen_timestamp, - ) + process_individual(attestation); }), - /* - * Batched unaggregated attestation verification. - */ - Work::GossipAttestationBatch { packages } => task_spawner.spawn_blocking(|| { - worker.process_gossip_attestation_batch(packages, Some(work_reprocessing_tx)) + Work::GossipAttestationBatch { + attestations, + process_batch, + } => task_spawner.spawn_blocking(move || { + process_batch(attestations); }), - /* - * Individual aggregated attestation verification. - */ Work::GossipAggregate { - message_id, - peer_id, aggregate, - seen_timestamp, + process_individual, + process_batch: _, } => task_spawner.spawn_blocking(move || { - worker.process_gossip_aggregate( - message_id, - peer_id, - aggregate, - Some(work_reprocessing_tx), - seen_timestamp, - ) + process_individual(aggregate); }), - /* - * Batched aggregated attestation verification. - */ - Work::GossipAggregateBatch { packages } => task_spawner.spawn_blocking(|| { - worker.process_gossip_aggregate_batch(packages, Some(work_reprocessing_tx)) - }), - /* - * Verification for beacon blocks received on gossip. - */ - Work::GossipBlock { - message_id, - peer_id, - peer_client, - block, - seen_timestamp, - } => { - let invalid_block_storage = self.invalid_block_storage.clone(); - task_spawner.spawn_async(async move { - worker - .process_gossip_block( - message_id, - peer_id, - peer_client, - block, - work_reprocessing_tx, - duplicate_cache, - invalid_block_storage, - seen_timestamp, - ) - .await - }) - } - /* - * Import for blocks that we received earlier than their intended slot. - */ - Work::DelayedImportBlock { - peer_id, - block, - seen_timestamp, - } => { - let invalid_block_storage = self.invalid_block_storage.clone(); - task_spawner.spawn_async(worker.process_gossip_verified_block( - peer_id, - *block, - work_reprocessing_tx, - invalid_block_storage, - seen_timestamp, - )) - } - /* - * Voluntary exits received on gossip. - */ - Work::GossipVoluntaryExit { - message_id, - peer_id, - voluntary_exit, + Work::GossipAggregateBatch { + aggregates, + process_batch, } => task_spawner.spawn_blocking(move || { - worker.process_gossip_voluntary_exit(message_id, peer_id, *voluntary_exit) + process_batch(aggregates); }), - /* - * Proposer slashings received on gossip. - */ - Work::GossipProposerSlashing { - message_id, - peer_id, - proposer_slashing, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing) - }), - /* - * Attester slashings received on gossip. - */ - Work::GossipAttesterSlashing { - message_id, - peer_id, - attester_slashing, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing) - }), - /* - * Sync committee message verification. - */ - Work::GossipSyncSignature { - message_id, - peer_id, - sync_signature, - subnet_id, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_sync_committee_signature( - message_id, - peer_id, - *sync_signature, - subnet_id, - seen_timestamp, - ) - }), - /* - * Sync contribution verification. - */ - Work::GossipSyncContribution { - message_id, - peer_id, - sync_contribution, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_sync_committee_contribution( - message_id, - peer_id, - *sync_contribution, - seen_timestamp, - ) - }), - /* - * BLS to execution change verification. - */ - Work::GossipBlsToExecutionChange { - message_id, - peer_id, - bls_to_execution_change, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_bls_to_execution_change( - message_id, - peer_id, - *bls_to_execution_change, - ) - }), - /* - * Light client finality update verification. - */ - Work::GossipLightClientFinalityUpdate { - message_id, - peer_id, - light_client_finality_update, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_finality_update( - message_id, - peer_id, - *light_client_finality_update, - seen_timestamp, - ) - }), - /* - * Light client optimistic update verification. - */ - Work::GossipLightClientOptimisticUpdate { - message_id, - peer_id, - light_client_optimistic_update, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_optimistic_update( - message_id, - peer_id, - *light_client_optimistic_update, - Some(work_reprocessing_tx), - seen_timestamp, - ) - }), - /* - * Verification for beacon blocks received during syncing via RPC. - */ - Work::RpcBlock { - block_root, - block, - seen_timestamp, - process_type, - should_process, - } => task_spawner.spawn_async(worker.process_rpc_block( - block_root, - block, - seen_timestamp, - process_type, - work_reprocessing_tx, - duplicate_cache, - should_process, - )), - /* - * Verification for a chain segment (multiple blocks). - */ - Work::ChainSegment { process_id, blocks } => { - let notify_execution_layer = if self - .network_globals - .sync_state - .read() - .is_syncing_finalized() - { - NotifyExecutionLayer::No - } else { - NotifyExecutionLayer::Yes - }; - - task_spawner.spawn_async(async move { - worker - .process_chain_segment(process_id, blocks, notify_execution_layer) - .await - }) - } - /* - * Processing of Status Messages. - */ - Work::Status { peer_id, message } => { - task_spawner.spawn_blocking(move || worker.process_status(peer_id, message)) - } - /* - * Processing of range syncing requests from other peers. - */ - Work::BlocksByRangeRequest { - peer_id, - request_id, - request, - } => task_spawner.spawn_blocking_with_manual_send_idle(move |send_idle_on_drop| { - worker.handle_blocks_by_range_request( - sub_executor, - send_idle_on_drop, - peer_id, - request_id, - request, - ) - }), - /* - * Processing of blocks by roots requests from other peers. - */ - Work::BlocksByRootsRequest { - peer_id, - request_id, - request, - } => task_spawner.spawn_blocking_with_manual_send_idle(move |send_idle_on_drop| { - worker.handle_blocks_by_root_request( - sub_executor, - send_idle_on_drop, - peer_id, - request_id, - request, - ) - }), - /* - * Processing of lightclient bootstrap requests from other peers. - */ - Work::LightClientBootstrapRequest { - peer_id, - request_id, - request, - } => task_spawner.spawn_blocking(move || { - worker.handle_light_client_bootstrap(peer_id, request_id, request) - }), - Work::UnknownBlockAttestation { - message_id, - peer_id, - attestation, - subnet_id, - should_import, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_attestation( - message_id, - peer_id, - attestation, - subnet_id, - should_import, - None, // Do not allow this attestation to be re-processed beyond this point. - seen_timestamp, - ) - }), - Work::UnknownBlockAggregate { - message_id, - peer_id, - aggregate, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_aggregate( - message_id, - peer_id, - aggregate, - None, - seen_timestamp, - ) + Work::ChainSegment(process_fn) => task_spawner.spawn_async(async move { + process_fn.await; }), + Work::UnknownBlockAttestation { process_fn } => task_spawner.spawn_blocking(process_fn), + Work::UnknownBlockAggregate { process_fn } => task_spawner.spawn_blocking(process_fn), Work::UnknownLightClientOptimisticUpdate { - message_id, - peer_id, - light_client_optimistic_update, - seen_timestamp, - } => task_spawner.spawn_blocking(move || { - worker.process_gossip_optimistic_update( - message_id, - peer_id, - *light_client_optimistic_update, - None, - seen_timestamp, - ) + parent_root: _, + process_fn, + } => task_spawner.spawn_blocking(process_fn), + Work::DelayedImportBlock { + beacon_block_slot: _, + beacon_block_root: _, + process_fn, + } => task_spawner.spawn_async(process_fn), + Work::RpcBlock { process_fn } => task_spawner.spawn_async(process_fn), + Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), + Work::GossipBlock(work) => task_spawner.spawn_async(async move { + work.await; }), + Work::BlocksByRangeRequest(work) | Work::BlocksByRootsRequest(work) => { + task_spawner.spawn_blocking_with_manual_send_idle(work) + } + Work::ChainSegmentBackfill(process_fn) => task_spawner.spawn_async(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), + }, + Work::GossipVoluntaryExit(process_fn) + | Work::GossipProposerSlashing(process_fn) + | Work::GossipAttesterSlashing(process_fn) + | Work::GossipSyncSignature(process_fn) + | Work::GossipSyncContribution(process_fn) + | Work::GossipLightClientFinalityUpdate(process_fn) + | Work::GossipLightClientOptimisticUpdate(process_fn) + | Work::Status(process_fn) + | Work::GossipBlsToExecutionChange(process_fn) + | Work::LightClientBootstrapRequest(process_fn) => { + task_spawner.spawn_blocking(process_fn) + } }; } } diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs new file mode 100644 index 000000000..e14c39e9a --- /dev/null +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -0,0 +1,150 @@ +pub use lighthouse_metrics::*; + +lazy_static::lazy_static! { + + /* + * Gossip processor + */ + pub static ref BEACON_PROCESSOR_WORK_EVENTS_RX_COUNT: Result = try_create_int_counter_vec( + "beacon_processor_work_events_rx_count", + "Count of work events received (but not necessarily processed)", + &["type"] + ); + pub static ref BEACON_PROCESSOR_WORK_EVENTS_IGNORED_COUNT: Result = try_create_int_counter_vec( + "beacon_processor_work_events_ignored_count", + "Count of work events purposefully ignored", + &["type"] + ); + pub static ref BEACON_PROCESSOR_WORK_EVENTS_STARTED_COUNT: Result = try_create_int_counter_vec( + "beacon_processor_work_events_started_count", + "Count of work events which have been started by a worker", + &["type"] + ); + pub static ref BEACON_PROCESSOR_WORKER_TIME: Result = try_create_histogram_vec( + "beacon_processor_worker_time", + "Time taken for a worker to fully process some parcel of work.", + &["type"] + ); + pub static ref BEACON_PROCESSOR_WORKERS_SPAWNED_TOTAL: Result = try_create_int_counter( + "beacon_processor_workers_spawned_total", + "The number of workers ever spawned by the gossip processing pool." + ); + pub static ref BEACON_PROCESSOR_WORKERS_ACTIVE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_workers_active_total", + "Count of active workers in the gossip processing pool." + ); + pub static ref BEACON_PROCESSOR_IDLE_EVENTS_TOTAL: Result = try_create_int_counter( + "beacon_processor_idle_events_total", + "Count of idle events processed by the gossip processor manager." + ); + pub static ref BEACON_PROCESSOR_EVENT_HANDLING_SECONDS: Result = try_create_histogram( + "beacon_processor_event_handling_seconds", + "Time spent handling a new message and allocating it to a queue or worker." + ); + // Gossip blocks. + pub static ref BEACON_PROCESSOR_GOSSIP_BLOCK_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_gossip_block_queue_total", + "Count of blocks from gossip waiting to be verified." + ); + // Gossip Exits. + pub static ref BEACON_PROCESSOR_EXIT_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_exit_queue_total", + "Count of exits from gossip waiting to be verified." + ); + // Gossip proposer slashings. + pub static ref BEACON_PROCESSOR_PROPOSER_SLASHING_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_proposer_slashing_queue_total", + "Count of proposer slashings from gossip waiting to be verified." + ); + // Gossip attester slashings. + pub static ref BEACON_PROCESSOR_ATTESTER_SLASHING_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_attester_slashing_queue_total", + "Count of attester slashings from gossip waiting to be verified." + ); + // Gossip BLS to execution changes. + pub static ref BEACON_PROCESSOR_BLS_TO_EXECUTION_CHANGE_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_bls_to_execution_change_queue_total", + "Count of address changes from gossip waiting to be verified." + ); + // Rpc blocks. + pub static ref BEACON_PROCESSOR_RPC_BLOCK_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_rpc_block_queue_total", + "Count of blocks from the rpc waiting to be verified." + ); + // Chain segments. + pub static ref BEACON_PROCESSOR_CHAIN_SEGMENT_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_chain_segment_queue_total", + "Count of chain segments from the rpc waiting to be verified." + ); + pub static ref BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_backfill_chain_segment_queue_total", + "Count of backfill chain segments from the rpc waiting to be verified." + ); + // Unaggregated attestations. + pub static ref BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_unaggregated_attestation_queue_total", + "Count of unagg. attestations waiting to be processed." + ); + // Aggregated attestations. + pub static ref BEACON_PROCESSOR_AGGREGATED_ATTESTATION_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_aggregated_attestation_queue_total", + "Count of agg. attestations waiting to be processed." + ); + // Sync committee messages. + pub static ref BEACON_PROCESSOR_SYNC_MESSAGE_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_sync_message_queue_total", + "Count of sync committee messages waiting to be processed." + ); + // Sync contribution. + pub static ref BEACON_PROCESSOR_SYNC_CONTRIBUTION_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_sync_contribution_queue_total", + "Count of sync committee contributions waiting to be processed." + ); + // HTTP API requests. + pub static ref BEACON_PROCESSOR_API_REQUEST_P0_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_api_request_p0_queue_total", + "Count of P0 HTTP requesets waiting to be processed." + ); + pub static ref BEACON_PROCESSOR_API_REQUEST_P1_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_api_request_p1_queue_total", + "Count of P1 HTTP requesets waiting to be processed." + ); + + /* + * Attestation reprocessing queue metrics. + */ + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL: Result = + try_create_int_gauge_vec( + "beacon_processor_reprocessing_queue_total", + "Count of items in a reprocessing queue.", + &["type"] + ); + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_EXPIRED_ATTESTATIONS: Result = try_create_int_counter( + "beacon_processor_reprocessing_queue_expired_attestations", + "Number of queued attestations which have expired before a matching block has been found." + ); + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_ATTESTATIONS: Result = try_create_int_counter( + "beacon_processor_reprocessing_queue_matched_attestations", + "Number of queued attestations where as matching block has been imported." + ); + + /* + * Light client update reprocessing queue metrics. + */ + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_EXPIRED_OPTIMISTIC_UPDATES: Result = 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." + ); + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: Result = try_create_int_counter( + "beacon_processor_reprocessing_queue_matched_optimistic_updates", + "Number of queued light client optimistic updates where as matching block has been imported." + ); + + /// Errors and Debugging Stats + pub static ref BEACON_PROCESSOR_SEND_ERROR_PER_WORK_TYPE: Result = + try_create_int_counter_vec( + "beacon_processor_send_error_per_work_type", + "Total number of beacon processor send error per work type", + &["type"] + ); +} diff --git a/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs similarity index 85% rename from beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs rename to beacon_node/beacon_processor/src/work_reprocessing_queue.rs index 427be6d51..9191509d3 100644 --- a/beacon_node/network/src/beacon_processor/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs @@ -10,23 +10,18 @@ //! //! Aggregated and unaggregated attestations that failed verification due to referencing an unknown //! block will be re-queued until their block is imported, or until they expire. -use super::MAX_SCHEDULED_WORK_QUEUE_LEN; -use crate::beacon_processor::{ChainSegmentProcessId, Work, WorkEvent}; use crate::metrics; -use crate::sync::manager::BlockProcessType; -use beacon_chain::{BeaconChainTypes, GossipVerifiedBlock, MAXIMUM_GOSSIP_CLOCK_DISPARITY}; +use crate::{AsyncFn, BlockingFn, Work, WorkEvent}; use fnv::FnvHashMap; use futures::task::Poll; use futures::{Stream, StreamExt}; use itertools::Itertools; -use lighthouse_network::{MessageId, PeerId}; use logging::TimeLatch; use slog::{crit, debug, error, trace, warn, Logger}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; -use std::sync::Arc; use std::task::Context; use std::time::Duration; use strum::AsRefStr; @@ -34,10 +29,7 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::time::error::Error as TimeError; use tokio_util::time::delay_queue::{DelayQueue, Key as DelayKey}; -use types::{ - Attestation, EthSpec, Hash256, LightClientOptimisticUpdate, SignedAggregateAndProof, - SignedBeaconBlock, SubnetId, -}; +use types::{EthSpec, Hash256, Slot}; const TASK_NAME: &str = "beacon_processor_reprocess_queue"; const GOSSIP_BLOCKS: &str = "gossip_blocks"; @@ -47,7 +39,7 @@ const LIGHT_CLIENT_UPDATES: &str = "lc_updates"; /// 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. -const ADDITIONAL_QUEUED_BLOCK_DELAY: Duration = Duration::from_millis(5); +pub const ADDITIONAL_QUEUED_BLOCK_DELAY: Duration = Duration::from_millis(5); /// For how long to queue aggregated and unaggregated attestations for re-processing. pub const QUEUED_ATTESTATION_DELAY: Duration = Duration::from_secs(12); @@ -84,12 +76,12 @@ pub const BACKFILL_SCHEDULE_IN_SLOT: [(u32, u32); 3] = [ /// Messages that the scheduler can receive. #[derive(AsRefStr)] -pub enum ReprocessQueueMessage { +pub enum ReprocessQueueMessage { /// A block that has been received early and we should queue for later processing. - EarlyBlock(QueuedGossipBlock), + EarlyBlock(QueuedGossipBlock), /// A gossip block for hash `X` is being imported, we should queue the rpc block for the same /// hash until the gossip block is imported. - RpcBlock(QueuedRpcBlock), + RpcBlock(QueuedRpcBlock), /// A block that was successfully processed. We use this to handle attestations and light client updates /// for unknown blocks. BlockImported { @@ -97,139 +89,127 @@ pub enum ReprocessQueueMessage { parent_root: Hash256, }, /// An unaggregated attestation that references an unknown block. - UnknownBlockUnaggregate(QueuedUnaggregate), + UnknownBlockUnaggregate(QueuedUnaggregate), /// An aggregated attestation that references an unknown block. - UnknownBlockAggregate(QueuedAggregate), + UnknownBlockAggregate(QueuedAggregate), /// A light client optimistic update that references a parent root that has not been seen as a parent. - UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), + UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), /// A new backfill batch that needs to be scheduled for processing. - BackfillSync(QueuedBackfillBatch), + BackfillSync(QueuedBackfillBatch), } /// Events sent by the scheduler once they are ready for re-processing. -pub enum ReadyWork { - Block(QueuedGossipBlock), - RpcBlock(QueuedRpcBlock), - Unaggregate(QueuedUnaggregate), - Aggregate(QueuedAggregate), - LightClientUpdate(QueuedLightClientUpdate), - BackfillSync(QueuedBackfillBatch), +pub enum ReadyWork { + Block(QueuedGossipBlock), + RpcBlock(QueuedRpcBlock), + IgnoredRpcBlock(IgnoredRpcBlock), + Unaggregate(QueuedUnaggregate), + Aggregate(QueuedAggregate), + LightClientUpdate(QueuedLightClientUpdate), + BackfillSync(QueuedBackfillBatch), } /// An Attestation for which the corresponding block was not seen while processing, queued for /// later. -pub struct QueuedUnaggregate { - pub peer_id: PeerId, - pub message_id: MessageId, - pub attestation: Box>, - pub subnet_id: SubnetId, - pub should_import: bool, - pub seen_timestamp: Duration, +pub struct QueuedUnaggregate { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, } /// An aggregated attestation for which the corresponding block was not seen while processing, queued for /// later. -pub struct QueuedAggregate { - pub peer_id: PeerId, - pub message_id: MessageId, - pub attestation: Box>, - pub seen_timestamp: Duration, +pub struct QueuedAggregate { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, } /// A light client update for which the corresponding parent block was not seen while processing, /// queued for later. -pub struct QueuedLightClientUpdate { - pub peer_id: PeerId, - pub message_id: MessageId, - pub light_client_optimistic_update: Box>, +pub struct QueuedLightClientUpdate { pub parent_root: Hash256, - pub seen_timestamp: Duration, + pub process_fn: BlockingFn, } /// A block that arrived early and has been queued for later import. -pub struct QueuedGossipBlock { - pub peer_id: PeerId, - pub block: Box>, - pub seen_timestamp: Duration, +pub struct QueuedGossipBlock { + pub beacon_block_slot: Slot, + pub beacon_block_root: Hash256, + pub process_fn: AsyncFn, } /// A block that arrived for processing when the same block was being imported over gossip. /// It is queued for later import. -pub struct QueuedRpcBlock { - pub block_root: Hash256, - pub block: Arc>, - pub process_type: BlockProcessType, - pub seen_timestamp: Duration, - /// Indicates if the beacon chain should process this block or not. - /// We use this to ignore block processing when rpc block queues are full. - pub should_process: bool, +pub struct QueuedRpcBlock { + pub beacon_block_root: Hash256, + /// Processes/imports the block. + pub process_fn: AsyncFn, + /// Ignores the block. + pub ignore_fn: BlockingFn, +} + +/// A block that arrived for processing when the same block was being imported over gossip. +/// It is queued for later import. +pub struct IgnoredRpcBlock { + pub process_fn: BlockingFn, } /// A backfill batch work that has been queued for processing later. -#[derive(Clone)] -pub struct QueuedBackfillBatch { - pub process_id: ChainSegmentProcessId, - pub blocks: Vec>>, -} +pub struct QueuedBackfillBatch(pub AsyncFn); -impl TryFrom> for QueuedBackfillBatch { +impl TryFrom> for QueuedBackfillBatch { type Error = WorkEvent; fn try_from(event: WorkEvent) -> Result> { match event { WorkEvent { - work: - Work::ChainSegment { - process_id: process_id @ ChainSegmentProcessId::BackSyncBatchId(_), - blocks, - }, + work: Work::ChainSegmentBackfill(process_fn), .. - } => Ok(QueuedBackfillBatch { process_id, blocks }), + } => Ok(QueuedBackfillBatch(process_fn)), _ => Err(event), } } } -impl From> for WorkEvent { - fn from(queued_backfill_batch: QueuedBackfillBatch) -> WorkEvent { - WorkEvent::chain_segment( - queued_backfill_batch.process_id, - queued_backfill_batch.blocks, - ) +impl From for WorkEvent { + fn from(queued_backfill_batch: QueuedBackfillBatch) -> WorkEvent { + WorkEvent { + drop_during_sync: false, + work: Work::ChainSegmentBackfill(queued_backfill_batch.0), + } } } /// Unifies the different messages processed by the block delay queue. -enum InboundEvent { +enum InboundEvent { /// A gossip block that was queued for later processing and is ready for import. - ReadyGossipBlock(QueuedGossipBlock), + ReadyGossipBlock(QueuedGossipBlock), /// A rpc block that was queued because the same gossip block was being imported /// will now be retried for import. - ReadyRpcBlock(QueuedRpcBlock), + ReadyRpcBlock(QueuedRpcBlock), /// An aggregated or unaggregated attestation is ready for re-processing. ReadyAttestation(QueuedAttestationId), /// A light client update that is ready for re-processing. ReadyLightClientUpdate(QueuedLightClientUpdateId), /// A backfill batch that was queued is ready for processing. - ReadyBackfillSync(QueuedBackfillBatch), + ReadyBackfillSync(QueuedBackfillBatch), /// A `DelayQueue` returned an error. DelayQueueError(TimeError, &'static str), /// A message sent to the `ReprocessQueue` - Msg(ReprocessQueueMessage), + Msg(ReprocessQueueMessage), } /// Manages scheduling works that need to be later re-processed. -struct ReprocessQueue { +struct ReprocessQueue { /// Receiver of messages relevant to schedule works for reprocessing. - work_reprocessing_rx: Receiver>, + work_reprocessing_rx: Receiver, /// Sender of works once they become ready - ready_work_tx: Sender>, + ready_work_tx: Sender, /* Queues */ /// Queue to manage scheduled early blocks. - gossip_block_delay_queue: DelayQueue>, + gossip_block_delay_queue: DelayQueue, /// Queue to manage scheduled early blocks. - rpc_block_delay_queue: DelayQueue>, + rpc_block_delay_queue: DelayQueue, /// Queue to manage scheduled attestations. attestations_delay_queue: DelayQueue, /// Queue to manage scheduled light client updates. @@ -239,17 +219,17 @@ struct ReprocessQueue { /// Queued blocks. queued_gossip_block_roots: HashSet, /// Queued aggregated attestations. - queued_aggregates: FnvHashMap, DelayKey)>, + queued_aggregates: FnvHashMap, /// Queued attestations. - queued_unaggregates: FnvHashMap, DelayKey)>, + queued_unaggregates: FnvHashMap, /// Attestations (aggregated and unaggregated) per root. awaiting_attestations_per_root: HashMap>, /// Queued Light Client Updates. - queued_lc_updates: FnvHashMap, DelayKey)>, + queued_lc_updates: FnvHashMap, /// Light Client Updates per parent_root. awaiting_lc_updates_per_parent_root: HashMap>, /// Queued backfill batches - queued_backfill_batches: Vec>, + queued_backfill_batches: Vec, /* Aux */ /// Next attestation id, used for both aggregated and unaggregated attestations @@ -260,7 +240,7 @@ struct ReprocessQueue { attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, - slot_clock: Pin>, + slot_clock: Pin>, } pub type QueuedLightClientUpdateId = usize; @@ -271,20 +251,20 @@ enum QueuedAttestationId { Unaggregate(usize), } -impl QueuedAggregate { +impl QueuedAggregate { pub fn beacon_block_root(&self) -> &Hash256 { - &self.attestation.message.aggregate.data.beacon_block_root + &self.beacon_block_root } } -impl QueuedUnaggregate { +impl QueuedUnaggregate { pub fn beacon_block_root(&self) -> &Hash256 { - &self.attestation.data.beacon_block_root + &self.beacon_block_root } } -impl Stream for ReprocessQueue { - type Item = InboundEvent; +impl Stream for ReprocessQueue { + type Item = InboundEvent; fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // NOTE: implementing `Stream` is not necessary but allows to maintain the future selection @@ -375,16 +355,18 @@ impl Stream for ReprocessQueue { /// Starts the job that manages scheduling works that need re-processing. The returned `Sender` /// gives the communicating channel to receive those works. Once a work is ready, it is sent back /// via `ready_work_tx`. -pub fn spawn_reprocess_scheduler( - ready_work_tx: Sender>, +pub fn spawn_reprocess_scheduler( + ready_work_tx: Sender, + work_reprocessing_rx: Receiver, executor: &TaskExecutor, - slot_clock: T::SlotClock, + slot_clock: S, log: Logger, -) -> Sender> { - let (work_reprocessing_tx, work_reprocessing_rx) = mpsc::channel(MAX_SCHEDULED_WORK_QUEUE_LEN); - // Basic sanity check. - assert!(ADDITIONAL_QUEUED_BLOCK_DELAY < MAXIMUM_GOSSIP_CLOCK_DISPARITY); - + maximum_gossip_clock_disparity: Duration, +) -> Result<(), String> { + // Sanity check + if ADDITIONAL_QUEUED_BLOCK_DELAY >= maximum_gossip_clock_disparity { + return Err("The block delay and gossip disparity don't match.".to_string()); + } let mut queue = ReprocessQueue { work_reprocessing_rx, ready_work_tx, @@ -423,19 +405,18 @@ pub fn spawn_reprocess_scheduler( }, TASK_NAME, ); - - work_reprocessing_tx + Ok(()) } -impl ReprocessQueue { - fn handle_message(&mut self, msg: InboundEvent, slot_clock: &T::SlotClock, log: &Logger) { +impl ReprocessQueue { + fn handle_message(&mut self, msg: InboundEvent, slot_clock: &S, log: &Logger) { use ReprocessQueueMessage::*; match msg { // Some block has been indicated as "early" and should be processed when the // appropriate slot arrives. InboundEvent::Msg(EarlyBlock(early_block)) => { - let block_slot = early_block.block.block.slot(); - let block_root = early_block.block.block_root; + let block_slot = early_block.beacon_block_slot; + let block_root = early_block.beacon_block_root; // Don't add the same block to the queue twice. This prevents DoS attacks. if self.queued_gossip_block_roots.contains(&block_root) { @@ -494,7 +475,7 @@ impl ReprocessQueue { // for the same block hash is being imported. We wait for `QUEUED_RPC_BLOCK_DELAY` // and then send the rpc block back for processing assuming the gossip import // has completed by then. - InboundEvent::Msg(RpcBlock(mut rpc_block)) => { + InboundEvent::Msg(RpcBlock(rpc_block)) => { // Check to ensure this won't over-fill the queue. if self.rpc_block_delay_queue.len() >= MAXIMUM_QUEUED_BLOCKS { if self.rpc_block_debounce.elapsed() { @@ -507,10 +488,11 @@ impl ReprocessQueue { } // Return the block to the beacon processor signalling to // ignore processing for this block - rpc_block.should_process = false; if self .ready_work_tx - .try_send(ReadyWork::RpcBlock(rpc_block)) + .try_send(ReadyWork::IgnoredRpcBlock(IgnoredRpcBlock { + process_fn: rpc_block.ignore_fn, + })) .is_err() { error!( @@ -529,7 +511,7 @@ impl ReprocessQueue { debug!( log, "Sending rpc block for reprocessing"; - "block_root" => %queued_rpc_block.block.canonical_root() + "block_root" => %queued_rpc_block.beacon_block_root ); if self .ready_work_tx @@ -767,7 +749,7 @@ impl ReprocessQueue { } // A block that was queued for later processing is now ready to be processed. InboundEvent::ReadyGossipBlock(ready_block) => { - let block_root = ready_block.block.block_root; + let block_root = ready_block.beacon_block_root; if !self.queued_gossip_block_roots.remove(&block_root) { // Log an error to alert that we've made a bad assumption about how this @@ -885,18 +867,28 @@ impl ReprocessQueue { "millis_from_slot_start" => millis_from_slot_start ); - if self + match self .ready_work_tx - .try_send(ReadyWork::BackfillSync(queued_backfill_batch.clone())) - .is_err() + .try_send(ReadyWork::BackfillSync(queued_backfill_batch)) { - error!( + // The message was sent successfully. + Ok(()) => (), + // The message was not sent, recover it from the returned `Err`. + Err(mpsc::error::TrySendError::Full(ReadyWork::BackfillSync(batch))) + | Err(mpsc::error::TrySendError::Closed(ReadyWork::BackfillSync(batch))) => { + error!( + log, + "Failed to send scheduled backfill work"; + "info" => "sending work back to queue" + ); + self.queued_backfill_batches.insert(0, batch) + } + // The message was not sent and we didn't get the correct + // return result. This is a logic error. + _ => crit!( log, - "Failed to send scheduled backfill work"; - "info" => "sending work back to queue" - ); - self.queued_backfill_batches - .insert(0, queued_backfill_batch); + "Unexpected return from try_send error"; + ), } } } @@ -927,7 +919,7 @@ impl ReprocessQueue { // only recompute the `next_backfill_batch_event` if there are backfill batches in the queue if !self.queued_backfill_batches.is_empty() { self.next_backfill_batch_event = Some(Box::pin(tokio::time::sleep( - ReprocessQueue::::duration_until_next_backfill_batch_event(&self.slot_clock), + ReprocessQueue::::duration_until_next_backfill_batch_event(&self.slot_clock), ))); } else { self.next_backfill_batch_event = None @@ -936,7 +928,7 @@ impl ReprocessQueue { /// Returns duration until the next scheduled processing time. The schedule ensure that backfill /// processing is done in windows of time that aren't critical - fn duration_until_next_backfill_batch_event(slot_clock: &T::SlotClock) -> Duration { + fn duration_until_next_backfill_batch_event(slot_clock: &S) -> Duration { let slot_duration = slot_clock.slot_duration(); slot_clock .millis_from_current_slot_start() @@ -966,16 +958,9 @@ impl ReprocessQueue { #[cfg(test)] mod tests { use super::*; - use beacon_chain::builder::Witness; - use beacon_chain::eth1_chain::CachingEth1Backend; use slot_clock::TestingSlotClock; - use store::MemoryStore; - use types::MainnetEthSpec as E; use types::Slot; - type TestBeaconChainType = - Witness, E, MemoryStore, MemoryStore>; - #[test] fn backfill_processing_schedule_calculation() { let slot_duration = Duration::from_secs(12); @@ -988,7 +973,7 @@ mod tests { for &event_duration_from_slot_start in event_times.iter() { let duration_to_next_event = - ReprocessQueue::::duration_until_next_backfill_batch_event( + ReprocessQueue::::duration_until_next_backfill_batch_event( &slot_clock, ); @@ -1005,7 +990,7 @@ mod tests { // check for next event beyond the current slot let duration_to_next_slot = slot_clock.duration_to_next_slot().unwrap(); let duration_to_next_event = - ReprocessQueue::::duration_until_next_backfill_batch_event( + ReprocessQueue::::duration_until_next_backfill_batch_event( &slot_clock, ); assert_eq!( diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index b79fc5e40..21b9b8413 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "builder_client" version = "0.1.0" -edition = "2021" +edition = { workspace = true } authors = ["Sean Anderson "] [dependencies] -reqwest = { version = "0.11.0", features = ["json","stream"] } -sensitive_url = { path = "../../common/sensitive_url" } -eth2 = { path = "../../common/eth2" } -serde = { version = "1.0.116", features = ["derive"] } -serde_json = "1.0.58" -lighthouse_version = { path = "../../common/lighthouse_version" } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +eth2 = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +lighthouse_version = { workspace = true } diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 255c2fdd1..c78f686d0 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -72,7 +72,7 @@ impl BuilderHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP GET request, returning the `Response` for further processing. @@ -85,7 +85,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.send().await.map_err(Error::Reqwest)?; + let response = builder.send().await.map_err(Error::from)?; ok_or_error(response).await } @@ -114,7 +114,7 @@ impl BuilderHttpClient { if let Some(timeout) = timeout { builder = builder.timeout(timeout); } - let response = builder.json(body).send().await.map_err(Error::Reqwest)?; + let response = builder.json(body).send().await.map_err(Error::from)?; ok_or_error(response).await } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index d39bb2e3e..b60748e30 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -2,44 +2,46 @@ name = "client" version = "0.2.0" authors = ["Sigma Prime "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -serde_yaml = "0.8.13" -state_processing = { path = "../../consensus/state_processing" } -operation_pool = { path = "../operation_pool" } -tokio = "1.14.0" +serde_yaml = { workspace = true } +state_processing = { workspace = true } +operation_pool = { workspace = true } +tokio = { workspace = true } [dependencies] -beacon_chain = { path = "../beacon_chain" } -store = { path = "../store" } -network = { path = "../network" } +beacon_chain = { workspace = true } +store = { workspace = true } +network = { workspace = true } timer = { path = "../timer" } -lighthouse_network = { path = "../lighthouse_network" } -logging = { path = "../../common/logging" } -parking_lot = "0.12.0" -types = { path = "../../consensus/types" } -eth2_config = { path = "../../common/eth2_config" } -slot_clock = { path = "../../common/slot_clock" } -serde = "1.0.116" +lighthouse_network = { workspace = true } +logging = { workspace = true } +parking_lot = { workspace = true } +types = { workspace = true } +eth2_config = { workspace = true } +slot_clock = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -error-chain = "0.12.4" -slog = { version = "2.5.2", features = ["max_level_trace"] } -tokio = "1.14.0" -dirs = "3.0.1" -eth1 = { path = "../eth1" } -eth2 = { path = "../../common/eth2" } -sensitive_url = { path = "../../common/sensitive_url" } -genesis = { path = "../genesis" } -task_executor = { path = "../../common/task_executor" } -environment = { path = "../../lighthouse/environment" } -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } +error-chain = { workspace = true } +slog = { workspace = true } +tokio = { workspace = true } +dirs = { workspace = true } +eth1 = { workspace = true } +eth2 = { workspace = true } +sensitive_url = { workspace = true } +genesis = { workspace = true } +task_executor = { workspace = true } +environment = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } time = "0.3.5" -directory = {path = "../../common/directory"} -http_api = { path = "../http_api" } +directory = { workspace = true } +http_api = { workspace = true } http_metrics = { path = "../http_metrics" } -slasher = { path = "../../slasher", default-features = false } +slasher = { workspace = true } slasher_service = { path = "../../slasher/service" } -monitoring_api = {path = "../../common/monitoring_api"} -execution_layer = { path = "../execution_layer" } +monitoring_api = { workspace = true } +execution_layer = { workspace = true } +beacon_processor = { workspace = true } +num_cpus = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 215244b9b..50e78aa45 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -11,8 +11,10 @@ use beacon_chain::{ slot_clock::{SlotClock, SystemTimeSlotClock}, state_advance_timer::spawn_state_advance_timer, store::{HotColdDB, ItemStore, LevelDB, StoreConfig}, - BeaconChain, BeaconChainTypes, Eth1ChainBackend, ServerSentEventHandler, + BeaconChain, BeaconChainTypes, Eth1ChainBackend, MigratorConfig, ServerSentEventHandler, }; +use beacon_processor::BeaconProcessorConfig; +use beacon_processor::{BeaconProcessor, BeaconProcessorChannels}; use environment::RuntimeContext; use eth1::{Config as Eth1Config, Service as Eth1Service}; use eth2::{ @@ -71,6 +73,8 @@ pub struct ClientBuilder { http_api_config: http_api::Config, http_metrics_config: http_metrics::Config, slasher: Option>>, + beacon_processor_config: Option, + beacon_processor_channels: Option>, eth_spec_instance: T::EthSpec, } @@ -104,6 +108,8 @@ where http_metrics_config: <_>::default(), slasher: None, eth_spec_instance, + beacon_processor_config: None, + beacon_processor_channels: None, } } @@ -119,6 +125,12 @@ where self } + pub fn beacon_processor(mut self, config: BeaconProcessorConfig) -> Self { + self.beacon_processor_channels = Some(BeaconProcessorChannels::new(&config)); + self.beacon_processor_config = Some(config); + self + } + pub fn slasher(mut self, slasher: Arc>) -> Self { self.slasher = Some(slasher); self @@ -142,14 +154,18 @@ where let runtime_context = runtime_context.ok_or("beacon_chain_start_method requires a runtime context")?; let context = runtime_context.service_context("beacon".into()); + let log = context.log(); let spec = chain_spec.ok_or("beacon_chain_start_method requires a chain spec")?; let event_handler = if self.http_api_config.enabled { - Some(ServerSentEventHandler::new(context.log().clone())) + Some(ServerSentEventHandler::new( + context.log().clone(), + self.http_api_config.sse_capacity_multiplier, + )) } else { None }; - let execution_layer = if let Some(config) = config.execution_layer { + let execution_layer = if let Some(config) = config.execution_layer.clone() { let context = runtime_context.service_context("exec".into()); let execution_layer = ExecutionLayer::from_config( config, @@ -167,6 +183,9 @@ where .store(store) .task_executor(context.executor.clone()) .custom_spec(spec.clone()) + .store_migrator_config( + MigratorConfig::default().epochs_per_migration(chain_config.epochs_per_migration), + ) .chain_config(chain_config) .graffiti(graffiti) .event_handler(event_handler) @@ -231,23 +250,19 @@ where )?; builder.genesis_state(genesis_state).map(|v| (v, None))? } - ClientGenesis::SszBytes { - genesis_state_bytes, - } => { + ClientGenesis::GenesisState => { info!( context.log(), "Starting from known genesis state"; ); - let genesis_state = BeaconState::from_ssz_bytes(&genesis_state_bytes, &spec) - .map_err(|e| format!("Unable to parse genesis state SSZ: {:?}", e))?; + let genesis_state = genesis_state(&runtime_context, &config, log).await?; builder.genesis_state(genesis_state).map(|v| (v, None))? } ClientGenesis::WeakSubjSszBytes { anchor_state_bytes, anchor_block_bytes, - genesis_state_bytes, } => { info!(context.log(), "Starting checkpoint sync"); if config.chain.genesis_backfill { @@ -261,17 +276,13 @@ 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 genesis_state = BeaconState::from_ssz_bytes(&genesis_state_bytes, &spec) - .map_err(|e| format!("Unable to parse genesis state SSZ: {:?}", e))?; + let genesis_state = genesis_state(&runtime_context, &config, log).await?; builder .weak_subjectivity_state(anchor_state, anchor_block, genesis_state) .map(|v| (v, None))? } - ClientGenesis::CheckpointSyncUrl { - genesis_state_bytes, - url, - } => { + ClientGenesis::CheckpointSyncUrl { url } => { info!( context.log(), "Starting checkpoint sync"; @@ -290,7 +301,6 @@ where config.chain.checkpoint_sync_url_timeout, )), ); - let slots_per_epoch = TEthSpec::slots_per_epoch(); let deposit_snapshot = if config.sync_eth1_chain { // We want to fetch deposit snapshot before fetching the finalized beacon state to @@ -337,10 +347,23 @@ where None }; - debug!(context.log(), "Downloading finalized block"); - // Find a suitable finalized block on an epoch boundary. - let mut block = remote - .get_beacon_blocks_ssz::(BlockId::Finalized, &spec) + debug!( + context.log(), + "Downloading finalized state"; + ); + let state = remote + .get_debug_beacon_states_ssz::(StateId::Finalized, &spec) + .await + .map_err(|e| format!("Error loading checkpoint state from remote: {:?}", e))? + .ok_or_else(|| "Checkpoint state missing from remote".to_string())?; + + debug!(context.log(), "Downloaded finalized state"; "slot" => ?state.slot()); + + let finalized_block_slot = state.latest_block_header().slot; + + debug!(context.log(), "Downloading finalized block"; "block_slot" => ?finalized_block_slot); + let block = remote + .get_beacon_blocks_ssz::(BlockId::Slot(finalized_block_slot), &spec) .await .map_err(|e| match e { ApiError::InvalidSsz(e) => format!( @@ -354,65 +377,14 @@ where debug!(context.log(), "Downloaded finalized block"); - let mut block_slot = block.slot(); - - while block.slot() % slots_per_epoch != 0 { - block_slot = (block_slot / slots_per_epoch - 1) * slots_per_epoch; - - debug!( - context.log(), - "Searching for aligned checkpoint block"; - "block_slot" => block_slot - ); - - if let Some(found_block) = remote - .get_beacon_blocks_ssz::(BlockId::Slot(block_slot), &spec) - .await - .map_err(|e| { - format!("Error fetching block at slot {}: {:?}", block_slot, e) - })? - { - block = found_block; - } - } - - debug!( - context.log(), - "Downloaded aligned finalized block"; - "block_root" => ?block.canonical_root(), - "block_slot" => block.slot(), - ); - - let state_root = block.state_root(); - debug!( - context.log(), - "Downloading finalized state"; - "state_root" => ?state_root - ); - let state = remote - .get_debug_beacon_states_ssz::(StateId::Root(state_root), &spec) - .await - .map_err(|e| { - format!( - "Error loading checkpoint state from remote {:?}: {:?}", - state_root, e - ) - })? - .ok_or_else(|| { - format!("Checkpoint state missing from remote: {:?}", state_root) - })?; - - debug!(context.log(), "Downloaded finalized state"); - - let genesis_state = BeaconState::from_ssz_bytes(&genesis_state_bytes, &spec) - .map_err(|e| format!("Unable to parse genesis state SSZ: {:?}", e))?; + let genesis_state = genesis_state(&runtime_context, &config, log).await?; info!( context.log(), "Loaded checkpoint block and state"; - "slot" => block.slot(), + "block_slot" => block.slot(), + "state_slot" => state.slot(), "block_root" => ?block.canonical_root(), - "state_root" => ?state_root, ); let service = @@ -476,6 +448,7 @@ where chain: None, network_senders: None, network_globals: None, + beacon_processor_send: None, eth1_service: Some(genesis_service.eth1_service.clone()), log: context.log().clone(), sse_logging_components: runtime_context.sse_logging_components.clone(), @@ -553,6 +526,10 @@ where .as_ref() .ok_or("network requires a runtime_context")? .clone(); + let beacon_processor_channels = self + .beacon_processor_channels + .as_ref() + .ok_or("network requires beacon_processor_channels")?; // If gossipsub metrics are required we build a registry to record them let mut gossipsub_registry = if config.metrics_enabled { @@ -568,6 +545,8 @@ where gossipsub_registry .as_mut() .map(|registry| registry.sub_registry_with_prefix("gossipsub")), + beacon_processor_channels.beacon_processor_tx.clone(), + beacon_processor_channels.work_reprocessing_tx.clone(), ) .await .map_err(|e| format!("Failed to start network: {:?}", e))?; @@ -690,6 +669,14 @@ where .runtime_context .as_ref() .ok_or("build requires a runtime context")?; + let beacon_processor_channels = self + .beacon_processor_channels + .take() + .ok_or("build requires beacon_processor_channels")?; + let beacon_processor_config = self + .beacon_processor_config + .take() + .ok_or("build requires a beacon_processor_config")?; let log = runtime_context.log().clone(); let http_api_listen_addr = if self.http_api_config.enabled { @@ -699,6 +686,7 @@ where 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()), sse_logging_components: runtime_context.sse_logging_components.clone(), log: log.clone(), }); @@ -742,7 +730,7 @@ where runtime_context .executor - .spawn_without_exit(async move { server.await }, "http-metrics"); + .spawn_without_exit(server, "http-metrics"); Some(listen_addr) } else { @@ -755,6 +743,25 @@ where } if let Some(beacon_chain) = self.beacon_chain.as_ref() { + if let Some(network_globals) = &self.network_globals { + let beacon_processor_context = runtime_context.service_context("bproc".into()); + BeaconProcessor { + network_globals: network_globals.clone(), + executor: beacon_processor_context.executor.clone(), + current_workers: 0, + config: beacon_processor_config, + log: beacon_processor_context.log().clone(), + } + .spawn_manager( + beacon_processor_channels.beacon_processor_rx, + beacon_processor_channels.work_reprocessing_tx, + beacon_processor_channels.work_reprocessing_rx, + None, + beacon_chain.slot_clock.clone(), + beacon_chain.spec.maximum_gossip_clock_disparity(), + )?; + } + let state_advance_context = runtime_context.service_context("state_advance".into()); let state_advance_log = state_advance_context.log().clone(); spawn_state_advance_timer( @@ -807,9 +814,6 @@ where execution_layer.spawn_clean_proposer_caches_routine::( beacon_chain.slot_clock.clone(), ); - - // Spawns a routine that polls the `exchange_transition_configuration` endpoint. - execution_layer.spawn_transition_configuration_poll(beacon_chain.spec.clone()); } // Spawn a service to publish BLS to execution changes at the Capella fork. @@ -1077,3 +1081,23 @@ where Ok(self) } } + +/// Obtain the genesis state from the `eth2_network_config` in `context`. +async fn genesis_state( + context: &RuntimeContext, + config: &ClientConfig, + log: &Logger, +) -> Result, String> { + let eth2_network_config = context + .eth2_network_config + .as_ref() + .ok_or("An eth2_network_config is required to obtain the genesis state")?; + eth2_network_config + .genesis_state::( + config.genesis_state_url.as_deref(), + config.genesis_state_url_timeout, + log, + ) + .await? + .ok_or_else(|| "Genesis state is unknown".to_string()) +} diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 95a00b374..adaf02798 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,4 +1,5 @@ use beacon_chain::validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD; +use beacon_processor::BeaconProcessorConfig; use directory::DEFAULT_ROOT_DIR; use environment::LoggerConfig; use network::NetworkConfig; @@ -6,6 +7,7 @@ use sensitive_url::SensitiveUrl; use serde_derive::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use std::time::Duration; use types::{Graffiti, PublicKeyBytes}; /// Default directory name for the freezer database under the top-level data dir. const DEFAULT_FREEZER_DB_DIR: &str = "freezer_db"; @@ -24,18 +26,13 @@ pub enum ClientGenesis { /// contract. #[default] DepositContract, - /// Loads the genesis state from SSZ-encoded `BeaconState` bytes. - /// - /// We include the bytes instead of the `BeaconState` because the `EthSpec` type - /// parameter would be very annoying. - SszBytes { genesis_state_bytes: Vec }, + /// Loads the genesis state from the genesis state in the `Eth2NetworkConfig`. + GenesisState, WeakSubjSszBytes { - genesis_state_bytes: Vec, anchor_state_bytes: Vec, anchor_block_bytes: Vec, }, CheckpointSyncUrl { - genesis_state_bytes: Vec, url: SensitiveUrl, }, } @@ -79,7 +76,9 @@ pub struct Config { pub monitoring_api: Option, pub slasher: Option, pub logger_config: LoggerConfig, - pub always_prefer_builder_payload: bool, + pub beacon_processor: BeaconProcessorConfig, + pub genesis_state_url: Option, + pub genesis_state_url_timeout: Duration, } impl Default for Config { @@ -106,7 +105,10 @@ impl Default for Config { validator_monitor_pubkeys: vec![], validator_monitor_individual_tracking_threshold: DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, logger_config: LoggerConfig::default(), - always_prefer_builder_payload: false, + beacon_processor: <_>::default(), + genesis_state_url: <_>::default(), + // This default value should always be overwritten by the CLI default value. + genesis_state_url_timeout: Duration::from_secs(60), } } } diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index 584a0d736..399aa0651 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -46,20 +46,6 @@ impl Client { self.http_metrics_listen_addr } - /// Returns the ipv4 port of the client's libp2p stack, if it was started. - pub fn libp2p_listen_ipv4_port(&self) -> Option { - self.network_globals - .as_ref() - .and_then(|n| n.listen_port_tcp4()) - } - - /// Returns the ipv6 port of the client's libp2p stack, if it was started. - pub fn libp2p_listen_ipv6_port(&self) -> Option { - self.network_globals - .as_ref() - .and_then(|n| n.listen_port_tcp6()) - } - /// Returns the list of libp2p addresses the client is listening to. pub fn libp2p_listen_addresses(&self) -> Option> { self.network_globals.as_ref().map(|n| n.listen_multiaddrs()) diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 1ff469fe3..2c7738e8f 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -1,7 +1,7 @@ use crate::metrics; use beacon_chain::{ capella_readiness::CapellaReadiness, - merge_readiness::{MergeConfig, MergeReadiness}, + merge_readiness::{GenesisExecutionPayloadStatus, MergeConfig, MergeReadiness}, BeaconChain, BeaconChainTypes, ExecutionStatus, }; use lighthouse_network::{types::SyncState, NetworkGlobals}; @@ -62,6 +62,9 @@ pub fn spawn_notifier( "wait_time" => estimated_time_pretty(Some(next_slot.as_secs() as f64)), ); eth1_logging(&beacon_chain, &log); + merge_readiness_logging(Slot::new(0), &beacon_chain, &log).await; + capella_readiness_logging(Slot::new(0), &beacon_chain, &log).await; + genesis_execution_payload_logging(&beacon_chain, &log).await; sleep(slot_duration).await; } _ => break, @@ -365,7 +368,7 @@ async fn merge_readiness_logging( return; } - match beacon_chain.check_merge_readiness().await { + match beacon_chain.check_merge_readiness(current_slot).await { MergeReadiness::Ready { config, current_difficulty, @@ -404,14 +407,6 @@ async fn merge_readiness_logging( "config" => ?other ), }, - readiness @ MergeReadiness::ExchangeTransitionConfigurationFailed { error: _ } => { - error!( - log, - "Not ready for merge"; - "info" => %readiness, - "hint" => "try updating Lighthouse and/or the execution layer", - ) - } readiness @ MergeReadiness::NotSynced => warn!( log, "Not ready for merge"; @@ -484,6 +479,79 @@ async fn capella_readiness_logging( } } +async fn genesis_execution_payload_logging( + beacon_chain: &BeaconChain, + log: &Logger, +) { + match beacon_chain + .check_genesis_execution_payload_is_correct() + .await + { + Ok(GenesisExecutionPayloadStatus::Correct(block_hash)) => { + info!( + log, + "Execution enabled from genesis"; + "genesis_payload_block_hash" => ?block_hash, + ); + } + Ok(GenesisExecutionPayloadStatus::BlockHashMismatch { got, expected }) => { + error!( + log, + "Genesis payload block hash mismatch"; + "info" => "genesis is misconfigured and likely to fail", + "consensus_node_block_hash" => ?expected, + "execution_node_block_hash" => ?got, + ); + } + Ok(GenesisExecutionPayloadStatus::TransactionsRootMismatch { got, expected }) => { + error!( + log, + "Genesis payload transactions root mismatch"; + "info" => "genesis is misconfigured and likely to fail", + "consensus_node_transactions_root" => ?expected, + "execution_node_transactions_root" => ?got, + ); + } + Ok(GenesisExecutionPayloadStatus::WithdrawalsRootMismatch { got, expected }) => { + error!( + log, + "Genesis payload withdrawals root mismatch"; + "info" => "genesis is misconfigured and likely to fail", + "consensus_node_withdrawals_root" => ?expected, + "execution_node_withdrawals_root" => ?got, + ); + } + Ok(GenesisExecutionPayloadStatus::OtherMismatch) => { + error!( + log, + "Genesis payload header mismatch"; + "info" => "genesis is misconfigured and likely to fail", + "detail" => "see debug logs for payload headers" + ); + } + Ok(GenesisExecutionPayloadStatus::Irrelevant) => { + info!( + log, + "Execution is not enabled from genesis"; + ); + } + Ok(GenesisExecutionPayloadStatus::AlreadyHappened) => { + warn!( + log, + "Unable to check genesis which has already occurred"; + "info" => "this is probably a race condition or a bug" + ); + } + Err(e) => { + error!( + log, + "Unable to check genesis execution payload"; + "error" => ?e + ); + } + } +} + fn eth1_logging(beacon_chain: &BeaconChain, log: &Logger) { let current_slot_opt = beacon_chain.slot().ok(); diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index cc982aee0..2f716cd19 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -2,33 +2,33 @@ name = "eth1" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -eth1_test_rig = { path = "../../testing/eth1_test_rig" } -serde_yaml = "0.8.13" -sloggers = { version = "2.1.1", features = ["json"] } -environment = { path = "../../lighthouse/environment" } +eth1_test_rig = { workspace = true } +serde_yaml = { workspace = true } +sloggers = { workspace = true } +environment = { workspace = true } [dependencies] -reqwest = { version = "0.11.0", features = ["native-tls-vendored"] } -execution_layer = { path = "../execution_layer" } -futures = "0.3.7" -serde_json = "1.0.58" -serde = { version = "1.0.116", features = ["derive"] } -hex = "0.4.2" -types = { path = "../../consensus/types"} -merkle_proof = { path = "../../consensus/merkle_proof"} -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -tree_hash = "0.5.0" -parking_lot = "0.12.0" -slog = "2.5.2" -superstruct = "0.5.0" -tokio = { version = "1.14.0", features = ["full"] } -state_processing = { path = "../../consensus/state_processing" } -lighthouse_metrics = { path = "../../common/lighthouse_metrics"} -lazy_static = "1.4.0" -task_executor = { path = "../../common/task_executor" } -eth2 = { path = "../../common/eth2" } -sensitive_url = { path = "../../common/sensitive_url" } +reqwest = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +hex = { workspace = true } +types = { workspace = true } +merkle_proof = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +tree_hash = { workspace = true } +parking_lot = { workspace = true } +slog = { workspace = true } +superstruct = { workspace = true } +tokio = { workspace = true } +state_processing = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +task_executor = { workspace = true } +eth2 = { workspace = true } +sensitive_url = { workspace = true } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 3ed7ba65d..3651e3718 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -1,52 +1,56 @@ [package] name = "execution_layer" version = "0.1.0" -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -types = { path = "../../consensus/types"} -tokio = { version = "1.10.0", features = ["full"] } +types = { workspace = true } +tokio = { workspace = true } async-trait = "0.1.51" -slog = "2.5.2" -futures = "0.3.7" -sensitive_url = { path = "../../common/sensitive_url" } -reqwest = { version = "0.11.0", features = ["json","stream"] } -ethereum_serde_utils = "0.5.0" -serde_json = "1.0.58" -serde = { version = "1.0.116", features = ["derive"] } -warp = { version = "0.3.2", features = ["tls"] } +slog = { workspace = true } +futures = { workspace = true } +sensitive_url = { workspace = true } +reqwest = { workspace = true } +ethereum_serde_utils = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +warp = { workspace = true } jsonwebtoken = "8" -environment = { path = "../../lighthouse/environment" } -bytes = "1.1.0" -task_executor = { path = "../../common/task_executor" } -hex = "0.4.2" -ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" -eth2 = { path = "../../common/eth2" } -state_processing = { path = "../../consensus/state_processing" } -superstruct = "0.6.0" -lru = "0.7.1" -exit-future = "0.2.0" -tree_hash = "0.5.0" -tree_hash_derive = "0.5.0" -parking_lot = "0.12.0" -slot_clock = { path = "../../common/slot_clock" } -tempfile = "3.1.0" -rand = "0.8.5" -zeroize = { version = "1.4.2", features = ["zeroize_derive"] } -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -lazy_static = "1.4.0" -ethers-core = "1.0.2" +environment = { workspace = true } +bytes = { workspace = true } +task_executor = { workspace = true } +hex = { workspace = true } +ethereum_ssz = { workspace = true } +ssz_types = { workspace = true } +eth2 = { workspace = true } +state_processing = { workspace = true } +superstruct = { workspace = true } +lru = { workspace = true } +exit-future = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +parking_lot = { workspace = true } +slot_clock = { workspace = true } +tempfile = { workspace = true } +rand = { workspace = true } +zeroize = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +ethers-core = { workspace = true } builder_client = { path = "../builder_client" } -fork_choice = { path = "../../consensus/fork_choice" } -mev-rs = { git = "https://github.com/ralexstokes/mev-rs" } -ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus" } -ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs" } -tokio-stream = { version = "0.1.9", features = [ "sync" ] } -strum = "0.24.0" +fork_choice = { workspace = true } +mev-rs = { git = "https://github.com/ralexstokes/mev-rs", rev = "216657016d5c0889b505857c89ae42c7aa2764af" } +axum = "0.6" +hyper = "0.14" +ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "e380108" } +ssz_rs = "0.9.0" +tokio-stream = { workspace = true } +strum = { workspace = true } keccak-hash = "0.10.0" hash256-std-hasher = "0.15.2" triehash = "0.8.4" hash-db = "0.15.2" +pretty_reqwest_error = { workspace = true } +arc-swap = "1.6.0" diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index e9b7dcc17..c889fead0 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -12,12 +12,13 @@ use types::{ }; impl ExecutionLayer { - /// Verify `payload.block_hash` locally within Lighthouse. + /// Calculate the block hash of an execution block. /// - /// No remote calls to the execution client will be made, so this is quite a cheap check. - pub fn verify_payload_block_hash(&self, payload: ExecutionPayloadRef) -> Result<(), Error> { - let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); - + /// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP + /// transactions. + pub fn calculate_execution_block_hash( + payload: ExecutionPayloadRef, + ) -> (ExecutionBlockHash, Hash256) { // Calculate the transactions root. // We're currently using a deprecated Parity library for this. We should move to a // better alternative when one appears, possibly following Reth. @@ -46,7 +47,19 @@ impl ExecutionLayer { // Hash the RLP encoding of the block header. let rlp_block_header = rlp_encode_block_header(&exec_block_header); - let header_hash = ExecutionBlockHash::from_root(keccak256(&rlp_block_header)); + ( + ExecutionBlockHash::from_root(keccak256(&rlp_block_header)), + rlp_transactions_root, + ) + } + + /// Verify `payload.block_hash` locally within Lighthouse. + /// + /// No remote calls to the execution client will be made, so this is quite a cheap check. + pub fn verify_payload_block_hash(&self, payload: ExecutionPayloadRef) -> Result<(), Error> { + let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); + + let (header_hash, rlp_transactions_root) = Self::calculate_execution_block_hash(payload); if header_hash != payload.block_hash() { return Err(Error::BlockHashMismatch { diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 4d2eb565e..359dcb522 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,15 +1,15 @@ use crate::engines::ForkchoiceState; use crate::http::{ - ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, ENGINE_FORKCHOICE_UPDATED_V1, - ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, - ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, + ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, }; use eth2::types::{SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2}; pub use ethers_core::types::Transaction; use ethers_core::utils::rlp::{self, Decodable, Rlp}; use http::deposit_methods::RpcError; pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1}; +use pretty_reqwest_error::PrettyReqwestError; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -32,7 +32,7 @@ pub type PayloadId = [u8; 8]; #[derive(Debug)] pub enum Error { - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), Auth(auth::Error), BadResponse(String), RequestFailed(String), @@ -67,7 +67,7 @@ impl From for Error { ) { Error::Auth(auth::Error::InvalidToken) } else { - Error::Reqwest(e) + Error::HttpClient(e.into()) } } } @@ -449,7 +449,6 @@ pub struct EngineCapabilities { pub get_payload_bodies_by_range_v1: bool, pub get_payload_v1: bool, pub get_payload_v2: bool, - pub exchange_transition_configuration_v1: bool, } impl EngineCapabilities { @@ -479,9 +478,6 @@ impl EngineCapabilities { if self.get_payload_v2 { response.push(ENGINE_GET_PAYLOAD_V2); } - if self.exchange_transition_configuration_v1 { - response.push(ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1); - } response } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 029866d95..0ce03e602 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -46,10 +46,6 @@ pub const ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1: &str = "engine_getPayloadBodiesB pub const ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1: &str = "engine_getPayloadBodiesByRangeV1"; pub const ENGINE_GET_PAYLOAD_BODIES_TIMEOUT: Duration = Duration::from_secs(10); -pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1: &str = - "engine_exchangeTransitionConfigurationV1"; -pub const ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT: Duration = Duration::from_secs(1); - pub const ENGINE_EXCHANGE_CAPABILITIES: &str = "engine_exchangeCapabilities"; pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1); @@ -68,7 +64,6 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, - ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, ]; /// This is necessary because a user might run a capella-enabled version of @@ -83,7 +78,6 @@ pub static PRE_CAPELLA_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilit get_payload_bodies_by_range_v1: false, get_payload_v1: true, get_payload_v2: false, - exchange_transition_configuration_v1: true, }; /// Contains methods to convert arbitrary bytes to an ETH2 deposit contract object. @@ -934,24 +928,6 @@ impl HttpJsonRpc { .collect()) } - pub async fn exchange_transition_configuration_v1( - &self, - transition_configuration: TransitionConfigurationV1, - ) -> Result { - let params = json!([transition_configuration]); - - let response = self - .rpc_request( - ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1, - params, - ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1_TIMEOUT - * self.execution_timeout_multiplier, - ) - .await?; - - Ok(response) - } - pub async fn exchange_capabilities(&self) -> Result { let params = json!([LIGHTHOUSE_CAPABILITIES]); @@ -982,8 +958,6 @@ impl HttpJsonRpc { .contains(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1), get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1), get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2), - exchange_transition_configuration_v1: capabilities - .contains(ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1), }), } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 19fa91b12..32e255a16 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -5,6 +5,7 @@ //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. use crate::payload_cache::PayloadCache; +use arc_swap::ArcSwapOption; use auth::{strip_prefix, Auth, JwtKey}; use builder_client::BuilderHttpClient; pub use engine_api::EngineCapabilities; @@ -38,11 +39,11 @@ use tokio::{ }; use tokio_stream::wrappers::WatchStream; use tree_hash::TreeHash; -use types::{AbstractExecPayload, BeaconStateError, ExecPayload, Withdrawals}; +use types::{AbstractExecPayload, BeaconStateError, ExecPayload}; use types::{ - BlindedPayload, BlockType, ChainSpec, Epoch, ExecutionBlockHash, ExecutionPayload, - ExecutionPayloadCapella, ExecutionPayloadMerge, ForkName, ForkVersionedResponse, - ProposerPreparationData, PublicKeyBytes, Signature, SignedBeaconBlock, Slot, Uint256, + BlindedPayload, BlockType, ChainSpec, Epoch, ExecutionPayloadCapella, ExecutionPayloadMerge, + ForkVersionedResponse, ProposerPreparationData, PublicKeyBytes, Signature, SignedBeaconBlock, + Slot, }; mod block_hash; @@ -74,11 +75,9 @@ const EXECUTION_BLOCKS_LRU_CACHE_SIZE: usize = 128; const DEFAULT_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]; -const CONFIG_POLL_INTERVAL: Duration = Duration::from_secs(60); - /// A payload alongside some information about where it came from. pub enum ProvenancedPayload

{ - /// A good ol' fashioned farm-to-table payload from your local EE. + /// A good old fashioned farm-to-table payload from your local EE. Local(P), /// A payload from a builder (e.g. mev-boost). Builder(P), @@ -163,7 +162,7 @@ impl> BlockProposalContents { engine: Arc, - builder: Option, + builder: ArcSwapOption, execution_engine_forkchoice_lock: Mutex<()>, suggested_fee_recipient: Option

, proposer_preparation_data: Mutex>, @@ -326,25 +325,9 @@ impl ExecutionLayer { Engine::new(api, executor.clone(), &log) }; - let builder = builder_url - .map(|url| { - let builder_client = BuilderHttpClient::new(url.clone(), builder_user_agent) - .map_err(Error::Builder)?; - - info!( - log, - "Using external block builder"; - "builder_url" => ?url, - "builder_profit_threshold" => builder_profit_threshold, - "local_user_agent" => builder_client.get_user_agent(), - ); - Ok::<_, Error>(builder_client) - }) - .transpose()?; - let inner = Inner { engine: Arc::new(engine), - builder, + builder: ArcSwapOption::empty(), execution_engine_forkchoice_lock: <_>::default(), suggested_fee_recipient, proposer_preparation_data: Mutex::new(HashMap::new()), @@ -358,19 +341,45 @@ impl ExecutionLayer { last_new_payload_errored: RwLock::new(false), }; - Ok(Self { + let el = Self { inner: Arc::new(inner), - }) - } -} + }; + + if let Some(builder_url) = builder_url { + el.set_builder_url(builder_url, builder_user_agent)?; + } + + Ok(el) + } -impl ExecutionLayer { fn engine(&self) -> &Arc { &self.inner.engine } - pub fn builder(&self) -> &Option { - &self.inner.builder + pub fn builder(&self) -> Option> { + self.inner.builder.load_full() + } + + /// Set the builder URL after initialization. + /// + /// This is useful for breaking circular dependencies between mock ELs and mock builders in + /// tests. + pub fn set_builder_url( + &self, + builder_url: SensitiveUrl, + builder_user_agent: Option, + ) -> Result<(), Error> { + let builder_client = BuilderHttpClient::new(builder_url.clone(), builder_user_agent) + .map_err(Error::Builder)?; + info!( + self.log(), + "Using external block builder"; + "builder_url" => ?builder_url, + "builder_profit_threshold" => self.inner.builder_profit_threshold.as_u128(), + "local_user_agent" => builder_client.get_user_agent(), + ); + self.inner.builder.swap(Some(Arc::new(builder_client))); + Ok(()) } /// Cache a full payload, keyed on the `tree_hash_root` of the payload @@ -380,7 +389,7 @@ impl ExecutionLayer { /// Attempt to retrieve a full payload from the payload cache by the payload root pub fn get_payload_by_root(&self, root: &Hash256) -> Option> { - self.inner.payload_cache.pop(root) + self.inner.payload_cache.get(root) } pub fn executor(&self) -> &TaskExecutor { @@ -502,24 +511,6 @@ impl ExecutionLayer { self.spawn(preparation_cleaner, "exec_preparation_cleanup"); } - /// Spawns a routine that polls the `exchange_transition_configuration` endpoint. - pub fn spawn_transition_configuration_poll(&self, spec: ChainSpec) { - let routine = |el: ExecutionLayer| async move { - loop { - if let Err(e) = el.exchange_transition_configuration(&spec).await { - error!( - el.log(), - "Failed to check transition config"; - "error" => ?e - ); - } - sleep(CONFIG_POLL_INTERVAL).await; - } - }; - - self.spawn(routine, "exec_config_poll"); - } - /// Returns `true` if the execution engine is synced and reachable. pub async fn is_synced(&self) -> bool { self.engine().is_synced().await @@ -529,9 +520,9 @@ impl ExecutionLayer { /// /// This function is a wrapper over `Self::is_synced` that makes an additional /// check for the execution layer sync status. Checks if the latest block has - /// a `block_number != 0`. + /// a `block_number != 0` *if* the `current_slot` is also `> 0`. /// Returns the `Self::is_synced` response if unable to get latest block. - pub async fn is_synced_for_notifier(&self) -> bool { + 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 @@ -540,7 +531,7 @@ impl ExecutionLayer { .get_block_by_number(BlockByNumberQuery::Tag(LATEST_TAG)) .await { - if block.block_number == 0 { + if block.block_number == 0 && current_slot > 0 { return false; } } @@ -826,16 +817,23 @@ impl ExecutionLayer { let relay_value = relay.data.message.value; let local_value = *local.block_value(); - if !self.inner.always_prefer_builder_payload - && local_value >= relay_value - { - info!( - self.log(), - "Local block is more profitable than relay block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value - ); - return Ok(ProvenancedPayload::Local(local)); + if !self.inner.always_prefer_builder_payload { + if local_value >= relay_value { + info!( + self.log(), + "Local block is more profitable than relay block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + return Ok(ProvenancedPayload::Local(local)); + } else { + info!( + self.log(), + "Relay block is more profitable than local block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + } } match verify_builder_bid( @@ -851,7 +849,7 @@ impl ExecutionLayer { BlockProposalContents::Payload { payload: relay.data.message.header, block_value: relay.data.message.value, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, )), Err(reason) if !reason.payload_invalid() => { @@ -906,7 +904,7 @@ impl ExecutionLayer { BlockProposalContents::Payload { payload: relay.data.message.header, block_value: relay.data.message.value, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, )), // If the payload is valid then use it. The local EE failed @@ -915,7 +913,7 @@ impl ExecutionLayer { BlockProposalContents::Payload { payload: relay.data.message.header, block_value: relay.data.message.value, - _phantom: PhantomData::default(), + _phantom: PhantomData, }, )), Err(reason) => { @@ -1122,7 +1120,7 @@ impl ExecutionLayer { Ok(BlockProposalContents::Payload { payload: execution_payload.into(), block_value, - _phantom: PhantomData::default(), + _phantom: PhantomData, }) }) .await @@ -1311,53 +1309,6 @@ impl ExecutionLayer { .map_err(Error::EngineError) } - pub async fn exchange_transition_configuration(&self, spec: &ChainSpec) -> Result<(), Error> { - let local = TransitionConfigurationV1 { - terminal_total_difficulty: spec.terminal_total_difficulty, - terminal_block_hash: spec.terminal_block_hash, - terminal_block_number: 0, - }; - - let result = self - .engine() - .request(|engine| engine.api.exchange_transition_configuration_v1(local)) - .await; - - match result { - Ok(remote) => { - if local.terminal_total_difficulty != remote.terminal_total_difficulty - || local.terminal_block_hash != remote.terminal_block_hash - { - error!( - self.log(), - "Execution client config mismatch"; - "msg" => "ensure lighthouse and the execution client are up-to-date and \ - configured consistently", - "remote" => ?remote, - "local" => ?local, - ); - Err(Error::EngineError(Box::new(EngineError::Api { - error: ApiError::TransitionConfigurationMismatch, - }))) - } else { - debug!( - self.log(), - "Execution client config is OK"; - ); - Ok(()) - } - } - Err(e) => { - error!( - self.log(), - "Unable to get transition config"; - "error" => ?e, - ); - Err(Error::EngineError(Box::new(e))) - } - } - } - /// Returns the execution engine capabilities resulting from a call to /// engine_exchangeCapabilities. If the capabilities cache is not populated, /// or if it is populated with a cached result of age >= `age_limit`, this @@ -1654,6 +1605,17 @@ impl ExecutionLayer { } } + pub async fn get_block_by_number( + &self, + query: BlockByNumberQuery<'_>, + ) -> Result, Error> { + self.engine() + .request(|engine| async move { engine.api.get_block_by_number(query).await }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } + pub async fn get_payload_by_hash_legacy( &self, hash: ExecutionBlockHash, @@ -2011,6 +1973,22 @@ async fn timed_future, T>(metric: &str, future: F) -> (T, (result, duration) } +fn noop( + _: &ExecutionLayer, + _: ExecutionPayloadRef, +) -> Option> { + None +} + +#[cfg(test)] +/// Returns the duration since the unix epoch. +fn timestamp_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_secs() +} + #[cfg(test)] mod test { use super::*; @@ -2157,19 +2135,3 @@ mod test { .await; } } - -fn noop( - _: &ExecutionLayer, - _: ExecutionPayloadRef, -) -> Option> { - None -} - -#[cfg(test)] -/// Returns the duration since the unix epoch. -fn timestamp_now() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs() -} diff --git a/beacon_node/execution_layer/src/payload_cache.rs b/beacon_node/execution_layer/src/payload_cache.rs index 60a8f2a95..1722edff4 100644 --- a/beacon_node/execution_layer/src/payload_cache.rs +++ b/beacon_node/execution_layer/src/payload_cache.rs @@ -30,4 +30,8 @@ impl PayloadCache { pub fn pop(&self, root: &Hash256) -> Option> { self.payloads.lock().pop(&PayloadCacheId(*root)) } + + pub fn get(&self, hash: &Hash256) -> Option> { + self.payloads.lock().get(&PayloadCacheId(*hash)).cloned() + } } 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 79468b211..62cab5ad2 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -357,15 +357,6 @@ pub async fn handle_rpc( Ok(serde_json::to_value(response).unwrap()) } - ENGINE_EXCHANGE_TRANSITION_CONFIGURATION_V1 => { - let block_generator = ctx.execution_block_generator.read(); - let transition_config: TransitionConfigurationV1 = TransitionConfigurationV1 { - terminal_total_difficulty: block_generator.terminal_total_difficulty, - terminal_block_hash: block_generator.terminal_block_hash, - terminal_block_number: block_generator.terminal_block_number, - }; - Ok(serde_json::to_value(transition_config).unwrap()) - } ENGINE_EXCHANGE_CAPABILITIES => { let engine_capabilities = ctx.engine_capabilities.read(); Ok(serde_json::to_value(engine_capabilities.to_response()).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 668d1fb3b..c9dd196fa 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -11,11 +11,17 @@ use ethereum_consensus::{ }; use fork_choice::ForkchoiceUpdateParameters; use mev_rs::{ - bellatrix::{BuilderBid as BuilderBidBellatrix, SignedBuilderBid as SignedBuilderBidBellatrix}, - capella::{BuilderBid as BuilderBidCapella, SignedBuilderBid as SignedBuilderBidCapella}, - sign_builder_message, verify_signed_builder_message, BidRequest, BlindedBlockProviderError, - BlindedBlockProviderServer, BuilderBid, ExecutionPayload as ServerPayload, - SignedBlindedBeaconBlock, SignedBuilderBid, SignedValidatorRegistration, + blinded_block_provider::Server as BlindedBlockProviderServer, + signing::{sign_builder_message, verify_signed_builder_message}, + types::{ + bellatrix::{ + BuilderBid as BuilderBidBellatrix, SignedBuilderBid as SignedBuilderBidBellatrix, + }, + capella::{BuilderBid as BuilderBidCapella, SignedBuilderBid as SignedBuilderBidCapella}, + BidRequest, BuilderBid, ExecutionPayload as ServerPayload, SignedBlindedBeaconBlock, + SignedBuilderBid, SignedValidatorRegistration, + }, + Error as MevError, }; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; @@ -34,6 +40,11 @@ use types::{ Uint256, }; +pub type MockBuilderServer = axum::Server< + hyper::server::conn::AddrIncoming, + axum::routing::IntoMakeService, +>; + #[derive(Clone)] pub enum Operation { FeeRecipient(Address), @@ -47,7 +58,7 @@ pub enum Operation { } impl Operation { - fn apply(self, bid: &mut B) -> Result<(), BlindedBlockProviderError> { + fn apply(self, bid: &mut B) -> Result<(), MevError> { match self { Operation::FeeRecipient(fee_recipient) => { *bid.fee_recipient_mut() = to_ssz_rs(&fee_recipient)? @@ -73,7 +84,7 @@ pub trait BidStuff { fn prev_randao_mut(&mut self) -> &mut Hash32; fn block_number_mut(&mut self) -> &mut u64; fn timestamp_mut(&mut self) -> &mut u64; - fn withdrawals_root_mut(&mut self) -> Result<&mut Root, BlindedBlockProviderError>; + fn withdrawals_root_mut(&mut self) -> Result<&mut Root, MevError>; fn sign_builder_message( &mut self, @@ -134,11 +145,9 @@ impl BidStuff for BuilderBid { } } - fn withdrawals_root_mut(&mut self) -> Result<&mut Root, BlindedBlockProviderError> { + fn withdrawals_root_mut(&mut self) -> Result<&mut Root, MevError> { match self { - Self::Bellatrix(_) => Err(BlindedBlockProviderError::Custom( - "withdrawals_root called on bellatrix bid".to_string(), - )), + Self::Bellatrix(_) => Err(MevError::InvalidFork), Self::Capella(bid) => Ok(&mut bid.header.withdrawals_root), } } @@ -166,19 +175,25 @@ impl BidStuff for BuilderBid { } } -pub struct TestingBuilder { - server: BlindedBlockProviderServer>, - pub builder: MockBuilder, +#[derive(Clone)] +pub struct MockBuilder { + el: ExecutionLayer, + beacon_client: BeaconNodeHttpClient, + spec: ChainSpec, + context: Arc, + val_registration_cache: Arc>>, + builder_sk: SecretKey, + operations: Arc>>, + invalidate_signatures: Arc>, } -impl TestingBuilder { - pub fn new( +impl MockBuilder { + pub fn new_for_testing( mock_el_url: SensitiveUrl, - builder_url: SensitiveUrl, beacon_url: SensitiveUrl, spec: ChainSpec, executor: TaskExecutor, - ) -> Self { + ) -> (Self, MockBuilderServer) { let file = NamedTempFile::new().unwrap(); let path = file.path().into(); std::fs::write(&path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); @@ -207,39 +222,13 @@ impl TestingBuilder { spec, context, ); - let port = builder_url.full.port().unwrap(); - let host: Ipv4Addr = builder_url - .full - .host_str() - .unwrap() - .to_string() - .parse() - .unwrap(); - let server = BlindedBlockProviderServer::new(host, port, builder.clone()); - Self { server, builder } + let host: Ipv4Addr = Ipv4Addr::LOCALHOST; + let port = 0; + let provider = BlindedBlockProviderServer::new(host, port, builder.clone()); + let server = provider.serve(); + (builder, server) } - pub async fn run(&self) { - let server = self.server.serve(); - if let Err(err) = server.await { - println!("error while listening for incoming: {err}") - } - } -} - -#[derive(Clone)] -pub struct MockBuilder { - el: ExecutionLayer, - beacon_client: BeaconNodeHttpClient, - spec: ChainSpec, - context: Arc, - val_registration_cache: Arc>>, - builder_sk: SecretKey, - operations: Arc>>, - invalidate_signatures: Arc>, -} - -impl MockBuilder { pub fn new( el: ExecutionLayer, beacon_client: BeaconNodeHttpClient, @@ -274,7 +263,7 @@ impl MockBuilder { *self.invalidate_signatures.write() = false; } - fn apply_operations(&self, bid: &mut B) -> Result<(), BlindedBlockProviderError> { + fn apply_operations(&self, bid: &mut B) -> Result<(), MevError> { let mut guard = self.operations.write(); while let Some(op) = guard.pop() { op.apply(bid)?; @@ -288,7 +277,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { async fn register_validators( &self, registrations: &mut [SignedValidatorRegistration], - ) -> Result<(), BlindedBlockProviderError> { + ) -> Result<(), MevError> { for registration in registrations { let pubkey = registration.message.public_key.clone(); let message = &mut registration.message; @@ -307,10 +296,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { Ok(()) } - async fn fetch_best_bid( - &self, - bid_request: &BidRequest, - ) -> Result { + async fn fetch_best_bid(&self, bid_request: &BidRequest) -> Result { let slot = Slot::new(bid_request.slot); let fork = self.spec.fork_name_at_slot::(slot); let signed_cached_data = self @@ -336,7 +322,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { .map_err(convert_err)? .block_hash(); if head_execution_hash != from_ssz_rs(&bid_request.parent_hash)? { - return Err(BlindedBlockProviderError::Custom(format!( + return Err(custom_err(format!( "head mismatch: {} {}", head_execution_hash, bid_request.parent_hash ))); @@ -396,7 +382,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { .get_debug_beacon_states(StateId::Head) .await .map_err(convert_err)? - .ok_or_else(|| BlindedBlockProviderError::Custom("missing head state".to_string()))? + .ok_or_else(|| custom_err("missing head state".to_string()))? .data; let prev_randao = head_state .get_randao_mix(head_state.current_epoch()) @@ -409,10 +395,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { PayloadAttributes::new(timestamp, *prev_randao, fee_recipient, Some(vec![])) } ForkName::Base | ForkName::Altair => { - return Err(BlindedBlockProviderError::Custom(format!( - "Unsupported fork: {}", - fork - ))); + return Err(MevError::InvalidFork); } }; @@ -452,12 +435,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { value: to_ssz_rs(&Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI))?, public_key: self.builder_sk.public_key(), }), - ForkName::Base | ForkName::Altair => { - return Err(BlindedBlockProviderError::Custom(format!( - "Unsupported fork: {}", - fork - ))) - } + ForkName::Base | ForkName::Altair => return Err(MevError::InvalidFork), }; *message.gas_limit_mut() = cached_data.gas_limit; @@ -475,7 +453,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { async fn open_bid( &self, signed_block: &mut SignedBlindedBeaconBlock, - ) -> Result { + ) -> Result { let node = match signed_block { SignedBlindedBeaconBlock::Bellatrix(block) => { block.message.body.execution_payload_header.hash_tree_root() @@ -496,9 +474,7 @@ impl mev_rs::BlindedBlockProvider for MockBuilder { } } -pub fn from_ssz_rs( - ssz_rs_data: &T, -) -> Result { +pub fn from_ssz_rs(ssz_rs_data: &T) -> Result { U::from_ssz_bytes( ssz_rs::serialize(ssz_rs_data) .map_err(convert_err)? @@ -507,12 +483,17 @@ pub fn from_ssz_rs( .map_err(convert_err) } -pub fn to_ssz_rs( - ssz_data: &T, -) -> Result { +pub fn to_ssz_rs(ssz_data: &T) -> Result { ssz_rs::deserialize::(&ssz_data.as_ssz_bytes()).map_err(convert_err) } -fn convert_err(e: E) -> BlindedBlockProviderError { - BlindedBlockProviderError::Custom(format!("{e:?}")) +fn convert_err(e: E) -> MevError { + custom_err(format!("{e:?}")) +} + +// This is a bit of a hack since the `Custom` variant was removed from `mev_rs::Error`. +fn custom_err(s: String) -> MevError { + MevError::Consensus(ethereum_consensus::state_transition::Error::Io( + std::io::Error::new(std::io::ErrorKind::Other, s), + )) } 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 2b512d8b1..05f6c137e 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 @@ -31,7 +31,6 @@ impl MockExecutionLayer { None, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec, - None, ) } @@ -43,7 +42,6 @@ impl MockExecutionLayer { builder_threshold: Option, jwt_key: Option, spec: ChainSpec, - builder_url: Option, ) -> Self { let handle = executor.handle().unwrap(); @@ -65,7 +63,6 @@ impl MockExecutionLayer { let config = Config { execution_endpoints: vec![url], - builder_url, secret_files: vec![path], suggested_fee_recipient: Some(Address::repeat_byte(42)), builder_profit_threshold: builder_threshold.unwrap_or(DEFAULT_BUILDER_THRESHOLD_WEI), diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index a8e7bab27..059c0275b 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -25,7 +25,7 @@ use warp::{http::StatusCode, Filter, Rejection}; use crate::EngineCapabilities; pub use execution_block_generator::{generate_pow_block, Block, ExecutionBlockGenerator}; pub use hook::Hook; -pub use mock_builder::{Context as MockBuilderContext, MockBuilder, Operation, TestingBuilder}; +pub use mock_builder::{Context as MockBuilderContext, MockBuilder, MockBuilderServer, Operation}; pub use mock_execution_layer::MockExecutionLayer; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; @@ -43,7 +43,6 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_bodies_by_range_v1: true, get_payload_v1: true, get_payload_v2: true, - exchange_transition_configuration_v1: true, }; mod execution_block_generator; diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index 8a7d22496..b01e6a6ae 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -2,23 +2,23 @@ name = "genesis" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -eth1_test_rig = { path = "../../testing/eth1_test_rig" } -sensitive_url = { path = "../../common/sensitive_url" } +eth1_test_rig = { workspace = true } +sensitive_url = { workspace = true } [dependencies] -futures = "0.3.7" -types = { path = "../../consensus/types"} -environment = { path = "../../lighthouse/environment"} -eth1 = { path = "../eth1"} -rayon = "1.4.1" -state_processing = { path = "../../consensus/state_processing" } -merkle_proof = { path = "../../consensus/merkle_proof" } -ethereum_ssz = "0.5.0" -ethereum_hashing = "1.0.0-beta.2" -tree_hash = "0.5.0" -tokio = { version = "1.14.0", features = ["full"] } -slog = "2.5.2" -int_to_bytes = { path = "../../consensus/int_to_bytes" } +futures = { workspace = true } +types = { workspace = true } +environment = { workspace = true } +eth1 = { workspace = true } +rayon = { workspace = true } +state_processing = { workspace = true } +merkle_proof = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_hashing = { workspace = true } +tree_hash = { workspace = true } +tokio = { workspace = true } +slog = { workspace = true } +int_to_bytes = { workspace = true } diff --git a/beacon_node/genesis/src/common.rs b/beacon_node/genesis/src/common.rs index 06bf99f9f..e48fa3620 100644 --- a/beacon_node/genesis/src/common.rs +++ b/beacon_node/genesis/src/common.rs @@ -39,7 +39,7 @@ pub fn genesis_deposits( Ok(deposit_data .into_iter() - .zip(proofs.into_iter()) + .zip(proofs) .map(|(data, proof)| (data, proof.into())) .map(|(data, proof)| Deposit { proof, data }) .collect()) diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 2b117b26c..b58e0442f 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -2,53 +2,54 @@ name = "http_api" version = "0.1.0" authors = ["Paul Hauner "] -edition = "2021" -autotests = false # using a single test binary compiles faster +edition = { workspace = true } +autotests = false # using a single test binary compiles faster [dependencies] -warp = { version = "0.3.2", features = ["tls"] } -serde = { version = "1.0.116", features = ["derive"] } -tokio = { version = "1.14.0", features = ["macros","sync"] } -tokio-stream = { version = "0.1.3", features = ["sync"] } -types = { path = "../../consensus/types" } -hex = "0.4.2" -beacon_chain = { path = "../beacon_chain" } -eth2 = { path = "../../common/eth2", features = ["lighthouse"] } -slog = "2.5.2" -network = { path = "../network" } -lighthouse_network = { path = "../lighthouse_network" } -eth1 = { path = "../eth1" } -state_processing = { path = "../../consensus/state_processing" } -lighthouse_version = { path = "../../common/lighthouse_version" } -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -lazy_static = "1.4.0" -warp_utils = { path = "../../common/warp_utils" } -slot_clock = { path = "../../common/slot_clock" } -ethereum_ssz = "0.5.0" +warp = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +types = { workspace = true } +hex = { workspace = true } +beacon_chain = { workspace = true } +eth2 = { workspace = true } +slog = { workspace = true } +network = { workspace = true } +lighthouse_network = { workspace = true } +eth1 = { workspace = true } +state_processing = { workspace = true } +lighthouse_version = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +warp_utils = { workspace = true } +slot_clock = { workspace = true } +ethereum_ssz = { workspace = true } bs58 = "0.4.0" -futures = "0.3.8" -execution_layer = {path = "../execution_layer"} -parking_lot = "0.12.0" -safe_arith = {path = "../../consensus/safe_arith"} -task_executor = { path = "../../common/task_executor" } -lru = "0.7.7" -tree_hash = "0.5.0" -sysinfo = "0.26.5" +futures = { workspace = true } +execution_layer = { workspace = true } +parking_lot = { workspace = true } +safe_arith = { workspace = true } +task_executor = { workspace = true } +lru = { workspace = true } +tree_hash = { workspace = true } +sysinfo = { workspace = true } system_health = { path = "../../common/system_health" } -directory = { path = "../../common/directory" } -logging = { path = "../../common/logging" } -ethereum_serde_utils = "0.5.0" -operation_pool = { path = "../operation_pool" } -sensitive_url = { path = "../../common/sensitive_url" } -unused_port = {path = "../../common/unused_port"} -store = { path = "../store" } +directory = { workspace = true } +logging = { workspace = true } +ethereum_serde_utils = { workspace = true } +operation_pool = { workspace = true } +sensitive_url = { workspace = true } +store = { workspace = true } +bytes = { workspace = true } +beacon_processor = { workspace = true } [dev-dependencies] -environment = { path = "../../lighthouse/environment" } -serde_json = "1.0.58" -proto_array = { path = "../../consensus/proto_array" } -genesis = { path = "../genesis" } +environment = { workspace = true } +serde_json = { workspace = true } +proto_array = { workspace = true } +genesis = { workspace = true } [[test]] name = "bn_http_api_tests" -path = "tests/main.rs" \ No newline at end of file +path = "tests/main.rs" diff --git a/beacon_node/http_api/src/attester_duties.rs b/beacon_node/http_api/src/attester_duties.rs index 5c3e42083..f3242a2b3 100644 --- a/beacon_node/http_api/src/attester_duties.rs +++ b/beacon_node/http_api/src/attester_duties.rs @@ -1,9 +1,7 @@ //! Contains the handler for the `GET validator/duties/attester/{epoch}` endpoint. use crate::state_id::StateId; -use beacon_chain::{ - BeaconChain, BeaconChainError, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY, -}; +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{self as api_types}; use slot_clock::SlotClock; use state_processing::state_advance::partial_state_advance; @@ -32,12 +30,11 @@ pub fn attester_duties( // will equal `current_epoch + 1` let tolerant_current_epoch = chain .slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .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 == tolerant_current_epoch || request_epoch == current_epoch + 1 || request_epoch == tolerant_current_epoch + 1 { @@ -48,7 +45,7 @@ pub fn attester_duties( request_epoch, current_epoch ))) } else { - // request_epoch < current_epoch + // request_epoch < current_epoch, in fact we only allow `request_epoch == current_epoch-1` in this case compute_historic_attester_duties(request_epoch, request_indices, chain) } } diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index 1b924f382..e099e130a 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -75,7 +75,7 @@ impl PackingEfficiencyHandler { available_attestations: HashSet::new(), included_attestations: HashMap::new(), committee_store: CommitteeStore::new(), - _phantom: PhantomData::default(), + _phantom: PhantomData, }; handler.compute_epoch(start_epoch, &starting_state, spec)?; diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs index 828be8e57..299bc019c 100644 --- a/beacon_node/http_api/src/block_rewards.rs +++ b/beacon_node/http_api/src/block_rewards.rs @@ -49,7 +49,7 @@ pub fn get_block_rewards( .map_err(beacon_chain_error)?; state - .build_all_caches(&chain.spec) + .build_caches(&chain.spec) .map_err(beacon_state_error)?; let mut reward_cache = Default::default(); diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs new file mode 100644 index 000000000..90203f2d6 --- /dev/null +++ b/beacon_node/http_api/src/builder_states.rs @@ -0,0 +1,72 @@ +use crate::StateId; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use safe_arith::SafeArith; +use state_processing::per_block_processing::get_expected_withdrawals; +use state_processing::state_advance::partial_state_advance; +use std::sync::Arc; +use types::{BeaconState, EthSpec, ForkName, Slot, Withdrawals}; + +const MAX_EPOCH_LOOKAHEAD: u64 = 2; + +/// Get the withdrawals computed from the specified state, that will be included in the block +/// that gets built on the specified state. +pub fn get_next_withdrawals( + chain: &Arc>, + mut state: BeaconState, + state_id: StateId, + proposal_slot: Slot, +) -> Result, warp::Rejection> { + get_next_withdrawals_sanity_checks(chain, &state, proposal_slot)?; + + // 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) = + 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 + ))); + } + } + + match get_expected_withdrawals(&state, &chain.spec) { + Ok(withdrawals) => Ok(withdrawals), + Err(e) => Err(warp_utils::reject::custom_server_error(format!( + "failed to get expected withdrawal: {:?}", + e + ))), + } +} + +fn get_next_withdrawals_sanity_checks( + chain: &BeaconChain, + state: &BeaconState, + proposal_slot: Slot, +) -> Result<(), warp::Rejection> { + if proposal_slot <= state.slot() { + return Err(warp_utils::reject::custom_bad_request( + "proposal slot must be greater than the pre-state slot".to_string(), + )); + } + + let fork = chain.spec.fork_name_at_slot::(proposal_slot); + if let ForkName::Base | ForkName::Altair | ForkName::Merge = fork { + return Err(warp_utils::reject::custom_bad_request( + "the specified state is a pre-capella state.".to_string(), + )); + } + + let look_ahead_limit = MAX_EPOCH_LOOKAHEAD + .safe_mul(T::EthSpec::slots_per_epoch()) + .map_err(warp_utils::reject::arith_error)?; + if proposal_slot >= state.slot() + look_ahead_limit { + return Err(warp_utils::reject::custom_bad_request(format!( + "proposal slot is greater than or equal to the look ahead limit: {look_ahead_limit}" + ))); + } + + Ok(()) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 55e00bab3..cde05e682 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -10,6 +10,7 @@ mod attester_duties; mod block_id; mod block_packing_efficiency; mod block_rewards; +mod builder_states; mod database; mod metrics; mod proposer_duties; @@ -18,8 +19,10 @@ mod standard_block_rewards; mod state_id; mod sync_committee_rewards; mod sync_committees; +mod task_spawner; pub mod test_utils; mod ui; +mod validator; mod validator_inclusion; mod version; @@ -28,11 +31,14 @@ use beacon_chain::{ validator_monitor::timestamp_now, AttestationError as AttnError, BeaconChain, BeaconChainError, BeaconChainTypes, ProduceBlockVerification, WhenSlotSkipped, }; +use beacon_processor::BeaconProcessorSend; pub use block_id::BlockId; +use builder_states::get_next_withdrawals; +use bytes::Bytes; use directory::DEFAULT_ROOT_DIR; use eth2::types::{ - self as api_types, EndpointVersion, ForkChoice, ForkChoiceNode, SkipRandaoVerification, - ValidatorId, ValidatorStatus, + self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode, + SkipRandaoVerification, ValidatorId, ValidatorStatus, }; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; @@ -40,7 +46,9 @@ use logging::SSELoggingComponents; use network::{NetworkMessage, NetworkSenders, ValidatorSubscriptionMessage}; use operation_pool::ReceivedPreCapella; use parking_lot::RwLock; -use publish_blocks::ProvenancedBlock; +pub use publish_blocks::{ + publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock, +}; use serde::{Deserialize, Serialize}; use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; @@ -54,8 +62,15 @@ use std::pin::Pin; use std::sync::Arc; use sysinfo::{System, SystemExt}; use system_health::observe_system_health_bn; -use tokio::sync::mpsc::{Sender, UnboundedSender}; -use tokio_stream::{wrappers::BroadcastStream, StreamExt}; +use task_spawner::{Priority, TaskSpawner}; +use tokio::sync::{ + mpsc::{Sender, UnboundedSender}, + oneshot, +}; +use tokio_stream::{ + wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}, + StreamExt, +}; use types::{ Attestation, AttestationData, AttestationShufflingId, AttesterSlashing, BeaconStateError, BlindedPayload, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, FullPayload, @@ -64,6 +79,7 @@ use types::{ SignedContributionAndProof, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncCommitteeMessage, SyncContributionData, }; +use validator::pubkey_to_validator_index; use version::{ add_consensus_version_header, execution_optimistic_finalized_fork_versioned_response, fork_versioned_response, inconsistent_fork_rejection, unsupported_version_rejection, V1, V2, @@ -72,11 +88,7 @@ use warp::http::StatusCode; use warp::sse::Event; use warp::Reply; use warp::{http::Response, Filter}; -use warp_utils::{ - query::multi_key_query, - task::{blocking_json_task, blocking_response_task}, - uor::UnifyingOrFilter, -}; +use warp_utils::{query::multi_key_query, uor::UnifyingOrFilter}; const API_PREFIX: &str = "eth"; @@ -108,6 +120,7 @@ pub struct Context { pub chain: Option>>, pub network_senders: Option>, pub network_globals: Option>>, + pub beacon_processor_send: Option>, pub eth1_service: Option, pub sse_logging_components: Option, pub log: Logger, @@ -124,6 +137,10 @@ pub struct Config { pub allow_sync_stalled: bool, pub spec_fork_name: Option, pub data_dir: PathBuf, + pub sse_capacity_multiplier: usize, + pub enable_beacon_processor: bool, + #[serde(with = "eth2::types::serde_status_code")] + pub duplicate_block_status_code: StatusCode, } impl Default for Config { @@ -137,6 +154,9 @@ impl Default for Config { allow_sync_stalled: false, spec_fork_name: None, data_dir: PathBuf::from(DEFAULT_ROOT_DIR), + sse_capacity_multiplier: 1, + enable_beacon_processor: true, + duplicate_block_status_code: StatusCode::ACCEPTED, } } } @@ -324,6 +344,7 @@ pub fn serve( }; let eth_v1 = single_version(V1); + let eth_v2 = single_version(V2); // Create a `warp` filter that provides access to the network globals. let inner_network_globals = ctx.network_globals.clone(); @@ -484,6 +505,16 @@ pub fn serve( let app_start = std::time::Instant::now(); let app_start_filter = warp::any().map(move || app_start); + // Create a `warp` filter that provides access to the `TaskSpawner`. + let beacon_processor_send = ctx + .beacon_processor_send + .clone() + .filter(|_| config.enable_beacon_processor); + let task_spawner_filter = + warp::any().map(move || TaskSpawner::new(beacon_processor_send.clone())); + + let duplicate_block_status_code = ctx.config.duplicate_block_status_code; + /* * * Start of HTTP method definitions. @@ -495,17 +526,20 @@ pub fn serve( .and(warp::path("beacon")) .and(warp::path("genesis")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let genesis_data = api_types::GenesisData { - genesis_time: chain.genesis_time, - genesis_validators_root: chain.genesis_validators_root, - genesis_fork_version: chain.spec.genesis_fork_version, - }; - Ok(api_types::GenericResponse::from(genesis_data)) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let genesis_data = api_types::GenesisData { + genesis_time: chain.genesis_time, + genesis_validators_root: chain.genesis_validators_root, + genesis_fork_version: chain.spec.genesis_fork_version, + }; + Ok(api_types::GenericResponse::from(genesis_data)) + }) + }, + ); /* * beacon/states/{state_id} @@ -519,6 +553,7 @@ pub fn serve( "Invalid state ID".to_string(), )) })) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); // GET beacon/states/{state_id}/root @@ -526,65 +561,77 @@ pub fn serve( .clone() .and(warp::path("root")) .and(warp::path::end()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_json_task(move || { - let (root, execution_optimistic, finalized) = state_id.root(&chain)?; - Ok(root) - .map(api_types::RootData::from) - .map(api_types::GenericResponse::from) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) - }) - }); + .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(root) + .map(api_types::RootData::from) + .map(api_types::GenericResponse::from) + .map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) + }) + }, + ); // GET beacon/states/{state_id}/fork let get_beacon_state_fork = beacon_states_path .clone() .and(warp::path("fork")) .and(warp::path::end()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_json_task(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), + .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), + }) }) - }) - }); + }, + ); // 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()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_json_task(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, - )) - }, - )?; + .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), + Ok(api_types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) }) - }) - }); + }, + ); // GET beacon/states/{state_id}/validator_balances?id let get_beacon_state_validator_balances = beacon_states_path @@ -592,11 +639,12 @@ pub fn serve( .and(warp::path("validator_balances")) .and(warp::path::end()) .and(multi_key_query::()) - .and_then( + .then( |state_id: StateId, + task_spawner: TaskSpawner, chain: Arc>, query_res: Result| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P1, move || { let query = query_res?; let (data, execution_optimistic, finalized) = state_id .map_state_and_execution_optimistic_and_finalized( @@ -649,11 +697,12 @@ pub fn serve( .and(warp::path("validators")) .and(warp::path::end()) .and(multi_key_query::()) - .and_then( + .then( |state_id: StateId, + task_spawner: TaskSpawner, chain: Arc>, query_res: Result| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P1, move || { let query = query_res?; let (data, execution_optimistic, finalized) = state_id .map_state_and_execution_optimistic_and_finalized( @@ -732,17 +781,25 @@ pub fn serve( )) })) .and(warp::path::end()) - .and_then( - |state_id: StateId, chain: Arc>, validator_id: ValidatorId| { - blocking_json_task(move || { + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + validator_id: ValidatorId| { + 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 index_opt = match &validator_id { - ValidatorId::PublicKey(pubkey) => { - state.validators().iter().position(|v| v.pubkey == *pubkey) - } + 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), }; @@ -792,9 +849,12 @@ pub fn serve( .and(warp::path("committees")) .and(warp::query::()) .and(warp::path::end()) - .and_then( - |state_id: StateId, chain: Arc>, query: api_types::CommitteesQuery| { - blocking_json_task(move || { + .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, @@ -972,11 +1032,12 @@ pub fn serve( .and(warp::path("sync_committees")) .and(warp::query::()) .and(warp::path::end()) - .and_then( + .then( |state_id: StateId, + task_spawner: TaskSpawner, chain: Arc>, query: api_types::SyncCommitteesQuery| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P1, move || { let (sync_committee, execution_optimistic, finalized) = state_id .map_state_and_execution_optimistic_and_finalized( &chain, @@ -1037,9 +1098,12 @@ pub fn serve( .and(warp::path("randao")) .and(warp::query::()) .and(warp::path::end()) - .and_then( - |state_id: StateId, chain: Arc>, query: api_types::RandaoQuery| { - blocking_json_task(move || { + .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, @@ -1074,10 +1138,13 @@ pub fn serve( .and(warp::path("headers")) .and(warp::query::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |query: api_types::HeadersQuery, chain: Arc>| { - blocking_json_task(move || { + .then( + |query: api_types::HeadersQuery, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { let (root, block, execution_optimistic, finalized) = match (query.slot, query.parent_root) { // No query parameters, return the canonical head block. @@ -1171,36 +1238,41 @@ pub fn serve( )) })) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|block_id: BlockId, chain: Arc>| { - blocking_json_task(move || { - let (root, execution_optimistic, finalized) = block_id.root(&chain)?; - // Ignore the second `execution_optimistic` since the first one has more - // information about the original request. - let (block, _execution_optimistic, _finalized) = - BlockId::from_root(root).blinded_block(&chain)?; + .then( + |block_id: BlockId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (root, execution_optimistic, finalized) = block_id.root(&chain)?; + // Ignore the second `execution_optimistic` since the first one has more + // information about the original request. + let (block, _execution_optimistic, _finalized) = + BlockId::from_root(root).blinded_block(&chain)?; - let canonical = chain - .block_root_at_slot(block.slot(), WhenSlotSkipped::None) - .map_err(warp_utils::reject::beacon_chain_error)? - .map_or(false, |canonical| root == canonical); + let canonical = chain + .block_root_at_slot(block.slot(), WhenSlotSkipped::None) + .map_err(warp_utils::reject::beacon_chain_error)? + .map_or(false, |canonical| root == canonical); - let data = api_types::BlockHeaderData { - root, - canonical, - header: api_types::BlockHeaderAndSignature { - message: block.message().block_header(), - signature: block.signature().clone().into(), - }, - }; + let data = api_types::BlockHeaderData { + root, + canonical, + header: api_types::BlockHeaderAndSignature { + message: block.message().block_header(), + signature: block.signature().clone().into(), + }, + }; - Ok(api_types::ExecutionOptimisticFinalizedResponse { - execution_optimistic: Some(execution_optimistic), - finalized: Some(finalized), - data, + Ok(api_types::ExecutionOptimisticFinalizedResponse { + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + data, + }) }) - }) - }); + }, + ); /* * beacon/blocks @@ -1212,47 +1284,276 @@ pub fn serve( .and(warp::path("blocks")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |block: Arc>, - chain: Arc>, - network_tx: UnboundedSender>, - log: Logger| async move { - publish_blocks::publish_block( - None, - ProvenancedBlock::Local(block), - chain, - &network_tx, - log, - ) - .await - .map(|()| warp::reply().into_response()) + .then( + move |block: Arc>, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_blocks::publish_block( + None, + ProvenancedBlock::local(block), + chain, + &network_tx, + log, + BroadcastValidation::default(), + duplicate_block_status_code, + ) + .await + }) + }, + ); + + let post_beacon_blocks_ssz = eth_v1 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |block_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let block = + SignedBeaconBlock::::from_ssz_bytes(&block_bytes, &chain.spec) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "invalid SSZ: {e:?}" + )) + })?; + publish_blocks::publish_block( + None, + ProvenancedBlock::local(Arc::new(block)), + chain, + &network_tx, + log, + BroadcastValidation::default(), + duplicate_block_status_code, + ) + .await + }) + }, + ); + + let post_beacon_blocks_v2 = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |validation_level: api_types::BroadcastValidationQuery, + block: Arc>, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_blocks::publish_block( + None, + ProvenancedBlock::local(block), + chain, + &network_tx, + log, + validation_level.broadcast_validation, + duplicate_block_status_code, + ) + .await + }) + }, + ); + + let post_beacon_blocks_v2_ssz = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |validation_level: api_types::BroadcastValidationQuery, + block_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let block = + SignedBeaconBlock::::from_ssz_bytes(&block_bytes, &chain.spec) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "invalid SSZ: {e:?}" + )) + })?; + publish_blocks::publish_block( + None, + ProvenancedBlock::local(Arc::new(block)), + chain, + &network_tx, + log, + validation_level.broadcast_validation, + duplicate_block_status_code, + ) + .await + }) }, ); /* - * beacon/blocks + * beacon/blinded_blocks */ - // POST beacon/blocks + // POST beacon/blinded_blocks let post_beacon_blinded_blocks = eth_v1 .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |block: SignedBeaconBlock>, - chain: Arc>, - network_tx: UnboundedSender>, - log: Logger| async move { - publish_blocks::publish_blinded_block(block, chain, &network_tx, log) + .then( + move |block: SignedBlindedBeaconBlock, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + BroadcastValidation::default(), + duplicate_block_status_code, + ) .await - .map(|()| warp::reply().into_response()) + }) + }, + ); + + // POST beacon/blocks + let post_beacon_blinded_blocks_ssz = eth_v1 + .and(warp::path("beacon")) + .and(warp::path("blinded_blocks")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |block_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let block = SignedBlindedBeaconBlock::::from_ssz_bytes( + &block_bytes, + &chain.spec, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + BroadcastValidation::default(), + duplicate_block_status_code, + ) + .await + }) + }, + ); + + let post_beacon_blinded_blocks_v2 = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blinded_blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |validation_level: api_types::BroadcastValidationQuery, + block: SignedBlindedBeaconBlock, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + validation_level.broadcast_validation, + duplicate_block_status_code, + ) + .await + }) + }, + ); + + let post_beacon_blinded_blocks_v2_ssz = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("blinded_blocks")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .then( + move |validation_level: api_types::BroadcastValidationQuery, + block_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>, + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let block = SignedBlindedBeaconBlock::::from_ssz_bytes( + &block_bytes, + &chain.spec, + ) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_blocks::publish_blinded_block( + block, + chain, + &network_tx, + log, + validation_level.broadcast_validation, + duplicate_block_status_code, + ) + .await + }) }, ); @@ -1266,12 +1567,14 @@ pub fn serve( .and(warp::path("beacon")) .and(warp::path("blocks")) .and(block_id_or_err) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); let beacon_blocks_path_any = any_version .and(warp::path("beacon")) .and(warp::path("blocks")) .and(block_id_or_err) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); // GET beacon/blocks/{block_id} @@ -1279,12 +1582,13 @@ pub fn serve( .clone() .and(warp::path::end()) .and(warp::header::optional::("accept")) - .and_then( + .then( |endpoint_version: EndpointVersion, block_id: BlockId, + task_spawner: TaskSpawner, chain: Arc>, accept_header: Option| { - async move { + task_spawner.spawn_async_with_rejection(Priority::P1, async move { let (block, execution_optimistic, finalized) = block_id.full_block(&chain).await?; let fork_name = block @@ -1312,7 +1616,7 @@ pub fn serve( .map(|res| warp::reply::json(&res).into_response()), } .map(|resp| add_consensus_version_header(resp, fork_name)) - } + }) }, ); @@ -1321,44 +1625,56 @@ pub fn serve( .clone() .and(warp::path("root")) .and(warp::path::end()) - .and_then(|block_id: BlockId, chain: Arc>| { - blocking_json_task(move || { - let (block, execution_optimistic, finalized) = block_id.blinded_block(&chain)?; - Ok(api_types::GenericResponse::from(api_types::RootData::from( - block.canonical_root(), - )) - .add_execution_optimistic_finalized(execution_optimistic, finalized)) - }) - }); + .then( + |block_id: BlockId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (block, execution_optimistic, finalized) = + block_id.blinded_block(&chain)?; + Ok(api_types::GenericResponse::from(api_types::RootData::from( + block.canonical_root(), + )) + .add_execution_optimistic_finalized(execution_optimistic, finalized)) + }) + }, + ); // GET beacon/blocks/{block_id}/attestations let get_beacon_block_attestations = beacon_blocks_path_v1 .clone() .and(warp::path("attestations")) .and(warp::path::end()) - .and_then(|block_id: BlockId, chain: Arc>| { - blocking_json_task(move || { - let (block, execution_optimistic, finalized) = block_id.blinded_block(&chain)?; - Ok( - api_types::GenericResponse::from(block.message().body().attestations().clone()) - .add_execution_optimistic_finalized(execution_optimistic, finalized), - ) - }) - }); + .then( + |block_id: BlockId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (block, execution_optimistic, finalized) = + block_id.blinded_block(&chain)?; + Ok(api_types::GenericResponse::from( + block.message().body().attestations().clone(), + ) + .add_execution_optimistic_finalized(execution_optimistic, finalized)) + }) + }, + ); // GET beacon/blinded_blocks/{block_id} let get_beacon_blinded_block = eth_v1 .and(warp::path("beacon")) .and(warp::path("blinded_blocks")) .and(block_id_or_err) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(warp::path::end()) .and(warp::header::optional::("accept")) - .and_then( + .then( |block_id: BlockId, + task_spawner: TaskSpawner, chain: Arc>, accept_header: Option| { - blocking_response_task(move || { + task_spawner.blocking_response_task(Priority::P1, move || { let (block, execution_optimistic, finalized) = block_id.blinded_block(&chain)?; let fork_name = block @@ -1400,6 +1716,7 @@ pub fn serve( let beacon_pool_path = eth_v1 .and(warp::path("beacon")) .and(warp::path("pool")) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); // POST beacon/pool/attestations @@ -1410,12 +1727,13 @@ pub fn serve( .and(warp::body::json()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, attestations: Vec>, network_tx: UnboundedSender>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let seen_timestamp = timestamp_now(); let mut failures = Vec::new(); let mut num_already_known = 0; @@ -1551,9 +1869,11 @@ pub fn serve( .and(warp::path("attestations")) .and(warp::path::end()) .and(warp::query::()) - .and_then( - |chain: Arc>, query: api_types::AttestationPoolQuery| { - blocking_json_task(move || { + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + query: api_types::AttestationPoolQuery| { + task_spawner.blocking_json_task(Priority::P1, move || { let query_filter = |data: &AttestationData| { query.slot.map_or(true, |slot| slot == data.slot) && query @@ -1582,11 +1902,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(network_tx_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, slashing: AttesterSlashing, network_tx: UnboundedSender>| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let outcome = chain .verify_attester_slashing_for_gossip(slashing.clone()) .map_err(|e| { @@ -1623,12 +1944,14 @@ pub fn serve( .clone() .and(warp::path("attester_slashings")) .and(warp::path::end()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let attestations = chain.op_pool.get_all_attester_slashings(); - Ok(api_types::GenericResponse::from(attestations)) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let attestations = chain.op_pool.get_all_attester_slashings(); + Ok(api_types::GenericResponse::from(attestations)) + }) + }, + ); // POST beacon/pool/proposer_slashings let post_beacon_pool_proposer_slashings = beacon_pool_path @@ -1637,11 +1960,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(network_tx_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, slashing: ProposerSlashing, network_tx: UnboundedSender>| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let outcome = chain .verify_proposer_slashing_for_gossip(slashing.clone()) .map_err(|e| { @@ -1678,12 +2002,14 @@ pub fn serve( .clone() .and(warp::path("proposer_slashings")) .and(warp::path::end()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let attestations = chain.op_pool.get_all_proposer_slashings(); - Ok(api_types::GenericResponse::from(attestations)) - }) - }); + .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)) + }) + }, + ); // POST beacon/pool/voluntary_exits let post_beacon_pool_voluntary_exits = beacon_pool_path @@ -1692,11 +2018,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(network_tx_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, exit: SignedVoluntaryExit, network_tx: UnboundedSender>| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let outcome = chain .verify_voluntary_exit_for_gossip(exit.clone()) .map_err(|e| { @@ -1731,12 +2058,14 @@ pub fn serve( .clone() .and(warp::path("voluntary_exits")) .and(warp::path::end()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let attestations = chain.op_pool.get_all_voluntary_exits(); - Ok(api_types::GenericResponse::from(attestations)) - }) - }); + .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)) + }) + }, + ); // POST beacon/pool/sync_committees let post_beacon_pool_sync_committees = beacon_pool_path @@ -1746,12 +2075,13 @@ pub fn serve( .and(warp::body::json()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, signatures: Vec, network_tx: UnboundedSender>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { sync_committees::process_sync_committee_signatures( signatures, network_tx, &chain, log, )?; @@ -1765,12 +2095,14 @@ pub fn serve( .clone() .and(warp::path("bls_to_execution_changes")) .and(warp::path::end()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let address_changes = chain.op_pool.get_all_bls_to_execution_changes(); - Ok(api_types::GenericResponse::from(address_changes)) - }) - }); + .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)) + }) + }, + ); // POST beacon/pool/bls_to_execution_changes let post_beacon_pool_bls_to_execution_changes = beacon_pool_path @@ -1780,12 +2112,13 @@ pub fn serve( .and(warp::body::json()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, address_changes: Vec, network_tx: UnboundedSender>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let mut failures = vec![]; for (index, address_change) in address_changes.into_iter().enumerate() { @@ -1869,10 +2202,13 @@ pub fn serve( .and(warp::path("deposit_snapshot")) .and(warp::path::end()) .and(warp::header::optional::("accept")) + .and(task_spawner_filter.clone()) .and(eth1_service_filter.clone()) - .and_then( - |accept_header: Option, eth1_service: eth1::Service| { - blocking_response_task(move || match accept_header { + .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::Json) | None => { let snapshot = eth1_service.get_deposit_snapshot(); Ok( @@ -1913,6 +2249,7 @@ pub fn serve( let beacon_rewards_path = eth_v1 .and(warp::path("beacon")) .and(warp::path("rewards")) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); // GET beacon/rewards/blocks/{block_id} @@ -1921,17 +2258,75 @@ pub fn serve( .and(warp::path("blocks")) .and(block_id_or_err) .and(warp::path::end()) - .and_then(|chain: Arc>, block_id: BlockId| { - blocking_json_task(move || { - let (rewards, execution_optimistic, finalized) = - standard_block_rewards::compute_beacon_block_rewards(chain, block_id)?; - Ok(rewards) - .map(api_types::GenericResponse::from) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) - }) - }); + .then( + |task_spawner: TaskSpawner, + chain: Arc>, + block_id: BlockId| { + task_spawner.blocking_json_task(Priority::P1, move || { + let (rewards, execution_optimistic, finalized) = + standard_block_rewards::compute_beacon_block_rewards(chain, block_id)?; + Ok(rewards) + .map(api_types::GenericResponse::from) + .map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) + }) + }, + ); + + /* + * builder/states + */ + + let builder_states_path = eth_v1 + .and(warp::path("builder")) + .and(warp::path("states")) + .and(chain_filter.clone()); + + // GET builder/states/{state_id}/expected_withdrawals + let get_expected_withdrawals = builder_states_path + .clone() + .and(task_spawner_filter.clone()) + .and(warp::path::param::()) + .and(warp::path("expected_withdrawals")) + .and(warp::query::()) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .then( + |chain: Arc>, + task_spawner: TaskSpawner, + state_id: StateId, + query: api_types::ExpectedWithdrawalsQuery, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (state, execution_optimistic, finalized) = state_id.state(&chain)?; + let proposal_slot = query.proposal_slot.unwrap_or(state.slot() + 1); + let withdrawals = + get_next_withdrawals::(&chain, state, state_id, proposal_slot)?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(withdrawals.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => Ok(warp::reply::json( + &api_types::ExecutionOptimisticFinalizedResponse { + data: withdrawals, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }, + ) + .into_response()), + } + }) + }, + ); /* * beacon/rewards @@ -1940,6 +2335,7 @@ pub fn serve( let beacon_rewards_path = eth_v1 .and(warp::path("beacon")) .and(warp::path("rewards")) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()); // POST beacon/rewards/attestations/{epoch} @@ -1949,15 +2345,14 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path::end()) .and(warp::body::json()) - .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, epoch: Epoch, - validators: Vec, - log: Logger| { - blocking_json_task(move || { + validators: Vec| { + task_spawner.blocking_json_task(Priority::P1, move || { let attestation_rewards = chain - .compute_attestation_rewards(epoch, validators, log) + .compute_attestation_rewards(epoch, validators) .map_err(|e| match e { BeaconChainError::MissingBeaconState(root) => { warp_utils::reject::custom_not_found(format!( @@ -2002,12 +2397,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, block_id: BlockId, validators: Vec, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P1, move || { let (rewards, execution_optimistic, finalized) = sync_committee_rewards::compute_sync_committee_rewards( chain, block_id, validators, log, @@ -2032,46 +2428,55 @@ pub fn serve( let get_config_fork_schedule = config_path .and(warp::path("fork_schedule")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let forks = ForkName::list_all() - .into_iter() - .filter_map(|fork_name| chain.spec.fork_for_name(fork_name)) - .collect::>(); - Ok(api_types::GenericResponse::from(forks)) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let forks = ForkName::list_all() + .into_iter() + .filter_map(|fork_name| chain.spec.fork_for_name(fork_name)) + .collect::>(); + Ok(api_types::GenericResponse::from(forks)) + }) + }, + ); // GET config/spec let spec_fork_name = ctx.config.spec_fork_name; let get_config_spec = config_path .and(warp::path("spec")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(move |chain: Arc>| { - blocking_json_task(move || { - let config_and_preset = - ConfigAndPreset::from_chain_spec::(&chain.spec, spec_fork_name); - Ok(api_types::GenericResponse::from(config_and_preset)) - }) - }); + .then( + move |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + let config_and_preset = + ConfigAndPreset::from_chain_spec::(&chain.spec, spec_fork_name); + Ok(api_types::GenericResponse::from(config_and_preset)) + }) + }, + ); // GET config/deposit_contract let get_config_deposit_contract = config_path .and(warp::path("deposit_contract")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - Ok(api_types::GenericResponse::from( - api_types::DepositContractData { - address: chain.spec.deposit_contract_address, - chain_id: chain.spec.deposit_chain_id, - }, - )) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + Ok(api_types::GenericResponse::from( + api_types::DepositContractData { + address: chain.spec.deposit_contract_address, + chain_id: chain.spec.deposit_chain_id, + }, + )) + }) + }, + ); /* * debug @@ -2089,13 +2494,15 @@ pub fn serve( })) .and(warp::path::end()) .and(warp::header::optional::("accept")) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( + .then( |endpoint_version: EndpointVersion, state_id: StateId, accept_header: Option, + task_spawner: TaskSpawner, chain: Arc>| { - blocking_response_task(move || match accept_header { + task_spawner.blocking_response_task(Priority::P1, move || match accept_header { Some(api_types::Accept::Ssz) => { // We can ignore the optimistic status for the "fork" since it's a // specification constant that doesn't change across competing heads of the @@ -2147,10 +2554,13 @@ pub fn serve( .and(warp::path("beacon")) .and(warp::path("heads")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |endpoint_version: EndpointVersion, chain: Arc>| { - blocking_json_task(move || { + .then( + |endpoint_version: EndpointVersion, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { let heads = chain .heads() .into_iter() @@ -2183,48 +2593,51 @@ pub fn serve( .and(warp::path("debug")) .and(warp::path("fork_choice")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let beacon_fork_choice = chain.canonical_head.fork_choice_read_lock(); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let beacon_fork_choice = chain.canonical_head.fork_choice_read_lock(); - let proto_array = beacon_fork_choice.proto_array().core_proto_array(); + let proto_array = beacon_fork_choice.proto_array().core_proto_array(); - let fork_choice_nodes = proto_array - .nodes - .iter() - .map(|node| { - let execution_status = if node.execution_status.is_execution_enabled() { - Some(node.execution_status.to_string()) - } else { - None - }; + let fork_choice_nodes = proto_array + .nodes + .iter() + .map(|node| { + let execution_status = if node.execution_status.is_execution_enabled() { + Some(node.execution_status.to_string()) + } else { + None + }; - ForkChoiceNode { - slot: node.slot, - block_root: node.root, - parent_root: node - .parent - .and_then(|index| proto_array.nodes.get(index)) - .map(|parent| parent.root), - justified_epoch: node.justified_checkpoint.epoch, - finalized_epoch: node.finalized_checkpoint.epoch, - weight: node.weight, - validity: execution_status, - execution_block_hash: node - .execution_status - .block_hash() - .map(|block_hash| block_hash.into_root()), - } + ForkChoiceNode { + slot: node.slot, + block_root: node.root, + parent_root: node + .parent + .and_then(|index| proto_array.nodes.get(index)) + .map(|parent| parent.root), + justified_epoch: node.justified_checkpoint.epoch, + finalized_epoch: node.finalized_checkpoint.epoch, + weight: node.weight, + validity: execution_status, + execution_block_hash: node + .execution_status + .block_hash() + .map(|block_hash| block_hash.into_root()), + } + }) + .collect::>(); + Ok(ForkChoice { + justified_checkpoint: proto_array.justified_checkpoint, + finalized_checkpoint: proto_array.finalized_checkpoint, + fork_choice_nodes, }) - .collect::>(); - Ok(ForkChoice { - justified_checkpoint: proto_array.justified_checkpoint, - finalized_checkpoint: proto_array.finalized_checkpoint, - fork_choice_nodes, }) - }) - }); + }, + ); /* * node @@ -2235,50 +2648,54 @@ pub fn serve( .and(warp::path("node")) .and(warp::path("identity")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_json_task(move || { - let enr = network_globals.local_enr(); - let p2p_addresses = enr.multiaddr_p2p_tcp(); - let discovery_addresses = enr.multiaddr_p2p_udp(); - let meta_data = network_globals.local_metadata.read(); - Ok(api_types::GenericResponse::from(api_types::IdentityData { - peer_id: network_globals.local_peer_id().to_base58(), - enr, - p2p_addresses, - discovery_addresses, - metadata: api_types::MetaData { - seq_number: *meta_data.seq_number(), - attnets: format!( - "0x{}", - hex::encode(meta_data.attnets().clone().into_bytes()), - ), - syncnets: format!( - "0x{}", - hex::encode( - meta_data - .syncnets() - .map(|x| x.clone()) - .unwrap_or_default() - .into_bytes() - ) - ), - }, - })) - }) - }); + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let enr = network_globals.local_enr(); + let p2p_addresses = enr.multiaddr_p2p_tcp(); + let discovery_addresses = enr.multiaddr_p2p_udp(); + let meta_data = network_globals.local_metadata.read(); + Ok(api_types::GenericResponse::from(api_types::IdentityData { + peer_id: network_globals.local_peer_id().to_base58(), + enr, + p2p_addresses, + discovery_addresses, + metadata: api_types::MetaData { + seq_number: *meta_data.seq_number(), + attnets: format!( + "0x{}", + hex::encode(meta_data.attnets().clone().into_bytes()), + ), + syncnets: format!( + "0x{}", + hex::encode( + meta_data + .syncnets() + .map(|x| x.clone()) + .unwrap_or_default() + .into_bytes() + ) + ), + }, + })) + }) + }, + ); // GET node/version let get_node_version = eth_v1 .and(warp::path("node")) .and(warp::path("version")) .and(warp::path::end()) - .and_then(|| { - blocking_json_task(move || { - Ok(api_types::GenericResponse::from(api_types::VersionData { - version: version_with_platform(), - })) - }) + // Bypass the `task_spawner` since this method returns a static string. + .then(|| async { + warp::reply::json(&api_types::GenericResponse::from(api_types::VersionData { + version: version_with_platform(), + })) + .into_response() }); // GET node/syncing @@ -2286,10 +2703,13 @@ pub fn serve( .and(warp::path("node")) .and(warp::path("syncing")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) .and(chain_filter.clone()) - .and_then( - |network_globals: Arc>, chain: Arc>| { + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>, + chain: Arc>| { async move { let el_offline = if let Some(el) = &chain.execution_layer { el.is_offline_or_erroring().await @@ -2297,32 +2717,34 @@ pub fn serve( true }; - blocking_json_task(move || { - let head_slot = chain.canonical_head.cached_head().head_slot(); - let current_slot = chain.slot_clock.now_or_genesis().ok_or_else(|| { - warp_utils::reject::custom_server_error( - "Unable to read slot clock".into(), - ) - })?; + task_spawner + .blocking_json_task(Priority::P0, move || { + let head_slot = chain.canonical_head.cached_head().head_slot(); + let current_slot = + chain.slot_clock.now_or_genesis().ok_or_else(|| { + warp_utils::reject::custom_server_error( + "Unable to read slot clock".into(), + ) + })?; - // Taking advantage of saturating subtraction on slot. - let sync_distance = current_slot - head_slot; + // Taking advantage of saturating subtraction on slot. + let sync_distance = current_slot - head_slot; - let is_optimistic = chain - .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?; + let is_optimistic = chain + .is_optimistic_or_invalid_head() + .map_err(warp_utils::reject::beacon_chain_error)?; - let syncing_data = api_types::SyncingData { - is_syncing: network_globals.sync_state.read().is_syncing(), - is_optimistic: Some(is_optimistic), - el_offline: Some(el_offline), - head_slot, - sync_distance, - }; + let syncing_data = api_types::SyncingData { + is_syncing: network_globals.sync_state.read().is_syncing(), + is_optimistic: Some(is_optimistic), + el_offline: Some(el_offline), + head_slot, + sync_distance, + }; - Ok(api_types::GenericResponse::from(syncing_data)) - }) - .await + Ok(api_types::GenericResponse::from(syncing_data)) + }) + .await } }, ); @@ -2332,25 +2754,48 @@ pub fn serve( .and(warp::path("node")) .and(warp::path("health")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_response_task(move || match *network_globals.sync_state.read() { - SyncState::SyncingFinalized { .. } - | SyncState::SyncingHead { .. } - | SyncState::SyncTransition - | SyncState::BackFillSyncing { .. } => Ok(warp::reply::with_status( - warp::reply(), - warp::http::StatusCode::PARTIAL_CONTENT, - )), - SyncState::Synced => Ok(warp::reply::with_status( - warp::reply(), - warp::http::StatusCode::OK, - )), - SyncState::Stalled => Err(warp_utils::reject::not_synced( - "sync stalled, beacon chain may not yet be initialized.".to_string(), - )), - }) - }); + .and(chain_filter.clone()) + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>, + chain: Arc>| { + async move { + let el_offline = if let Some(el) = &chain.execution_layer { + el.is_offline_or_erroring().await + } else { + true + }; + + task_spawner + .blocking_response_task(Priority::P0, move || { + let is_optimistic = chain + .is_optimistic_or_invalid_head() + .map_err(warp_utils::reject::beacon_chain_error)?; + + let is_syncing = !network_globals.sync_state.read().is_synced(); + + if el_offline { + Err(warp_utils::reject::not_synced( + "execution layer is offline".to_string(), + )) + } else if is_syncing || is_optimistic { + Ok(warp::reply::with_status( + warp::reply(), + warp::http::StatusCode::PARTIAL_CONTENT, + )) + } else { + Ok(warp::reply::with_status( + warp::reply(), + warp::http::StatusCode::OK, + )) + } + }) + .await + } + }, + ); // GET node/peers/{peer_id} let get_node_peers_by_id = eth_v1 @@ -2358,10 +2803,13 @@ pub fn serve( .and(warp::path("peers")) .and(warp::path::param::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then( - |requested_peer_id: String, network_globals: Arc>| { - blocking_json_task(move || { + .then( + |requested_peer_id: String, + task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { let peer_id = PeerId::from_bytes( &bs58::decode(requested_peer_id.as_str()) .into_vec() @@ -2377,12 +2825,8 @@ pub fn serve( })?; if let Some(peer_info) = network_globals.peers.read().peer_info(&peer_id) { - let address = if let Some(socket_addr) = peer_info.seen_addresses().next() { - let mut addr = lighthouse_network::Multiaddr::from(socket_addr.ip()); - addr.push(lighthouse_network::multiaddr::Protocol::Tcp( - socket_addr.port(), - )); - addr.to_string() + let address = if let Some(multiaddr) = peer_info.seen_multiaddrs().next() { + multiaddr.to_string() } else if let Some(addr) = peer_info.listening_addresses().first() { addr.to_string() } else { @@ -2415,11 +2859,13 @@ pub fn serve( .and(warp::path("peers")) .and(warp::path::end()) .and(multi_key_query::()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then( + .then( |query_res: Result, + task_spawner: TaskSpawner, network_globals: Arc>| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P1, move || { let query = query_res?; let mut peers: Vec = Vec::new(); network_globals @@ -2428,13 +2874,8 @@ pub fn serve( .peers() .for_each(|(peer_id, peer_info)| { let address = - if let Some(socket_addr) = peer_info.seen_addresses().next() { - let mut addr = - lighthouse_network::Multiaddr::from(socket_addr.ip()); - addr.push(lighthouse_network::multiaddr::Protocol::Tcp( - socket_addr.port(), - )); - addr.to_string() + if let Some(multiaddr) = peer_info.seen_multiaddrs().next() { + multiaddr.to_string() } else if let Some(addr) = peer_info.listening_addresses().first() { addr.to_string() } else { @@ -2483,38 +2924,42 @@ pub fn serve( .and(warp::path("node")) .and(warp::path("peer_count")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_json_task(move || { - let mut connected: u64 = 0; - let mut connecting: u64 = 0; - let mut disconnected: u64 = 0; - let mut disconnecting: u64 = 0; + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + let mut connected: u64 = 0; + let mut connecting: u64 = 0; + let mut disconnected: u64 = 0; + let mut disconnecting: u64 = 0; - network_globals - .peers - .read() - .peers() - .for_each(|(_, peer_info)| { - let state = api_types::PeerState::from_peer_connection_status( - peer_info.connection_status(), - ); - match state { - api_types::PeerState::Connected => connected += 1, - api_types::PeerState::Connecting => connecting += 1, - api_types::PeerState::Disconnected => disconnected += 1, - api_types::PeerState::Disconnecting => disconnecting += 1, - } - }); + network_globals + .peers + .read() + .peers() + .for_each(|(_, peer_info)| { + let state = api_types::PeerState::from_peer_connection_status( + peer_info.connection_status(), + ); + match state { + api_types::PeerState::Connected => connected += 1, + api_types::PeerState::Connecting => connecting += 1, + api_types::PeerState::Disconnected => disconnected += 1, + api_types::PeerState::Disconnecting => disconnecting += 1, + } + }); - Ok(api_types::GenericResponse::from(api_types::PeerCount { - connected, - connecting, - disconnected, - disconnecting, - })) - }) - }); + Ok(api_types::GenericResponse::from(api_types::PeerCount { + connected, + connecting, + disconnected, + disconnecting, + })) + }) + }, + ); /* * validator */ @@ -2531,11 +2976,19 @@ pub fn serve( })) .and(warp::path::end()) .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then(|epoch: Epoch, chain: Arc>, log: Logger| { - blocking_json_task(move || proposer_duties::proposer_duties(epoch, &chain, &log)) - }); + .then( + |epoch: Epoch, + task_spawner: TaskSpawner, + chain: Arc>, + log: Logger| { + task_spawner.blocking_json_task(Priority::P0, move || { + proposer_duties::proposer_duties(epoch, &chain, &log) + }) + }, + ); // GET validator/blocks/{slot} let get_validator_blocks = any_version @@ -2549,56 +3002,78 @@ pub fn serve( .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()) .and(log_filter.clone()) - .and_then( + .then( |endpoint_version: EndpointVersion, slot: Slot, query: api_types::ValidatorBlocksQuery, + accept_header: Option, + task_spawner: TaskSpawner, chain: Arc>, - log: Logger| async move { - debug!( - log, - "Block production request from HTTP API"; - "slot" => slot - ); + log: Logger| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!( + log, + "Block production request from HTTP API"; + "slot" => slot + ); - let randao_reveal = query.randao_reveal.decompress().map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "randao reveal is not a valid BLS signature: {:?}", - e - )) - })?; + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; - let randao_verification = - if query.skip_randao_verification == SkipRandaoVerification::Yes { - if !randao_reveal.is_infinity() { - return Err(warp_utils::reject::custom_bad_request( + let randao_verification = + if query.skip_randao_verification == SkipRandaoVerification::Yes { + if !randao_reveal.is_infinity() { + return Err(warp_utils::reject::custom_bad_request( "randao_reveal must be point-at-infinity if verification is skipped" .into(), )); - } - ProduceBlockVerification::NoVerification - } else { - ProduceBlockVerification::VerifyRandao - }; + } + ProduceBlockVerification::NoVerification + } else { + ProduceBlockVerification::VerifyRandao + }; - let (block, _) = chain - .produce_block_with_verification::>( - randao_reveal, - slot, - query.graffiti.map(Into::into), - randao_verification, - ) - .await - .map_err(warp_utils::reject::block_production_error)?; - let fork_name = block - .to_ref() - .fork_name(&chain.spec) - .map_err(inconsistent_fork_rejection)?; + let (block, _) = chain + .produce_block_with_verification::>( + randao_reveal, + slot, + query.graffiti.map(Into::into), + randao_verification, + ) + .await + .map_err(warp_utils::reject::block_production_error)?; + let fork_name = block + .to_ref() + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; - fork_versioned_response(endpoint_version, fork_name, block) - .map(|response| warp::reply::json(&response).into_response()) + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(block.as_ssz_bytes().into()) + .map(|res: Response| { + add_consensus_version_header(res, fork_name) + }) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => fork_versioned_response(endpoint_version, fork_name, block) + .map(|response| warp::reply::json(&response).into_response()) + .map(|res| add_consensus_version_header(res, fork_name)), + } + }) }, ); @@ -2614,48 +3089,70 @@ pub fn serve( .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()) - .and_then( + .then( |slot: Slot, query: api_types::ValidatorBlocksQuery, - chain: Arc>| async move { - let randao_reveal = query.randao_reveal.decompress().map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "randao reveal is not a valid BLS signature: {:?}", - e - )) - })?; + accept_header: Option, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; - let randao_verification = - if query.skip_randao_verification == SkipRandaoVerification::Yes { - if !randao_reveal.is_infinity() { - return Err(warp_utils::reject::custom_bad_request( + let randao_verification = + if query.skip_randao_verification == SkipRandaoVerification::Yes { + if !randao_reveal.is_infinity() { + return Err(warp_utils::reject::custom_bad_request( "randao_reveal must be point-at-infinity if verification is skipped" .into() )); - } - ProduceBlockVerification::NoVerification - } else { - ProduceBlockVerification::VerifyRandao - }; + } + ProduceBlockVerification::NoVerification + } else { + ProduceBlockVerification::VerifyRandao + }; - let (block, _) = chain - .produce_block_with_verification::>( - randao_reveal, - slot, - query.graffiti.map(Into::into), - randao_verification, - ) - .await - .map_err(warp_utils::reject::block_production_error)?; - let fork_name = block - .to_ref() - .fork_name(&chain.spec) - .map_err(inconsistent_fork_rejection)?; + let (block, _) = chain + .produce_block_with_verification::>( + randao_reveal, + slot, + query.graffiti.map(Into::into), + randao_verification, + ) + .await + .map_err(warp_utils::reject::block_production_error)?; + let fork_name = block + .to_ref() + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; - // Pose as a V2 endpoint so we return the fork `version`. - fork_versioned_response(V2, fork_name, block) - .map(|response| warp::reply::json(&response).into_response()) + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(block.as_ssz_bytes().into()) + .map(|res: Response| { + add_consensus_version_header(res, fork_name) + }) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + // Pose as a V2 endpoint so we return the fork `version`. + _ => fork_versioned_response(V2, fork_name, block) + .map(|response| warp::reply::json(&response).into_response()) + .map(|res| add_consensus_version_header(res, fork_name)), + } + }) }, ); @@ -2666,10 +3163,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::query::()) .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |query: api_types::ValidatorAttestationDataQuery, chain: Arc>| { - blocking_json_task(move || { + .then( + |query: api_types::ValidatorAttestationDataQuery, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { let current_slot = chain .slot() .map_err(warp_utils::reject::beacon_chain_error)?; @@ -2698,10 +3198,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::query::()) .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |query: api_types::ValidatorAggregateAttestationQuery, chain: Arc>| { - blocking_json_task(move || { + .then( + |query: api_types::ValidatorAggregateAttestationQuery, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { chain .get_aggregated_attestation_by_slot_and_root( query.slot, @@ -2736,16 +3239,20 @@ pub fn serve( .and(warp::path::end()) .and(not_while_syncing_filter.clone()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |epoch: Epoch, indices: api_types::ValidatorIndexData, chain: Arc>| { - blocking_json_task(move || { + .then( + |epoch: Epoch, + indices: api_types::ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { attester_duties::attester_duties(epoch, &indices.0, &chain) }) }, ); - // POST validator/duties/sync + // POST validator/duties/sync/{epoch} let post_validator_duties_sync = eth_v1 .and(warp::path("validator")) .and(warp::path("duties")) @@ -2758,10 +3265,14 @@ pub fn serve( .and(warp::path::end()) .and(not_while_syncing_filter.clone()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |epoch: Epoch, indices: api_types::ValidatorIndexData, chain: Arc>| { - blocking_json_task(move || { + .then( + |epoch: Epoch, + indices: api_types::ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { sync_committees::sync_committee_duties(epoch, &indices.0, &chain) }) }, @@ -2774,10 +3285,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::query::()) .and(not_while_syncing_filter.clone()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |sync_committee_data: SyncContributionData, chain: Arc>| { - blocking_json_task(move || { + .then( + |sync_committee_data: SyncContributionData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { chain .get_aggregated_sync_committee_contribution(&sync_committee_data) .map_err(|e| { @@ -2802,15 +3316,17 @@ pub fn serve( .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::body::json()) .and(network_tx_filter.clone()) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, aggregates: Vec>, network_tx: UnboundedSender>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { let seen_timestamp = timestamp_now(); let mut verified_aggregates = Vec::with_capacity(aggregates.len()); let mut messages = Vec::with_capacity(aggregates.len()); @@ -2843,7 +3359,7 @@ pub fn serve( // It's reasonably likely that two different validators produce // identical aggregates, especially if they're using the same beacon // node. - Err(AttnError::AttestationAlreadyKnown(_)) => continue, + Err(AttnError::AttestationSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // @@ -2913,16 +3429,18 @@ pub fn serve( .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::body::json()) .and(network_tx_filter) .and(log_filter.clone()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, contributions: Vec>, network_tx: UnboundedSender>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { sync_committees::process_signed_contribution_and_proofs( contributions, network_tx, @@ -2941,14 +3459,16 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_subscription_tx_filter.clone()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then( + .then( |subscriptions: Vec, validator_subscription_tx: Sender, + task_spawner: TaskSpawner, chain: Arc>, log: Logger| { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { for subscription in &subscriptions { chain .validator_monitor @@ -2990,45 +3510,49 @@ pub fn serve( .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(log_filter.clone()) .and(warp::body::json()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, log: Logger, - preparation_data: Vec| async move { - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; + preparation_data: Vec| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BeaconChainError::ExecutionLayerMissing) + .map_err(warp_utils::reject::beacon_chain_error)?; - let current_slot = chain - .slot() - .map_err(warp_utils::reject::beacon_chain_error)?; - let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); + let current_slot = chain + .slot() + .map_err(warp_utils::reject::beacon_chain_error)?; + let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - debug!( - log, - "Received proposer preparation data"; - "count" => preparation_data.len(), - ); + debug!( + log, + "Received proposer preparation data"; + "count" => preparation_data.len(), + ); - execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) - .await; + execution_layer + .update_proposer_preparation(current_epoch, &preparation_data) + .await; - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; + 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()) + Ok::<_, warp::reject::Rejection>(warp::reply::json(&()).into_response()) + }) }, ); @@ -3037,127 +3561,177 @@ pub fn serve( .and(warp::path("validator")) .and(warp::path("register_validator")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) .and(warp::body::json()) - .and_then( - |chain: Arc>, + .then( + |task_spawner: TaskSpawner, + chain: Arc>, log: Logger, - register_val_data: Vec| async move { - let execution_layer = chain - .execution_layer - .as_ref() - .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; - let current_slot = chain - .slot_clock - .now_or_genesis() - .ok_or(BeaconChainError::UnableToReadSlot) - .map_err(warp_utils::reject::beacon_chain_error)?; - let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); + register_val_data: Vec| async { + let (tx, rx) = oneshot::channel(); - debug!( - log, - "Received register validator request"; - "count" => register_val_data.len(), - ); + 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::beacon_chain_error)?; + let current_slot = chain + .slot_clock + .now_or_genesis() + .ok_or(BeaconChainError::UnableToReadSlot) + .map_err(warp_utils::reject::beacon_chain_error)?; + let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); - let head_snapshot = chain.head_snapshot(); - let spec = &chain.spec; + debug!( + log, + "Received register validator request"; + "count" => register_val_data.len(), + ); - let (preparation_data, filtered_registration_data): ( - Vec, - 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); + let head_snapshot = chain.head_snapshot(); + let spec = &chain.spec; - // 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, - }, - register_data, - ) - }) + let (preparation_data, filtered_registration_data): ( + Vec, + 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, + }, + register_data, + ) + }) + }) }) - }) - .unzip(); + .unzip(); - // Update the prepare beacon proposer cache based on this request. - execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) + // Update the prepare beacon proposer cache based on this request. + execution_layer + .update_proposer_preparation(current_epoch, &preparation_data) + .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!( + log, + "Forwarding register validator request to connected builder"; + "count" => filtered_registration_data.len(), + ); + + // 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::beacon_chain_error)? + .builder(); + let builder = arc_builder + .as_ref() + .ok_or(BeaconChainError::BuilderMissing) + .map_err(warp_utils::reject::beacon_chain_error)?; + + builder + .post_builder_validators(&filtered_registration_data) + .await + .map(|resp| warp::reply::json(&resp).into_response()) + .map_err(|e| { + warn!( + log, + "Relay error when registering validator(s)"; + "num_registrations" => filtered_registration_data.len(), + "error" => ?e + ); + // 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; - // 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 - )) - })?; + if initial_result.is_err() { + return task_spawner::convert_rejection(initial_result).await; + } - let builder = execution_layer - .builder() - .as_ref() - .ok_or(BeaconChainError::BuilderMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; - - info!( - log, - "Forwarding register validator request to connected builder"; - "count" => filtered_registration_data.len(), - ); - - builder - .post_builder_validators(&filtered_registration_data) - .await - .map(|resp| warp::reply::json(&resp).into_response()) - .map_err(|e| { - warn!( - log, - "Relay error when registering validator(s)"; - "num_registrations" => filtered_registration_data.len(), - "error" => ?e - ); - // 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:?}")) - }) + // Await a response from the builder without blocking a + // `BeaconProcessor` worker. + task_spawner::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 }, ); // POST validator/sync_committee_subscriptions @@ -3167,15 +3741,17 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_subscription_tx_filter) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then( + .then( |subscriptions: Vec, validator_subscription_tx: Sender, + task_spawner: TaskSpawner, chain: Arc>, log: Logger | { - blocking_json_task(move || { + task_spawner.blocking_json_task(Priority::P0, move || { for subscription in subscriptions { chain .validator_monitor @@ -3203,15 +3779,61 @@ pub fn serve( }, ); + // POST vaidator/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::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |epoch: Epoch, + indices: Vec, + 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::beacon_chain_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 + .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)) + }) + }, + ); + // POST lighthouse/liveness let post_lighthouse_liveness = warp::path("lighthouse") .and(warp::path("liveness")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |request_data: api_types::LivenessRequestData, chain: Arc>| { - blocking_json_task(move || { + .then( + |request_data: api_types::LivenessRequestData, + 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() @@ -3250,8 +3872,9 @@ pub fn serve( let get_lighthouse_health = warp::path("lighthouse") .and(warp::path("health")) .and(warp::path::end()) - .and_then(|| { - blocking_json_task(move || { + .and(task_spawner_filter.clone()) + .then(|task_spawner: TaskSpawner| { + task_spawner.blocking_json_task(Priority::P0, move || { eth2::lighthouse::Health::observe() .map(api_types::GenericResponse::from) .map_err(warp_utils::reject::custom_bad_request) @@ -3263,13 +3886,18 @@ pub fn serve( .and(warp::path("ui")) .and(warp::path("health")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(system_info_filter) .and(app_start_filter) .and(data_dir_filter) .and(network_globals.clone()) - .and_then( - |sysinfo, app_start: std::time::Instant, data_dir, network_globals| { - blocking_json_task(move || { + .then( + |task_spawner: TaskSpawner, + sysinfo, + app_start: std::time::Instant, + data_dir, + network_globals| { + task_spawner.blocking_json_task(Priority::P0, move || { let app_uptime = app_start.elapsed().as_secs(); Ok(api_types::GenericResponse::from(observe_system_health_bn( sysinfo, @@ -3286,12 +3914,15 @@ pub fn serve( .and(warp::path("ui")) .and(warp::path("validator_count")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - ui::get_validator_count(chain).map(api_types::GenericResponse::from) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + ui::get_validator_count(chain).map(api_types::GenericResponse::from) + }) + }, + ); // POST lighthouse/ui/validator_metrics let post_lighthouse_ui_validator_metrics = warp::path("lighthouse") @@ -3299,10 +3930,13 @@ pub fn serve( .and(warp::path("validator_metrics")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |request_data: ui::ValidatorMetricsRequestData, chain: Arc>| { - blocking_json_task(move || { + .then( + |request_data: ui::ValidatorMetricsRequestData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { ui::post_validator_monitor_metrics(request_data, chain) .map(api_types::GenericResponse::from) }) @@ -3315,10 +3949,13 @@ pub fn serve( .and(warp::path("validator_info")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |request_data: ui::ValidatorInfoRequestData, chain: Arc>| { - blocking_json_task(move || { + .then( + |request_data: ui::ValidatorInfoRequestData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { ui::get_validator_info(request_data, chain) .map(api_types::GenericResponse::from) }) @@ -3329,21 +3966,26 @@ pub fn serve( let get_lighthouse_syncing = warp::path("lighthouse") .and(warp::path("syncing")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_json_task(move || { - Ok(api_types::GenericResponse::from( - network_globals.sync_state(), - )) - }) - }); + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + Ok(api_types::GenericResponse::from( + network_globals.sync_state(), + )) + }) + }, + ); // GET lighthouse/nat let get_lighthouse_nat = warp::path("lighthouse") .and(warp::path("nat")) + .and(task_spawner_filter.clone()) .and(warp::path::end()) - .and_then(|| { - blocking_json_task(move || { + .then(|task_spawner: TaskSpawner| { + task_spawner.blocking_json_task(Priority::P1, move || { Ok(api_types::GenericResponse::from( lighthouse_network::metrics::NAT_OPEN .as_ref() @@ -3358,57 +4000,70 @@ pub fn serve( let get_lighthouse_peers = warp::path("lighthouse") .and(warp::path("peers")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals.clone()) - .and_then(|network_globals: Arc>| { - blocking_json_task(move || { - Ok(network_globals - .peers - .read() - .peers() - .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { - peer_id: peer_id.to_string(), - peer_info: peer_info.clone(), - }) - .collect::>()) - }) - }); + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + Ok(network_globals + .peers + .read() + .peers() + .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + peer_id: peer_id.to_string(), + peer_info: peer_info.clone(), + }) + .collect::>()) + }) + }, + ); // GET lighthouse/peers/connected let get_lighthouse_peers_connected = warp::path("lighthouse") .and(warp::path("peers")) .and(warp::path("connected")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(network_globals) - .and_then(|network_globals: Arc>| { - blocking_json_task(move || { - Ok(network_globals - .peers - .read() - .connected_peers() - .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { - peer_id: peer_id.to_string(), - peer_info: peer_info.clone(), - }) - .collect::>()) - }) - }); + .then( + |task_spawner: TaskSpawner, + network_globals: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + Ok(network_globals + .peers + .read() + .connected_peers() + .map(|(peer_id, peer_info)| eth2::lighthouse::Peer { + peer_id: peer_id.to_string(), + peer_info: peer_info.clone(), + }) + .collect::>()) + }) + }, + ); // GET lighthouse/proto_array let get_lighthouse_proto_array = warp::path("lighthouse") .and(warp::path("proto_array")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_response_task(move || { - Ok::<_, warp::Rejection>(warp::reply::json(&api_types::GenericResponseRef::from( - chain - .canonical_head - .fork_choice_read_lock() - .proto_array() - .core_proto_array(), - ))) - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + Ok::<_, warp::Rejection>(warp::reply::json( + &api_types::GenericResponseRef::from( + chain + .canonical_head + .fork_choice_read_lock() + .proto_array() + .core_proto_array(), + ), + )) + }) + }, + ); // GET lighthouse/validator_inclusion/{epoch}/{validator_id} let get_lighthouse_validator_inclusion_global = warp::path("lighthouse") @@ -3416,10 +4071,14 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path::param::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then( - |epoch: Epoch, validator_id: ValidatorId, chain: Arc>| { - blocking_json_task(move || { + .then( + |epoch: Epoch, + validator_id: ValidatorId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { validator_inclusion::validator_inclusion_data(epoch, &validator_id, &chain) .map(api_types::GenericResponse::from) }) @@ -3432,82 +4091,94 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path("global")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|epoch: Epoch, chain: Arc>| { - blocking_json_task(move || { - validator_inclusion::global_validator_inclusion_data(epoch, &chain) - .map(api_types::GenericResponse::from) - }) - }); + .then( + |epoch: Epoch, task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + validator_inclusion::global_validator_inclusion_data(epoch, &chain) + .map(api_types::GenericResponse::from) + }) + }, + ); // 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()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - let current_slot_opt = chain.slot().ok(); + .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) - }) - }); + 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()) - .and_then(|eth1_service: eth1::Service| { - blocking_json_task(move || { - Ok(api_types::GenericResponse::from( - eth1_service - .blocks() - .read() - .iter() - .cloned() - .collect::>(), - )) - }) - }); + .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) - .and_then(|eth1_service: eth1::Service| { - blocking_json_task(move || { - Ok(api_types::GenericResponse::from( - eth1_service - .deposits() - .read() - .cache - .iter() - .cloned() - .collect::>(), - )) - }) - }); + .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/beacon/states/{state_id}/ssz let get_lighthouse_beacon_states_ssz = warp::path("lighthouse") @@ -3516,42 +4187,50 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path("ssz")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_response_task(move || { - // This debug endpoint provides no indication of optimistic status. - let (state, _execution_optimistic, _finalized) = state_id.state(&chain)?; - Response::builder() - .status(200) - .header("Content-Type", "application/ssz") - .body(state.as_ssz_bytes()) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }) - }) - }); + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_response_task(Priority::P1, move || { + // This debug endpoint provides no indication of optimistic status. + let (state, _execution_optimistic, _finalized) = state_id.state(&chain)?; + Response::builder() + .status(200) + .header("Content-Type", "application/ssz") + .body(state.as_ssz_bytes()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }) + }) + }, + ); // 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()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - if chain.eth1_chain.is_some() { - Ok(()) - } else { - Err(warp_utils::reject::custom_not_found( - "staking is not enabled, \ + .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(), - )) - } - }) - }); + .to_string(), + )) + } + }) + }, + ); let database_path = warp::path("lighthouse").and(warp::path("database")); @@ -3559,31 +4238,41 @@ pub fn serve( let get_lighthouse_database_info = database_path .and(warp::path("info")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| blocking_json_task(move || database::info(chain))); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || database::info(chain)) + }, + ); // POST lighthouse/database/reconstruct let post_lighthouse_database_reconstruct = database_path .and(warp::path("reconstruct")) .and(warp::path::end()) .and(not_while_syncing_filter) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| { - blocking_json_task(move || { - chain.store_migrator.process_reconstruction(); - Ok("success") - }) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + chain.store_migrator.process_reconstruction(); + Ok("success") + }) + }, + ); // POST lighthouse/database/historical_blocks let post_lighthouse_database_historical_blocks = database_path .and(warp::path("historical_blocks")) .and(warp::path::end()) .and(warp::body::json()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then( + .then( |blocks: Vec>>, + task_spawner: TaskSpawner, chain: Arc>, log: Logger| { info!( @@ -3592,7 +4281,9 @@ pub fn serve( "count" => blocks.len(), "source" => "http_api" ); - blocking_json_task(move || database::historical_blocks(chain, blocks)) + task_spawner.blocking_json_task(Priority::P1, move || { + database::historical_blocks(chain, blocks) + }) }, ); @@ -3602,10 +4293,13 @@ pub fn serve( .and(warp::path("block_rewards")) .and(warp::query::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then(|query, chain, log| { - blocking_json_task(move || block_rewards::get_block_rewards(query, chain, log)) + .then(|query, task_spawner: TaskSpawner, chain, log| { + task_spawner.blocking_json_task(Priority::P1, move || { + block_rewards::get_block_rewards(query, chain, log) + }) }); // POST lighthouse/analysis/block_rewards @@ -3614,11 +4308,16 @@ pub fn serve( .and(warp::path("block_rewards")) .and(warp::body::json()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) - .and_then(|blocks, chain, log| { - blocking_json_task(move || block_rewards::compute_block_rewards(blocks, chain, log)) - }); + .then( + |blocks, task_spawner: TaskSpawner, chain, log| { + task_spawner.blocking_json_task(Priority::P1, move || { + block_rewards::compute_block_rewards(blocks, chain, log) + }) + }, + ); // GET lighthouse/analysis/attestation_performance/{index} let get_lighthouse_attestation_performance = warp::path("lighthouse") @@ -3627,12 +4326,15 @@ pub fn serve( .and(warp::path::param::()) .and(warp::query::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|target, query, chain: Arc>| { - blocking_json_task(move || { - attestation_performance::get_attestation_performance(target, query, chain) - }) - }); + .then( + |target, query, task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + attestation_performance::get_attestation_performance(target, query, chain) + }) + }, + ); // GET lighthouse/analysis/block_packing_efficiency let get_lighthouse_block_packing_efficiency = warp::path("lighthouse") @@ -3640,35 +4342,46 @@ pub fn serve( .and(warp::path("block_packing_efficiency")) .and(warp::query::()) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|query, chain: Arc>| { - blocking_json_task(move || { - block_packing_efficiency::get_block_packing_efficiency(query, chain) - }) - }); + .then( + |query, task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + block_packing_efficiency::get_block_packing_efficiency(query, chain) + }) + }, + ); // GET lighthouse/merge_readiness let get_lighthouse_merge_readiness = warp::path("lighthouse") .and(warp::path("merge_readiness")) .and(warp::path::end()) + .and(task_spawner_filter.clone()) .and(chain_filter.clone()) - .and_then(|chain: Arc>| async move { - let merge_readiness = chain.check_merge_readiness().await; - Ok::<_, warp::reject::Rejection>( - warp::reply::json(&api_types::GenericResponse::from(merge_readiness)) - .into_response(), - ) - }); + .then( + |task_spawner: TaskSpawner, chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P1, async move { + let current_slot = chain.slot_clock.now_or_genesis().unwrap_or(Slot::new(0)); + let merge_readiness = chain.check_merge_readiness(current_slot).await; + Ok::<_, warp::reject::Rejection>( + warp::reply::json(&api_types::GenericResponse::from(merge_readiness)) + .into_response(), + ) + }) + }, + ); let get_events = eth_v1 .and(warp::path("events")) .and(warp::path::end()) .and(multi_key_query::()) + .and(task_spawner_filter.clone()) .and(chain_filter) - .and_then( + .then( |topics_res: Result, + task_spawner: TaskSpawner, chain: Arc>| { - blocking_response_task(move || { + task_spawner.blocking_response_task(Priority::P0, move || { let topics = topics_res?; // for each topic subscribed spawn a new subscription let mut receivers = Vec::with_capacity(topics.topics.len()); @@ -3704,22 +4417,29 @@ pub fn serve( } }; - receivers.push(BroadcastStream::new(receiver).map(|msg| { - match msg { - Ok(data) => Event::default() - .event(data.topic_name()) - .json_data(data) - .map_err(|e| { - warp_utils::reject::server_sent_event_error(format!( - "{:?}", - e - )) - }), - Err(e) => Err(warp_utils::reject::server_sent_event_error( - format!("{:?}", e), - )), - } - })); + receivers.push( + BroadcastStream::new(receiver) + .map(|msg| { + match msg { + Ok(data) => Event::default() + .event(data.topic_name()) + .json_data(data) + .unwrap_or_else(|e| { + Event::default() + .comment(format!("error - bad json: {e:?}")) + }), + // Do not terminate the stream if the channel fills + // up. Just drop some messages and send a comment to + // the client. + Err(BroadcastStreamRecvError::Lagged(n)) => { + Event::default().comment(format!( + "error - dropped {n} messages" + )) + } + } + }) + .map(Ok::<_, std::convert::Infallible>), + ); } } else { return Err(warp_utils::reject::custom_server_error( @@ -3729,7 +4449,7 @@ pub fn serve( let s = futures::stream::select_all(receivers); - Ok::<_, warp::Rejection>(warp::sse::reply(warp::sse::keep_alive().stream(s))) + Ok(warp::sse::reply(warp::sse::keep_alive().stream(s))) }) }, ); @@ -3739,38 +4459,46 @@ pub fn serve( let lighthouse_log_events = warp::path("lighthouse") .and(warp::path("logs")) .and(warp::path::end()) + .and(task_spawner_filter) .and(sse_component_filter) - .and_then(|sse_component: Option| { - blocking_response_task(move || { - if let Some(logging_components) = sse_component { - // Build a JSON stream - let s = - BroadcastStream::new(logging_components.sender.subscribe()).map(|msg| { - match msg { - Ok(data) => { - // Serialize to json - match data.to_json_string() { - // Send the json as a Server Side Event - Ok(json) => Ok(Event::default().data(json)), - Err(e) => Err(warp_utils::reject::server_sent_event_error( - format!("Unable to serialize to JSON {}", e), - )), + .then( + |task_spawner: TaskSpawner, sse_component: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + if let Some(logging_components) = sse_component { + // Build a JSON stream + let s = BroadcastStream::new(logging_components.sender.subscribe()).map( + |msg| { + match msg { + Ok(data) => { + // Serialize to json + match data.to_json_string() { + // Send the json as a Server Side Event + Ok(json) => Ok(Event::default().data(json)), + Err(e) => { + Err(warp_utils::reject::server_sent_event_error( + format!("Unable to serialize to JSON {}", e), + )) + } + } } + Err(e) => Err(warp_utils::reject::server_sent_event_error( + format!("Unable to receive event {}", e), + )), } - Err(e) => Err(warp_utils::reject::server_sent_event_error( - format!("Unable to receive event {}", e), - )), - } - }); + }, + ); - Ok::<_, warp::Rejection>(warp::sse::reply(warp::sse::keep_alive().stream(s))) - } else { - Err(warp_utils::reject::custom_server_error( - "SSE Logging is not enabled".to_string(), - )) - } - }) - }); + Ok::<_, warp::Rejection>(warp::sse::reply( + warp::sse::keep_alive().stream(s), + )) + } else { + Err(warp_utils::reject::custom_server_error( + "SSE Logging is not enabled".to_string(), + )) + } + }) + }, + ); // Define the ultimate set of routes that will be provided to the server. // Use `uor` rather than `or` in order to simplify types (see `UnifyingOrFilter`). @@ -3839,14 +4567,25 @@ pub fn serve( .uor(get_lighthouse_block_packing_efficiency) .uor(get_lighthouse_merge_readiness) .uor(get_events) + .uor(get_expected_withdrawals) .uor(lighthouse_log_events.boxed()) .recover(warp_utils::reject::handle_rejection), ) .boxed() .uor( warp::post().and( - post_beacon_blocks + warp::header::exact("Content-Type", "application/octet-stream") + // Routes which expect `application/octet-stream` go within this `and`. + .and( + post_beacon_blocks_ssz + .uor(post_beacon_blocks_v2_ssz) + .uor(post_beacon_blinded_blocks_ssz) + .uor(post_beacon_blinded_blocks_v2_ssz), + ) + .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) + .uor(post_beacon_blocks_v2) + .uor(post_beacon_blinded_blocks_v2) .uor(post_beacon_pool_attestations) .uor(post_beacon_pool_attester_slashings) .uor(post_beacon_pool_proposer_slashings) @@ -3863,6 +4602,7 @@ pub fn serve( .uor(post_validator_sync_committee_subscriptions) .uor(post_validator_prepare_beacon_proposer) .uor(post_validator_register_validator) + .uor(post_validator_liveness_epoch) .uor(post_lighthouse_liveness) .uor(post_lighthouse_database_reconstruct) .uor(post_lighthouse_database_historical_blocks) diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 7e946b89e..708df39b4 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -3,7 +3,7 @@ use crate::state_id::StateId; use beacon_chain::{ beacon_proposer_cache::{compute_proposer_duties_from_head, ensure_state_is_in_epoch}, - BeaconChain, BeaconChainError, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY, + BeaconChain, BeaconChainError, BeaconChainTypes, }; use eth2::types::{self as api_types}; use safe_arith::SafeArith; @@ -33,7 +33,7 @@ pub fn proposer_duties( // will equal `current_epoch + 1` let tolerant_current_epoch = chain .slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .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()); diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 1a5d5175b..58524f069 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,13 +1,16 @@ use crate::metrics; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; use beacon_chain::{ - BeaconChain, BeaconChainTypes, BlockError, CountUnrealized, NotifyExecutionLayer, + BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, + NotifyExecutionLayer, }; +use eth2::types::{BroadcastValidation, ErrorMessage}; use execution_layer::ProvenancedPayload; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; +use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -16,51 +19,138 @@ use types::{ AbstractExecPayload, BeaconBlockRef, BlindedPayload, EthSpec, ExecPayload, ExecutionBlockHash, FullPayload, Hash256, SignedBeaconBlock, }; -use warp::Rejection; +use warp::http::StatusCode; +use warp::{reply::Response, Rejection, Reply}; -pub enum ProvenancedBlock { +pub enum ProvenancedBlock> { /// The payload was built using a local EE. - Local(Arc>>), + Local(B, PhantomData), /// The payload was build using a remote builder (e.g., via a mev-boost /// compatible relay). - Builder(Arc>>), + Builder(B, PhantomData), +} + +impl> ProvenancedBlock { + pub fn local(block: B) -> Self { + Self::Local(block, PhantomData) + } + + pub fn builder(block: B) -> Self { + Self::Builder(block, PhantomData) + } } /// Handles a request from the HTTP API for full blocks. -pub async fn publish_block( +pub async fn publish_block>( block_root: Option, - provenanced_block: ProvenancedBlock, + provenanced_block: ProvenancedBlock, chain: Arc>, network_tx: &UnboundedSender>, log: Logger, -) -> Result<(), Rejection> { + validation_level: BroadcastValidation, + duplicate_status_code: StatusCode, +) -> Result { let seen_timestamp = timestamp_now(); let (block, is_locally_built_block) = match provenanced_block { - ProvenancedBlock::Local(block) => (block, true), - ProvenancedBlock::Builder(block) => (block, false), + ProvenancedBlock::Local(block, _) => (block, true), + ProvenancedBlock::Builder(block, _) => (block, false), }; - let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock); + let beacon_block = block.inner(); + let delay = get_block_delay_ms(seen_timestamp, beacon_block.message(), &chain.slot_clock); + debug!(log, "Signed block received in HTTP API"; "slot" => beacon_block.slot()); - debug!( - log, - "Signed block published to HTTP API"; - "slot" => block.slot() - ); + /* actually publish a block */ + let publish_block = move |block: Arc>, + sender, + log, + seen_timestamp| { + let publish_timestamp = timestamp_now(); + let publish_delay = publish_timestamp + .checked_sub(seen_timestamp) + .unwrap_or_else(|| Duration::from_secs(0)); - // Send the block, regardless of whether or not it is valid. The API - // specification is very clear that this is the desired behaviour. + info!(log, "Signed block published to network via HTTP API"; "slot" => block.slot(), "publish_delay" => ?publish_delay); - let message = PubsubMessage::BeaconBlock(block.clone()); - crate::publish_pubsub_message(network_tx, message)?; + let message = PubsubMessage::BeaconBlock(block); + crate::publish_pubsub_message(&sender, message) + .map_err(|_| BeaconChainError::UnableToPublish.into()) + }; - let block_root = block_root.unwrap_or_else(|| block.canonical_root()); + /* if we can form a `GossipVerifiedBlock`, we've passed our basic gossip checks */ + let gossip_verified_block = match block.into_gossip_verified_block(&chain) { + Ok(b) => b, + Err(BlockError::BlockIsAlreadyKnown) => { + // Allow the status code for duplicate blocks to be overridden based on config. + return Ok(warp::reply::with_status( + warp::reply::json(&ErrorMessage { + code: duplicate_status_code.as_u16(), + message: "duplicate block".to_string(), + stacktraces: vec![], + }), + duplicate_status_code, + ) + .into_response()); + } + Err(e) => { + warn!( + log, + "Not publishing block - not gossip verified"; + "slot" => beacon_block.slot(), + "error" => ?e + ); + return Err(warp_utils::reject::custom_bad_request(e.to_string())); + } + }; + + let block_root = block_root.unwrap_or(gossip_verified_block.block_root); + + if let BroadcastValidation::Gossip = validation_level { + publish_block( + beacon_block.clone(), + network_tx.clone(), + log.clone(), + seen_timestamp, + ) + .map_err(|_| warp_utils::reject::custom_server_error("unable to publish".into()))?; + } + + /* only publish if gossip- and consensus-valid and equivocation-free */ + let chain_clone = chain.clone(); + let block_clone = beacon_block.clone(); + let log_clone = log.clone(); + let sender_clone = network_tx.clone(); + + let publish_fn = move || match validation_level { + BroadcastValidation::Gossip => Ok(()), + BroadcastValidation::Consensus => { + publish_block(block_clone, sender_clone, log_clone, seen_timestamp) + } + BroadcastValidation::ConsensusAndEquivocation => { + if chain_clone + .observed_block_producers + .read() + .proposer_has_been_observed(block_clone.message(), block_root) + .map_err(|e| BlockError::BeaconChainError(e.into()))? + .is_slashable() + { + warn!( + log_clone, + "Not publishing equivocating block"; + "slot" => block_clone.slot() + ); + Err(BlockError::Slashable) + } else { + publish_block(block_clone, sender_clone, log_clone, seen_timestamp) + } + } + }; match chain .process_block( block_root, - block.clone(), - CountUnrealized::True, + gossip_verified_block, NotifyExecutionLayer::Yes, + publish_fn, ) .await { @@ -70,14 +160,14 @@ pub async fn publish_block( "Valid block from HTTP API"; "block_delay" => ?delay, "root" => format!("{}", root), - "proposer_index" => block.message().proposer_index(), - "slot" => block.slot(), + "proposer_index" => beacon_block.message().proposer_index(), + "slot" => beacon_block.slot(), ); // Notify the validator monitor. chain.validator_monitor.read().register_api_block( seen_timestamp, - block.message(), + beacon_block.message(), root, &chain.slot_clock, ); @@ -90,40 +180,39 @@ pub async fn publish_block( // blocks built with builders we consider the broadcast time to be // when the blinded block is published to the builder. if is_locally_built_block { - late_block_logging(&chain, seen_timestamp, block.message(), root, "local", &log) + late_block_logging( + &chain, + seen_timestamp, + beacon_block.message(), + root, + "local", + &log, + ) } - - Ok(()) + Ok(warp::reply().into_response()) } - Err(BlockError::BlockIsAlreadyKnown) => { - info!( - log, - "Block from HTTP API already known"; - "block" => ?block.canonical_root(), - "slot" => block.slot(), - ); - Ok(()) - } - Err(BlockError::RepeatProposal { proposer, slot }) => { - warn!( - log, - "Block ignored due to repeat proposal"; - "msg" => "this can happen when a VC uses fallback BNs. \ - whilst this is not necessarily an error, it can indicate issues with a BN \ - or between the VC and BN.", - "slot" => slot, - "proposer" => proposer, - ); - Ok(()) + Err(BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) => { + Err(warp_utils::reject::custom_server_error( + "unable to publish to network channel".to_string(), + )) } + Err(BlockError::Slashable) => Err(warp_utils::reject::custom_bad_request( + "proposal for this slot and proposer has already been seen".to_string(), + )), Err(e) => { - let msg = format!("{:?}", e); - error!( - log, - "Invalid block provided to HTTP API"; - "reason" => &msg - ); - Err(warp_utils::reject::broadcast_without_import(msg)) + if let BroadcastValidation::Gossip = validation_level { + Err(warp_utils::reject::broadcast_without_import(format!("{e}"))) + } else { + let msg = format!("{:?}", e); + error!( + log, + "Invalid block provided to HTTP API"; + "reason" => &msg + ); + Err(warp_utils::reject::custom_bad_request(format!( + "Invalid block: {e}" + ))) + } } } } @@ -135,21 +224,33 @@ pub async fn publish_blinded_block( chain: Arc>, network_tx: &UnboundedSender>, log: Logger, -) -> Result<(), Rejection> { + validation_level: BroadcastValidation, + duplicate_status_code: StatusCode, +) -> Result { let block_root = block.canonical_root(); - let full_block = reconstruct_block(chain.clone(), block_root, block, log.clone()).await?; - publish_block::(Some(block_root), full_block, chain, network_tx, log).await + let full_block: ProvenancedBlock>> = + reconstruct_block(chain.clone(), block_root, block, log.clone()).await?; + publish_block::( + Some(block_root), + full_block, + chain, + network_tx, + log, + validation_level, + duplicate_status_code, + ) + .await } /// 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. -async fn reconstruct_block( +pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: SignedBeaconBlock>, log: Logger, -) -> 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()) @@ -215,15 +316,15 @@ async fn reconstruct_block( None => block .try_into_full_block(None) .map(Arc::new) - .map(ProvenancedBlock::Local), + .map(ProvenancedBlock::local), Some(ProvenancedPayload::Local(full_payload)) => block .try_into_full_block(Some(full_payload)) .map(Arc::new) - .map(ProvenancedBlock::Local), + .map(ProvenancedBlock::local), Some(ProvenancedPayload::Builder(full_payload)) => block .try_into_full_block(Some(full_payload)) .map(Arc::new) - .map(ProvenancedBlock::Builder), + .map(ProvenancedBlock::builder), } .ok_or_else(|| { warp_utils::reject::custom_server_error("Unable to add payload to block".to_string()) diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index 9e4aadef1..1a76333e2 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -70,15 +70,30 @@ impl StateId { .map_err(BeaconChainError::DBError) .map_err(warp_utils::reject::beacon_chain_error)? { - let execution_optimistic = chain - .canonical_head - .fork_choice_read_lock() - .is_optimistic_or_invalid_block_no_fallback(&hot_summary.latest_block_root) - .map_err(BeaconChainError::ForkChoiceError) - .map_err(warp_utils::reject::beacon_chain_error)?; - let finalized = chain - .is_finalized_state(root, hot_summary.slot) + let finalization_status = chain + .state_finalization_and_canonicity(root, hot_summary.slot) .map_err(warp_utils::reject::beacon_chain_error)?; + let finalized = finalization_status.is_finalized(); + let fork_choice = chain.canonical_head.fork_choice_read_lock(); + let execution_optimistic = if finalization_status.slot_is_finalized + && !finalization_status.canonical + { + // This block is permanently orphaned and has likely been pruned from fork + // choice. If it isn't found in fork choice, mark it optimistic to be on the + // safe side. + fork_choice + .is_optimistic_or_invalid_block_no_fallback( + &hot_summary.latest_block_root, + ) + .unwrap_or(true) + } else { + // This block is either old and finalized, or recent and unfinalized, so + // it's safe to fallback to the optimistic status of the finalized block. + fork_choice + .is_optimistic_or_invalid_block(&hot_summary.latest_block_root) + .map_err(BeaconChainError::ForkChoiceError) + .map_err(warp_utils::reject::beacon_chain_error)? + }; return Ok((*root, execution_optimistic, finalized)); } else if let Some(_cold_state_slot) = chain .store diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index c728fbeb1..dcf41429f 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -6,7 +6,7 @@ use beacon_chain::sync_committee_verification::{ }; use beacon_chain::{ validator_monitor::timestamp_now, BeaconChain, BeaconChainError, BeaconChainTypes, - StateSkipConfig, MAXIMUM_GOSSIP_CLOCK_DISPARITY, + StateSkipConfig, }; use eth2::types::{self as api_types}; use lighthouse_network::PubsubMessage; @@ -85,7 +85,7 @@ fn duties_from_state_load( let current_epoch = chain.epoch()?; let tolerant_current_epoch = chain .slot_clock - .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)? .epoch(T::EthSpec::slots_per_epoch()); @@ -304,7 +304,7 @@ pub fn process_signed_contribution_and_proofs( } // If we already know the contribution, don't broadcast it or attempt to // further verify it. Return success. - Err(SyncVerificationError::SyncContributionAlreadyKnown(_)) => continue, + Err(SyncVerificationError::SyncContributionSupersetKnown(_)) => continue, // If we've already seen this aggregator produce an aggregate, just // skip this one. // diff --git a/beacon_node/http_api/src/task_spawner.rs b/beacon_node/http_api/src/task_spawner.rs new file mode 100644 index 000000000..8768e057d --- /dev/null +++ b/beacon_node/http_api/src/task_spawner.rs @@ -0,0 +1,192 @@ +use beacon_processor::{BeaconProcessorSend, BlockingOrAsync, Work, WorkEvent}; +use serde::Serialize; +use std::future::Future; +use tokio::sync::{mpsc::error::TrySendError, oneshot}; +use types::EthSpec; +use warp::reply::{Reply, Response}; + +/// Maps a request to a queue in the `BeaconProcessor`. +#[derive(Clone, Copy)] +pub enum Priority { + /// The highest priority. + P0, + /// The lowest priority. + P1, +} + +impl Priority { + /// Wrap `self` in a `WorkEvent` with an appropriate priority. + fn work_event(&self, process_fn: BlockingOrAsync) -> WorkEvent { + let work = match self { + Priority::P0 => Work::ApiRequestP0(process_fn), + Priority::P1 => Work::ApiRequestP1(process_fn), + }; + WorkEvent { + drop_during_sync: false, + work, + } + } +} + +/// Spawns tasks on the `BeaconProcessor` or directly on the tokio executor. +pub struct TaskSpawner { + /// Used to send tasks to the `BeaconProcessor`. The tokio executor will be + /// used if this is `None`. + beacon_processor_send: Option>, +} + +/// Convert a warp `Rejection` into a `Response`. +/// +/// This function should *always* be used to convert rejections into responses. This prevents warp +/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404 +pub async fn convert_rejection(res: Result) -> Response { + match res { + Ok(response) => response.into_response(), + Err(e) => match warp_utils::reject::handle_rejection(e).await { + Ok(reply) => reply.into_response(), + Err(_) => warp::reply::with_status( + warp::reply::json(&"unhandled error"), + eth2::StatusCode::INTERNAL_SERVER_ERROR, + ) + .into_response(), + }, + } +} + +impl TaskSpawner { + pub fn new(beacon_processor_send: Option>) -> Self { + Self { + beacon_processor_send, + } + } + + /// Executes a "blocking" (non-async) task which returns a `Response`. + pub async fn blocking_response_task(self, priority: Priority, func: F) -> Response + where + F: FnOnce() -> Result + Send + Sync + 'static, + T: Reply + Send + 'static, + { + if let Some(beacon_processor_send) = &self.beacon_processor_send { + // Create a closure that will execute `func` and send the result to + // a channel held by this thread. + let (tx, rx) = oneshot::channel(); + let process_fn = move || { + // Execute the function, collect the return value. + let func_result = func(); + // Send the result down the channel. Ignore any failures; the + // send can only fail if the receiver is dropped. + let _ = tx.send(func_result); + }; + + // Send the function to the beacon processor for execution at some arbitrary time. + let result = send_to_beacon_processor( + beacon_processor_send, + priority, + BlockingOrAsync::Blocking(Box::new(process_fn)), + rx, + ) + .await + .and_then(|x| x); + convert_rejection(result).await + } else { + // There is no beacon processor so spawn a task directly on the + // tokio executor. + convert_rejection(warp_utils::task::blocking_response_task(func).await).await + } + } + + /// Executes a "blocking" (non-async) task which returns a JSON-serializable + /// object. + pub async fn blocking_json_task(self, priority: Priority, func: F) -> Response + where + F: FnOnce() -> Result + Send + Sync + 'static, + T: Serialize + Send + 'static, + { + let func = || func().map(|t| warp::reply::json(&t).into_response()); + self.blocking_response_task(priority, func).await + } + + /// Executes an async task which may return a `Rejection`, which will be converted to a response. + pub async fn spawn_async_with_rejection( + self, + priority: Priority, + func: impl Future> + Send + Sync + 'static, + ) -> Response { + let result = self + .spawn_async_with_rejection_no_conversion(priority, func) + .await; + convert_rejection(result).await + } + + /// Same as `spawn_async_with_rejection` but returning a result with the unhandled rejection. + /// + /// If you call this function you MUST convert the rejection to a response and not let it + /// propagate into Warp's filters. See `convert_rejection`. + pub async fn spawn_async_with_rejection_no_conversion( + self, + priority: Priority, + func: impl Future> + Send + Sync + 'static, + ) -> Result { + if let Some(beacon_processor_send) = &self.beacon_processor_send { + // Create a wrapper future that will execute `func` and send the + // result to a channel held by this thread. + let (tx, rx) = oneshot::channel(); + let process_fn = async move { + // Await the future, collect the return value. + let func_result = func.await; + // Send the result down the channel. Ignore any failures; the + // send can only fail if the receiver is dropped. + let _ = tx.send(func_result); + }; + + // Send the function to the beacon processor for execution at some arbitrary time. + send_to_beacon_processor( + beacon_processor_send, + priority, + BlockingOrAsync::Async(Box::pin(process_fn)), + rx, + ) + .await + .and_then(|x| x) + } else { + // There is no beacon processor so spawn a task directly on the + // tokio executor. + tokio::task::spawn(func) + .await + .map_err(|_| { + warp_utils::reject::custom_server_error("Tokio failed to spawn task".into()) + }) + .and_then(|x| x) + } + } +} + +/// Send a task to the beacon processor and await execution. +/// +/// If the task is not executed, return an `Err` with an error message +/// for the API consumer. +async fn send_to_beacon_processor( + beacon_processor_send: &BeaconProcessorSend, + priority: Priority, + process_fn: BlockingOrAsync, + rx: oneshot::Receiver, +) -> Result { + let error_message = match beacon_processor_send.try_send(priority.work_event(process_fn)) { + Ok(()) => { + match rx.await { + // The beacon processor executed the task and sent a result. + Ok(func_result) => return Ok(func_result), + // The beacon processor dropped the channel without sending a + // result. The beacon processor dropped this task because its + // queues are full or it's shutting down. + Err(_) => "The task did not execute. The server is overloaded or shutting down.", + } + } + Err(TrySendError::Full(_)) => "The task was dropped. The server is overloaded.", + Err(TrySendError::Closed(_)) => "The task was dropped. The server is shutting down.", + }; + + Err(warp_utils::reject::custom_server_error( + error_message.to_string(), + )) +} diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index 9880a8ca6..4ab7f9135 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -5,16 +5,14 @@ use beacon_chain::{ }, BeaconChain, BeaconChainTypes, }; +use beacon_processor::{BeaconProcessor, BeaconProcessorChannels, BeaconProcessorConfig}; use directory::DEFAULT_ROOT_DIR; use eth2::{BeaconNodeHttpClient, Timeouts}; use lighthouse_network::{ discv5::enr::{CombinedKey, EnrBuilder}, - libp2p::{ - core::connection::ConnectionId, - swarm::{ - behaviour::{ConnectionEstablished, FromSwarm}, - NetworkBehaviour, - }, + libp2p::swarm::{ + behaviour::{ConnectionEstablished, FromSwarm}, + ConnectionId, NetworkBehaviour, }, rpc::methods::{MetaData, MetaDataV2}, types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState}, @@ -25,11 +23,11 @@ use network::{NetworkReceivers, NetworkSenders}; use sensitive_url::SensitiveUrl; use slog::Logger; use std::future::Future; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use store::MemoryStore; -use tokio::sync::oneshot; +use task_executor::test_utils::TestRuntime; use types::{ChainSpec, EthSpec}; pub const TCP_PORT: u16 = 42; @@ -42,7 +40,6 @@ pub struct InteractiveTester { pub harness: BeaconChainHarness>, pub client: BeaconNodeHttpClient, pub network_rx: NetworkReceivers, - _server_shutdown: oneshot::Sender<()>, } /// The result of calling `create_api_server`. @@ -51,7 +48,6 @@ pub struct InteractiveTester { pub struct ApiServer> { pub server: SFut, pub listening_socket: SocketAddr, - pub shutdown_tx: oneshot::Sender<()>, pub network_rx: NetworkReceivers, pub local_enr: Enr, pub external_peer_id: PeerId, @@ -99,10 +95,14 @@ impl InteractiveTester { let ApiServer { server, listening_socket, - shutdown_tx: _server_shutdown, network_rx, .. - } = create_api_server(harness.chain.clone(), harness.logger().clone()).await; + } = create_api_server( + harness.chain.clone(), + &harness.runtime, + harness.logger().clone(), + ) + .await; tokio::spawn(server); @@ -120,25 +120,18 @@ impl InteractiveTester { harness, client, network_rx, - _server_shutdown, } } } pub async fn create_api_server( chain: Arc>, + test_runtime: &TestRuntime, log: Logger, ) -> ApiServer> { - // Get a random unused port. - let port = unused_port::unused_tcp4_port().unwrap(); - create_api_server_on_port(chain, log, port).await -} + // Use port 0 to allocate a new unused port. + let port = 0; -pub async fn create_api_server_on_port( - chain: Arc>, - log: Logger, - port: u16, -) -> ApiServer> { let (network_senders, network_receivers) = NetworkSenders::new(); // Default metadata @@ -151,8 +144,6 @@ pub async fn create_api_server_on_port( let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); let network_globals = Arc::new(NetworkGlobals::new( enr.clone(), - Some(TCP_PORT), - None, meta_data, vec![], false, @@ -170,7 +161,7 @@ pub async fn create_api_server_on_port( local_addr: EXTERNAL_ADDR.parse().unwrap(), send_back_addr: EXTERNAL_ADDR.parse().unwrap(), }; - let connection_id = ConnectionId::new(1); + let connection_id = ConnectionId::new_unchecked(1); pm.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, connection_id, @@ -183,36 +174,60 @@ pub async fn create_api_server_on_port( let eth1_service = eth1::Service::new(eth1::Config::default(), log.clone(), 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 + // to fulfill an already in-flight HTTP request, therefore having only + // one worker will result in a deadlock. + max_workers: 2, + ..BeaconProcessorConfig::default() + }; + 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; + BeaconProcessor { + network_globals: network_globals.clone(), + executor: test_runtime.task_executor.clone(), + current_workers: 0, + config: beacon_processor_config, + log: log.clone(), + } + .spawn_manager( + beacon_processor_rx, + work_reprocessing_tx, + work_reprocessing_rx, + None, + chain.slot_clock.clone(), + chain.spec.maximum_gossip_clock_disparity(), + ) + .unwrap(); + let ctx = Arc::new(Context { config: Config { enabled: true, - listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: port, - allow_origin: None, - tls_config: None, - allow_sync_stalled: false, data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR), - spec_fork_name: None, + ..Config::default() }, chain: Some(chain), network_senders: Some(network_senders), network_globals: Some(network_globals), + beacon_processor_send: Some(beacon_processor_send), eth1_service: Some(eth1_service), sse_logging_components: None, log, }); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = crate::serve(ctx, server_shutdown).unwrap(); + let (listening_socket, server) = crate::serve(ctx, test_runtime.task_executor.exit()).unwrap(); ApiServer { server, listening_socket, - shutdown_tx, network_rx: network_receivers, local_enr: enr, external_peer_id: peer_id, diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs new file mode 100644 index 000000000..18e9dbf63 --- /dev/null +++ b/beacon_node/http_api/src/validator.rs @@ -0,0 +1,21 @@ +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use types::*; + +/// 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, BeaconChainError> { + chain + .validator_index(pubkey)? + .filter(|&index| { + state + .validators() + .get(index) + .map_or(false, |v| v.pubkey == *pubkey) + }) + .map(Result::Ok) + .transpose() +} diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs new file mode 100644 index 000000000..96ff37d81 --- /dev/null +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -0,0 +1,1358 @@ +use beacon_chain::{ + test_utils::{AttestationStrategy, BlockStrategy}, + GossipVerifiedBlock, +}; +use eth2::types::{BroadcastValidation, SignedBeaconBlock, SignedBlindedBeaconBlock}; +use http_api::test_utils::InteractiveTester; +use http_api::{publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock}; +use tree_hash::TreeHash; +use types::{Hash256, MainnetEthSpec, Slot}; +use warp::Rejection; +use warp_utils::reject::CustomBadRequest; + +use eth2::reqwest::StatusCode; + +type E = MainnetEthSpec; + +/* + * We have the following test cases, which are duplicated for the blinded variant of the route: + * + * - `broadcast_validation=gossip` + * - Invalid (400) + * - Full Pass (200) + * - Partial Pass (202) + * - `broadcast_validation=consensus` + * - Invalid (400) + * - Only gossip (400) + * - Only consensus pass (i.e., equivocates) (200) + * - Full pass (200) + * - `broadcast_validation=consensus_and_equivocation` + * - Invalid (400) + * - Invalid due to early equivocation (400) + * - Only gossip (400) + * - Only consensus (400) + * - Pass (200) + * + */ + +/// 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() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_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 + .client + .post_beacon_blocks_v2(&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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_partial_pass() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::random() + }) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + assert!(response.is_err()); + + let error_response = response.unwrap_err(); + + assert_eq!(error_response.status(), Some(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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_full_pass() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn gossip_full_pass_ssz() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2_ssz(&block, validation_level) + .await; + + assert!(response.is_ok()); + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_invalid() { + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_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 + .client + .post_beacon_blocks_v2(&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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_partial_pass_only_consensus() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); + assert!(gossip_block_a.is_err()); + + /* submit `block_b` which should induce equivocation */ + let channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result = publish_block( + None, + ProvenancedBlock::local(gossip_block_b.unwrap()), + tester.harness.chain.clone(), + &channel.0, + test_logger, + validation_level.unwrap(), + StatusCode::ACCEPTED, + ) + .await; + + assert!(publication_result.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block_b.canonical_root())); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn consensus_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_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 + .client + .post_beacon_blocks_v2(&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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_consensus_early_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + /* submit `block_a` as valid */ + assert!(tester + .client + .post_beacon_blocks_v2(&block_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 + .client + .post_beacon_blocks_v2(&block_b, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective but that equivocates **late** is rejected when using `broadcast_validation=consensus_and_equivocation`. +/// +/// This test is unique in that we can't actually test the HTTP API directly, but instead have to hook into the `publish_blocks` code manually. This is in order to handle the late equivocation case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_consensus_late_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a.clone(), slot_b).await; + let (block_b, state_after_b): (SignedBeaconBlock, _) = + tester.harness.make_block(state_a, slot_b).await; + + /* check for `make_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); + assert!(gossip_block_b.is_ok()); + let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); + assert!(gossip_block_a.is_err()); + + let channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result = publish_block( + None, + ProvenancedBlock::local(gossip_block_b.unwrap()), + tester.harness.chain, + &channel.0, + test_logger, + validation_level.unwrap(), + StatusCode::ACCEPTED, + ) + .await; + + assert!(publication_result.is_err()); + + let publication_error = publication_result.unwrap_err(); + + assert!(publication_error.find::().is_some()); + + assert_eq!( + *publication_error.find::().unwrap().0, + "proposal for this slot and proposer has already been seen".to_string() + ); +} + +/// 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn equivocation_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester.harness.make_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_invalid() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is valid from a gossip perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_partial_pass() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero() + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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)); +} + +// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_full_pass() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = 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())); +} + +// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=gossip`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_gossip_full_pass_ssz() { + /* this test targets gossip-level validation */ + 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2_ssz(&block, validation_level) + .await; + + assert!(response.is_ok()); + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// This test checks that a block that is only valid from a gossip perspective is rejected when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_consensus_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = Some(BroadcastValidation::Consensus); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = 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())); +} + +/// This test checks that a block that is **invalid** from a gossip perspective gets rejected when using `broadcast_validation=consensus_and_equivocation`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_invalid() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let chain_state_before = tester.harness.get_current_state(); + let slot = chain_state_before.slot() + 1; + + tester.harness.advance_slot(); + + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(chain_state_before, slot, |b| { + *b.state_root_mut() = Hash256::zero(); + *b.parent_root_mut() = Hash256::zero(); + }) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: NotFinalizedDescendant { block_parent_root: 0x0000000000000000000000000000000000000000000000000000000000000000 }".to_string()) + ); +} + +/// 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_consensus_early_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBlindedBeaconBlock, _) = tester + .harness + .make_blinded_block(state_a.clone(), slot_b) + .await; + let (block_b, state_after_b): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + /* check for `make_blinded_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + /* submit `block_a` as valid */ + assert!(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())); + + /* submit `block_b` which should induce equivocation */ + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blinded_blocks_v2(&block_b, validation_level) + .await; + assert!(response.is_err()); + + let error_response: eth2::Error = response.err().unwrap(); + + assert_eq!(error_response.status(), Some(StatusCode::BAD_REQUEST)); + + assert!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_gossip() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBeaconBlock, _) = tester + .harness + .make_block_with_modifier(state_a, slot_b, |b| *b.state_root_mut() = Hash256::zero()) + .await; + + let blinded_block: SignedBlindedBeaconBlock = block.into(); + + let response: Result<(), eth2::Error> = tester + .client + .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!( + matches!(error_response, eth2::Error::ServerMessage(err) if err.message == "BAD_REQUEST: Invalid block: StateRootMismatch { block: 0x0000000000000000000000000000000000000000000000000000000000000000, local: 0xfc675d642ff7a06458eb33c7d7b62a5813e34d1b2bb1aee3e395100b579da026 }".to_string()) + ); +} + +/// This test checks that a block that is valid from both a gossip and consensus perspective but that equivocates **late** is rejected when using `broadcast_validation=consensus_and_equivocation`. +/// +/// This test is unique in that we can't actually test the HTTP API directly, but instead have to hook into the `publish_blocks` code manually. This is in order to handle the late equivocation case. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_consensus_late_equivocation() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + let test_logger = tester.harness.logger().clone(); + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block_a, state_after_a): (SignedBlindedBeaconBlock, _) = tester + .harness + .make_blinded_block(state_a.clone(), slot_b) + .await; + let (block_b, state_after_b): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + /* check for `make_blinded_block` curios */ + assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); + assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); + assert_ne!(block_a.state_root(), block_b.state_root()); + + let unblinded_block_a = reconstruct_block( + tester.harness.chain.clone(), + block_a.state_root(), + block_a, + test_logger.clone(), + ) + .await + .unwrap(); + let unblinded_block_b = reconstruct_block( + tester.harness.chain.clone(), + block_b.clone().state_root(), + block_b.clone(), + test_logger.clone(), + ) + .await + .unwrap(); + + 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 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 channel = tokio::sync::mpsc::unbounded_channel(); + + let publication_result = publish_blinded_block( + block_b, + tester.harness.chain, + &channel.0, + test_logger, + validation_level.unwrap(), + StatusCode::ACCEPTED, + ) + .await; + + assert!(publication_result.is_err()); + + 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`. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +pub async fn blinded_equivocation_full_pass() { + /* this test targets gossip-level validation */ + let validation_level: Option = + Some(BroadcastValidation::ConsensusAndEquivocation); + + // 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 tester = InteractiveTester::::new(None, validator_count).await; + + // Create some chain depth. + tester.harness.advance_slot(); + tester + .harness + .extend_chain( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + tester.harness.advance_slot(); + + let slot_a = Slot::new(num_initial); + let slot_b = slot_a + 1; + + let state_a = tester.harness.get_current_state(); + let (block, _): (SignedBlindedBeaconBlock, _) = + tester.harness.make_blinded_block(state_a, slot_b).await; + + let response: Result<(), eth2::Error> = tester + .client + .post_beacon_blocks_v2(&block, validation_level) + .await; + + assert!(response.is_ok()); + assert!(tester + .harness + .chain + .block_is_known_to_fork_choice(&block.canonical_root())); +} diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 8a3ba887b..0ab3c706e 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -326,11 +326,8 @@ async fn sync_committee_indices_across_fork() { /// Assert that an HTTP API error has the given status code and indexed errors for the given indices. fn assert_server_indexed_error(error: eth2::Error, status_code: u16, indices: Vec) { - let eth2::Error::ServerIndexedMessage(IndexedErrorMessage { - code, - failures, - .. - }) = error else { + let eth2::Error::ServerIndexedMessage(IndexedErrorMessage { code, failures, .. }) = error + else { panic!("wrong error, expected ServerIndexedMessage, got: {error:?}") }; assert_eq!(code, status_code); diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index da9241974..d7ea7c262 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -2,8 +2,9 @@ use beacon_chain::{ chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, + ChainConfig, }; -use eth2::types::DepositContractData; +use eth2::types::{DepositContractData, StateId}; use execution_layer::{ForkchoiceState, PayloadAttributes}; use http_api::test_utils::InteractiveTester; use parking_lot::Mutex; @@ -17,7 +18,7 @@ use std::time::Duration; use tree_hash::TreeHash; use types::{ Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, - MainnetEthSpec, ProposerPreparationData, Slot, + MainnetEthSpec, MinimalEthSpec, ProposerPreparationData, Slot, }; type E = MainnetEthSpec; @@ -48,6 +49,76 @@ async fn deposit_contract_custom_network() { assert_eq!(result, expected); } +// Test that state lookups by root function correctly for states that are finalized but still +// present in the hot database, and have had their block pruned from fork choice. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn state_by_root_pruned_from_fork_choice() { + type E = MinimalEthSpec; + + let validator_count = 24; + let spec = ForkName::latest().make_genesis_spec(E::default_spec()); + + let tester = InteractiveTester::::new_with_initializer_and_mutator( + Some(spec.clone()), + validator_count, + Some(Box::new(move |builder| { + builder + .deterministic_keypairs(validator_count) + .fresh_ephemeral_store() + .chain_config(ChainConfig { + epochs_per_migration: 1024, + ..ChainConfig::default() + }) + })), + None, + ) + .await; + + let client = &tester.client; + let harness = &tester.harness; + + // Create some chain depth and finalize beyond fork choice's pruning depth. + let num_epochs = 8_u64; + let num_initial = num_epochs * E::slots_per_epoch(); + harness.advance_slot(); + harness + .extend_chain_with_sync( + num_initial as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + SyncCommitteeStrategy::NoValidators, + ) + .await; + + // Should now be finalized. + let finalized_epoch = harness.finalized_checkpoint().epoch; + assert_eq!(finalized_epoch, num_epochs - 2); + + // The split slot should still be at 0. + assert_eq!(harness.chain.store.get_split_slot(), 0); + + // States that are between the split and the finalized slot should be able to be looked up by + // state root. + for slot in 0..finalized_epoch.start_slot(E::slots_per_epoch()).as_u64() { + let state_root = harness + .chain + .state_root_at_slot(Slot::new(slot)) + .unwrap() + .unwrap(); + let response = client + .get_debug_beacon_states::(StateId::Root(state_root)) + .await + .unwrap() + .unwrap(); + + assert!(response.finalized.unwrap()); + assert!(!response.execution_optimistic.unwrap()); + + let mut state = response.data; + assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); + } +} + /// Data structure for tracking fork choice updates received by the mock execution layer. #[derive(Debug, Default)] struct ForkChoiceUpdates { diff --git a/beacon_node/http_api/tests/main.rs b/beacon_node/http_api/tests/main.rs index f5916d850..e0636424e 100644 --- a/beacon_node/http_api/tests/main.rs +++ b/beacon_node/http_api/tests/main.rs @@ -1,5 +1,6 @@ #![cfg(not(debug_assertions))] // Tests are too slow in debug. +pub mod broadcast_validation_tests; pub mod fork_tests; pub mod interactive_tests; pub mod status_tests; diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index ce725b75a..95f885faa 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -3,6 +3,7 @@ use beacon_chain::{ test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy}, BlockError, }; +use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; use http_api::test_utils::InteractiveTester; use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot}; @@ -143,3 +144,82 @@ async fn el_error_on_new_payload() { assert_eq!(api_response.is_optimistic, Some(false)); assert_eq!(api_response.is_syncing, false); } + +/// Check `node health` endpoint when the EL is offline. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_offline() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL offline + mock_el.server.set_syncing_response(Err("offline".into())); + mock_el.el.upcheck().await; + + let status = tester.client.get_node_health().await; + match status { + Ok(_) => { + panic!("should return 503 error status code"); + } + Err(e) => { + assert_eq!(e.status().unwrap(), 503); + } + } +} + +/// Check `node health` endpoint when the EL is online and synced. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_online_and_synced() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL synced + mock_el.server.set_syncing_response(Ok(false)); + mock_el.el.upcheck().await; + + let status = tester.client.get_node_health().await; + match status { + Ok(response) => { + assert_eq!(response, StatusCode::OK); + } + Err(_) => { + panic!("should return 200 status code"); + } + } +} + +/// Check `node health` endpoint when the EL is online but not synced. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_health_el_online_and_not_synced() { + let num_blocks = E::slots_per_epoch() / 2; + let num_validators = E::slots_per_epoch(); + let tester = post_merge_tester(num_blocks, num_validators).await; + let harness = &tester.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + + // EL not synced + harness.advance_slot(); + mock_el.server.all_payloads_syncing(true); + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let status = tester.client.get_node_health().await; + match status { + Ok(response) => { + assert_eq!(response, StatusCode::PARTIAL_CONTENT); + } + Err(_) => { + panic!("should return 206 status code"); + } + } +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a6c49ddae..1fbdab07c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1,7 +1,7 @@ use beacon_chain::test_utils::RelativeSyncCommittee; use beacon_chain::{ test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, - BeaconChain, StateSkipConfig, WhenSlotSkipped, MAXIMUM_GOSSIP_CLOCK_DISPARITY, + BeaconChain, ChainConfig, StateSkipConfig, WhenSlotSkipped, }; use environment::null_logger; use eth2::{ @@ -10,15 +10,14 @@ use eth2::{ types::{BlockId as CoreBlockId, ForkChoiceNode, StateId as CoreStateId, *}, BeaconNodeHttpClient, Error, StatusCode, Timeouts, }; -use execution_layer::test_utils::TestingBuilder; -use execution_layer::test_utils::DEFAULT_BUILDER_THRESHOLD_WEI; use execution_layer::test_utils::{ - Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, + MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_BUILDER_THRESHOLD_WEI, + DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, }; use futures::stream::{Stream, StreamExt}; use futures::FutureExt; use http_api::{ - test_utils::{create_api_server, create_api_server_on_port, ApiServer}, + test_utils::{create_api_server, ApiServer}, BlockId, StateId, }; use lighthouse_network::{Enr, EnrExt, PeerId}; @@ -28,9 +27,9 @@ use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use state_processing::per_block_processing::get_expected_withdrawals; use state_processing::per_slot_processing; +use state_processing::state_advance::partial_state_advance; use std::convert::TryInto; use std::sync::Arc; -use tokio::sync::oneshot; use tokio::time::Duration; use tree_hash::TreeHash; use types::application_domain::ApplicationDomain; @@ -70,15 +69,15 @@ struct ApiTester { attester_slashing: AttesterSlashing, proposer_slashing: ProposerSlashing, voluntary_exit: SignedVoluntaryExit, - _server_shutdown: oneshot::Sender<()>, network_rx: NetworkReceivers, local_enr: Enr, external_peer_id: PeerId, - mock_builder: Option>>, + mock_builder: Option>>, } struct ApiTesterConfig { spec: ChainSpec, + retain_historic_states: bool, builder_threshold: Option, } @@ -88,11 +87,19 @@ impl Default for ApiTesterConfig { spec.shard_committee_period = 2; Self { spec, + retain_historic_states: false, builder_threshold: None, } } } +impl ApiTesterConfig { + fn retain_historic_states(mut self) -> Self { + self.retain_historic_states = true; + self + } +} + impl ApiTester { pub async fn new() -> Self { // This allows for testing voluntary exits without building out a massive chain. @@ -112,20 +119,28 @@ impl ApiTester { } pub async fn new_from_config(config: ApiTesterConfig) -> Self { - // Get a random unused port let spec = config.spec; - let port = unused_port::unused_tcp4_port().unwrap(); - let beacon_url = SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(); - let harness = Arc::new( - BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec.clone()) - .logger(logging::test_logger()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .mock_execution_layer_with_builder(beacon_url.clone(), config.builder_threshold) - .build(), - ); + let mut harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone()) + .chain_config(ChainConfig { + reconstruct_historic_states: config.retain_historic_states, + ..ChainConfig::default() + }) + .logger(logging::test_logger()) + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer_with_config(config.builder_threshold) + .build(); + + harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .execution_block_generator() + .move_to_terminal_block() + .unwrap(); harness.advance_slot(); @@ -159,7 +174,7 @@ impl ApiTester { // `make_block` adds random graffiti, so this will produce an alternate block let (reorg_block, _reorg_state) = harness - .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap()) + .make_block(head.beacon_state.clone(), harness.chain.slot().unwrap() + 1) .await; let head_state_root = head.beacon_state_root(); @@ -233,30 +248,40 @@ impl ApiTester { let ApiServer { server, - listening_socket: _, - shutdown_tx, + listening_socket, network_rx, local_enr, external_peer_id, - } = create_api_server_on_port(chain.clone(), log, port).await; + } = create_api_server(chain.clone(), &harness.runtime, log).await; harness.runtime.task_executor.spawn(server, "api_server"); + // Late-initalize the mock builder now that the mock execution node and beacon API ports + // have been allocated. + 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()); + + // Start the mock builder service prior to building the chain out. + harness.runtime.task_executor.spawn( + async move { + if let Err(e) = mock_builder_server.await { + panic!("error in mock builder server: {e:?}"); + } + }, + "mock_builder_server", + ); + + let mock_builder = harness.mock_builder.clone(); + let client = BeaconNodeHttpClient::new( beacon_url, Timeouts::set_all(Duration::from_secs(SECONDS_PER_SLOT)), ); - let builder_ref = harness.mock_builder.as_ref().unwrap().clone(); - harness.runtime.task_executor.spawn( - async move { builder_ref.run().await }, - "mock_builder_server", - ); - - let mock_builder = harness.mock_builder.clone(); - Self { - harness, + harness: Arc::new(harness), chain, client, next_block, @@ -266,7 +291,6 @@ impl ApiTester { attester_slashing, proposer_slashing, voluntary_exit, - _server_shutdown: shutdown_tx, network_rx, local_enr, external_peer_id, @@ -320,11 +344,10 @@ impl ApiTester { let ApiServer { server, listening_socket, - shutdown_tx, network_rx, local_enr, external_peer_id, - } = create_api_server(chain.clone(), log).await; + } = create_api_server(chain.clone(), &harness.runtime, log).await; harness.runtime.task_executor.spawn(server, "api_server"); @@ -349,7 +372,6 @@ impl ApiTester { attester_slashing, proposer_slashing, voluntary_exit, - _server_shutdown: shutdown_tx, network_rx, local_enr, external_peer_id, @@ -371,7 +393,6 @@ impl ApiTester { .mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_BUILDER_THRESHOLD_WEI, ))); @@ -381,6 +402,7 @@ impl ApiTester { pub async fn new_mev_tester_no_builder_threshold() -> Self { let mut config = ApiTesterConfig { builder_threshold: Some(0), + retain_historic_states: false, spec: E::default_spec(), }; config.spec.altair_fork_epoch = Some(Epoch::new(0)); @@ -393,7 +415,6 @@ impl ApiTester { .mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, ))); @@ -1247,20 +1268,125 @@ impl ApiTester { self } - pub async fn test_post_beacon_blocks_invalid(mut self) -> Self { - let mut next_block = self.next_block.clone(); - *next_block.message_mut().proposer_index_mut() += 1; + pub async fn test_post_beacon_blocks_ssz_valid(mut self) -> Self { + let next_block = &self.next_block; - assert!(self.client.post_beacon_blocks(&next_block).await.is_err()); + self.client + .post_beacon_blocks_ssz(next_block) + .await + .unwrap(); assert!( self.network_rx.network_recv.recv().await.is_some(), - "invalid blocks should be sent to network" + "valid blocks should be sent to network" ); self } + pub async fn test_post_beacon_blocks_invalid(mut self) -> Self { + let block = self + .harness + .make_block_with_modifier( + self.harness.get_current_state(), + self.harness.get_current_slot(), + |b| { + *b.state_root_mut() = Hash256::zero(); + }, + ) + .await + .0; + + assert!(self.client.post_beacon_blocks(&block).await.is_err()); + + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "gossip valid blocks should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_blocks_ssz_invalid(mut self) -> Self { + let block = self + .harness + .make_block_with_modifier( + self.harness.get_current_state(), + self.harness.get_current_slot(), + |b| { + *b.state_root_mut() = Hash256::zero(); + }, + ) + .await + .0; + + assert!(self.client.post_beacon_blocks_ssz(&block).await.is_err()); + + assert!( + self.network_rx.network_recv.recv().await.is_some(), + "gossip valid blocks should be sent to network" + ); + + self + } + + pub async fn test_post_beacon_blocks_duplicate(self) -> Self { + let block = self + .harness + .make_block( + self.harness.get_current_state(), + self.harness.get_current_slot(), + ) + .await + .0; + + assert!(self.client.post_beacon_blocks(&block).await.is_ok()); + + let blinded_block = 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).await.unwrap_err(), + self.client + .post_beacon_blocks_v2(&block, None) + .await + .unwrap_err(), + self.client + .post_beacon_blocks_ssz(&block) + .await + .unwrap_err(), + self.client + .post_beacon_blocks_v2_ssz(&block, None) + .await + .unwrap_err(), + self.client + .post_beacon_blinded_blocks(&blinded_block) + .await + .unwrap_err(), + self.client + .post_beacon_blinded_blocks_v2(&blinded_block, None) + .await + .unwrap_err(), + self.client + .post_beacon_blinded_blocks_ssz(&blinded_block) + .await + .unwrap_err(), + self.client + .post_beacon_blinded_blocks_v2_ssz(&blinded_block, None) + .await + .unwrap_err(), + ]; + for (i, response) in responses.into_iter().enumerate() { + assert_eq!( + response.status().unwrap(), + StatusCode::ACCEPTED, + "response {i}" + ); + } + + self + } + pub async fn test_beacon_blocks(self) -> Self { for block_id in self.interesting_block_ids() { let expected = block_id @@ -1753,9 +1879,15 @@ impl ApiTester { } pub async fn test_get_node_health(self) -> Self { - let status = self.client.get_node_health().await.unwrap(); - assert_eq!(status, StatusCode::OK); - + let status = self.client.get_node_health().await; + match status { + Ok(_) => { + panic!("should return 503 error status code"); + } + Err(e) => { + assert_eq!(e.status().unwrap(), 503); + } + } self } @@ -2259,7 +2391,9 @@ impl ApiTester { .unwrap(); self.chain.slot_clock.set_current_time( - current_epoch_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY - Duration::from_millis(1), + current_epoch_start + - self.chain.spec.maximum_gossip_clock_disparity() + - Duration::from_millis(1), ); let dependent_root = self @@ -2296,9 +2430,9 @@ impl ApiTester { "should not get attester duties outside of tolerance" ); - self.chain - .slot_clock - .set_current_time(current_epoch_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY); + self.chain.slot_clock.set_current_time( + current_epoch_start - self.chain.spec.maximum_gossip_clock_disparity(), + ); self.client .get_validator_duties_proposer(current_epoch) @@ -2379,6 +2513,70 @@ impl ApiTester { self } + pub async fn test_block_production_ssz(self) -> Self { + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let block_bytes = self + .client + .get_validator_blocks_ssz::>(slot, &randao_reveal, None) + .await + .unwrap() + .expect("block bytes"); + + let block = + BeaconBlock::>::from_ssz_bytes(&block_bytes, &self.chain.spec) + .expect("block bytes can be decoded"); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + + self.client + .post_beacon_blocks_ssz(&signed_block) + .await + .unwrap(); + + assert_eq!(self.chain.head_beacon_block().as_ref(), &signed_block); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -2522,6 +2720,69 @@ impl ApiTester { } } + pub async fn test_blinded_block_production_ssz>(&self) { + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let block_bytes = self + .client + .get_validator_blinded_blocks_ssz::(slot, &randao_reveal, None) + .await + .unwrap() + .expect("block bytes"); + + let block = BeaconBlock::::from_ssz_bytes(&block_bytes, &self.chain.spec) + .expect("block bytes can be decoded"); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + + self.client + .post_beacon_blinded_blocks_ssz(&signed_block) + .await + .unwrap(); + + // This converts the generic `Payload` to a concrete type for comparison. + let head_block = SignedBeaconBlock::from(signed_block.clone()); + assert_eq!(head_block, signed_block); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + } + pub async fn test_blinded_block_production_no_verify_randao>( self, ) -> Self { @@ -2965,6 +3226,69 @@ impl ApiTester { self } + pub async fn test_post_validator_liveness_epoch(self) -> Self { + let epoch = self.chain.epoch().unwrap(); + let head_state = self.chain.head_beacon_state_cloned(); + let indices = (0..head_state.validators().len()) + .map(|i| i as u64) + .collect::>(); + + // Construct the expected response + let expected: Vec = head_state + .validators() + .iter() + .enumerate() + .map(|(index, _)| StandardLivenessResponseData { + index: index as u64, + is_live: false, + }) + .collect(); + + let result = self + .client + .post_validator_liveness_epoch(epoch, indices.clone()) + .await + .unwrap() + .data; + + assert_eq!(result, expected); + + // Attest to the current slot + self.client + .post_beacon_pool_attestations(self.attestations.as_slice()) + .await + .unwrap(); + + let result = self + .client + .post_validator_liveness_epoch(epoch, indices.clone()) + .await + .unwrap() + .data; + + let committees = head_state + .get_beacon_committees_at_slot(self.chain.slot().unwrap()) + .unwrap(); + let attesting_validators: Vec = committees + .into_iter() + .flat_map(|committee| committee.committee.iter().cloned()) + .collect(); + // All attesters should now be considered live + let expected = expected + .into_iter() + .map(|mut a| { + if attesting_validators.contains(&(a.index as usize)) { + a.is_live = true; + } + a + }) + .collect::>(); + + assert_eq!(result, expected); + + self + } + // Helper function for tests that require a valid RANDAO signature. async fn get_test_randao(&self, slot: Slot, epoch: Epoch) -> (u64, SignatureBytes) { let fork = self.chain.canonical_head.cached_head().head_fork(); @@ -3030,6 +3354,7 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + self } @@ -3038,7 +3363,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::GasLimit(30_000_000)); let slot = self.chain.slot().unwrap(); @@ -3081,7 +3405,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::FeeRecipient(test_fee_recipient)); let slot = self.chain.slot().unwrap(); @@ -3123,7 +3446,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::ParentHash(invalid_parent_hash)); let slot = self.chain.slot().unwrap(); @@ -3172,7 +3494,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::PrevRandao(invalid_prev_randao)); let slot = self.chain.slot().unwrap(); @@ -3217,7 +3538,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::BlockNumber(invalid_block_number)); let slot = self.chain.slot().unwrap(); @@ -3264,7 +3584,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Timestamp(invalid_timestamp)); let slot = self.chain.slot().unwrap(); @@ -3304,11 +3623,7 @@ impl ApiTester { } pub async fn test_payload_rejects_invalid_signature(self) -> Self { - self.mock_builder - .as_ref() - .unwrap() - .builder - .invalid_signatures(); + self.mock_builder.as_ref().unwrap().invalid_signatures(); let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -3586,7 +3901,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_BUILDER_THRESHOLD_WEI - 1, ))); @@ -3623,7 +3937,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, ))); @@ -3660,7 +3973,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, ))); @@ -3697,7 +4009,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1, ))); @@ -3734,7 +4045,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, ))); @@ -3751,7 +4061,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::WithdrawalsRoot(withdrawals_root)); let epoch = self.chain.epoch().unwrap(); @@ -3784,7 +4093,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::Value(Uint256::from( DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, ))); @@ -3792,7 +4100,6 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .builder .add_operation(Operation::WithdrawalsRoot(Hash256::repeat_byte(0x42))); let slot = self.chain.slot().unwrap(); @@ -4126,7 +4433,7 @@ impl ApiTester { .unwrap(); let expected_reorg = EventKind::ChainReorg(SseChainReorg { - slot: self.next_block.slot(), + slot: self.reorg_block.slot(), depth: 1, old_head_block: self.next_block.canonical_root(), old_head_state: self.next_block.state_root(), @@ -4136,6 +4443,8 @@ impl ApiTester { execution_optimistic: false, }); + self.harness.advance_slot(); + self.client .post_beacon_blocks(&self.reorg_block) .await @@ -4152,6 +4461,72 @@ impl ApiTester { self } + pub async fn test_get_expected_withdrawals_invalid_state(self) -> Self { + let state_id = CoreStateId::Root(Hash256::zero()); + + let result = self.client.get_expected_withdrawals(&state_id).await; + + match result { + Err(e) => { + assert_eq!(e.status().unwrap(), 404); + } + _ => panic!("query did not fail correctly"), + } + + self + } + + pub async fn test_get_expected_withdrawals_capella(self) -> Self { + let slot = self.chain.slot().unwrap(); + let state_id = CoreStateId::Slot(slot); + + // calculate the expected withdrawals + let (mut state, _, _) = StateId(state_id).state(&self.chain).unwrap(); + let proposal_slot = state.slot() + 1; + let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch()); + let (state_root, _, _) = StateId(state_id).root(&self.chain).unwrap(); + if proposal_epoch != state.current_epoch() { + let _ = partial_state_advance( + &mut state, + Some(state_root), + proposal_slot, + &self.chain.spec, + ); + } + let expected_withdrawals = get_expected_withdrawals(&state, &self.chain.spec).unwrap(); + + // fetch expected withdrawals from the client + let result = self.client.get_expected_withdrawals(&state_id).await; + match result { + Ok(withdrawal_response) => { + assert_eq!(withdrawal_response.execution_optimistic, Some(false)); + assert_eq!(withdrawal_response.finalized, Some(false)); + assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec()); + } + Err(e) => { + println!("{:?}", e); + panic!("query failed incorrectly"); + } + } + + self + } + + pub async fn test_get_expected_withdrawals_pre_capella(self) -> Self { + let state_id = CoreStateId::Head; + + let result = self.client.get_expected_withdrawals(&state_id).await; + + match result { + Err(e) => { + assert_eq!(e.status().unwrap(), 400); + } + _ => panic!("query did not fail correctly"), + } + + self + } + pub async fn test_get_events_altair(self) -> Self { let topics = vec![EventTopic::ContributionAndProof]; let mut events_future = self @@ -4371,6 +4746,22 @@ async fn post_beacon_blocks_valid() { ApiTester::new().await.test_post_beacon_blocks_valid().await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_blocks_ssz_valid() { + ApiTester::new() + .await + .test_post_beacon_blocks_ssz_valid() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_post_beacon_blocks_ssz_invalid() { + ApiTester::new() + .await + .test_post_beacon_blocks_ssz_invalid() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_beacon_blocks_invalid() { ApiTester::new() @@ -4379,6 +4770,14 @@ async fn post_beacon_blocks_invalid() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_blocks_duplicate() { + ApiTester::new() + .await + .test_post_beacon_blocks_duplicate() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn beacon_pools_post_attestations_valid() { ApiTester::new() @@ -4514,7 +4913,7 @@ async fn get_validator_duties_attester_with_skip_slots() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_validator_duties_proposer() { - ApiTester::new() + ApiTester::new_from_config(ApiTesterConfig::default().retain_historic_states()) .await .test_get_validator_duties_proposer() .await; @@ -4522,7 +4921,7 @@ async fn get_validator_duties_proposer() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_validator_duties_proposer_with_skip_slots() { - ApiTester::new() + ApiTester::new_from_config(ApiTesterConfig::default().retain_historic_states()) .await .skip_slots(E::slots_per_epoch() * 2) .test_get_validator_duties_proposer() @@ -4559,6 +4958,20 @@ async fn block_production_verify_randao_invalid() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_ssz_full_payload() { + ApiTester::new().await.test_block_production_ssz().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_ssz_with_skip_slots() { + ApiTester::new() + .await + .skip_slots(E::slots_per_epoch() * 2) + .test_block_production_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new() @@ -4567,6 +4980,14 @@ async fn blinded_block_production_full_payload_premerge() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn blinded_block_production_ssz_full_payload_premerge() { + ApiTester::new() + .await + .test_blinded_block_production_ssz::>() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_with_skip_slots_full_payload_premerge() { ApiTester::new() @@ -4576,6 +4997,15 @@ async fn blinded_block_production_with_skip_slots_full_payload_premerge() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn blinded_block_production_ssz_with_skip_slots_full_payload_premerge() { + ApiTester::new() + .await + .skip_slots(E::slots_per_epoch() * 2) + .test_blinded_block_production_ssz::>() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_no_verify_randao_full_payload_premerge() { ApiTester::new() @@ -4837,6 +5267,7 @@ async fn builder_payload_chosen_by_profit() { async fn builder_works_post_capella() { let mut config = ApiTesterConfig { builder_threshold: Some(0), + retain_historic_states: false, spec: E::default_spec(), }; config.spec.altair_fork_epoch = Some(Epoch::new(0)); @@ -4853,6 +5284,14 @@ async fn builder_works_post_capella() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_validator_liveness_epoch() { + ApiTester::new() + .await + .test_post_validator_liveness_epoch() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn lighthouse_endpoints() { ApiTester::new() @@ -4892,3 +5331,37 @@ async fn optimistic_responses() { .test_check_optimistic_responses() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn expected_withdrawals_invalid_pre_capella() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_get_expected_withdrawals_pre_capella() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn expected_withdrawals_invalid_state() { + 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)); + ApiTester::new_from_config(config) + .await + .test_get_expected_withdrawals_invalid_state() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn expected_withdrawals_valid_capella() { + 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)); + ApiTester::new_from_config(config) + .await + .test_get_expected_withdrawals_capella() + .await; +} diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index c98f2cb85..f8c93ad8f 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -2,25 +2,25 @@ name = "http_metrics" version = "0.1.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = "0.3.2" -serde = { version = "1.0.116", features = ["derive"] } -slog = "2.5.2" -beacon_chain = { path = "../beacon_chain" } -store = { path = "../store" } -lighthouse_network = { path = "../lighthouse_network" } -slot_clock = { path = "../../common/slot_clock" } -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -lighthouse_version = { path = "../../common/lighthouse_version" } -warp_utils = { path = "../../common/warp_utils" } -malloc_utils = { path = "../../common/malloc_utils" } +warp = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +beacon_chain = { workspace = true } +store = { workspace = true } +lighthouse_network = { workspace = true } +slot_clock = { workspace = true } +lighthouse_metrics = { workspace = true } +lighthouse_version = { workspace = true } +warp_utils = { workspace = true } +malloc_utils = { workspace = true } [dev-dependencies] -tokio = { version = "1.14.0", features = ["sync"] } -reqwest = { version = "0.11.0", features = ["json"] } -environment = { path = "../../lighthouse/environment" } -types = { path = "../../consensus/types" } +tokio = { workspace = true } +reqwest = { workspace = true } +environment = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/http_metrics/src/metrics.rs b/beacon_node/http_metrics/src/metrics.rs index 9b1569421..785206b75 100644 --- a/beacon_node/http_metrics/src/metrics.rs +++ b/beacon_node/http_metrics/src/metrics.rs @@ -1,6 +1,6 @@ use crate::Context; use beacon_chain::BeaconChainTypes; -use lighthouse_metrics::{Encoder, TextEncoder}; +use lighthouse_metrics::TextEncoder; use lighthouse_network::prometheus_client::encoding::text::encode; use malloc_utils::scrape_allocator_metrics; @@ -9,7 +9,7 @@ pub use lighthouse_metrics::*; pub fn gather_prometheus_metrics( ctx: &Context, ) -> std::result::Result { - let mut buffer = vec![]; + let mut buffer = String::new(); let encoder = TextEncoder::new(); // There are two categories of metrics: @@ -50,7 +50,7 @@ pub fn gather_prometheus_metrics( } encoder - .encode(&lighthouse_metrics::gather(), &mut buffer) + .encode_utf8(&lighthouse_metrics::gather(), &mut buffer) .unwrap(); // encode gossipsub metrics also if they exist if let Some(registry) = ctx.gossipsub_registry.as_ref() { @@ -59,5 +59,5 @@ pub fn gather_prometheus_metrics( } } - String::from_utf8(buffer).map_err(|e| format!("Failed to encode prometheus info: {:?}", e)) + Ok(buffer) } diff --git a/beacon_node/http_metrics/tests/tests.rs b/beacon_node/http_metrics/tests/tests.rs index 89fde3237..b88a790af 100644 --- a/beacon_node/http_metrics/tests/tests.rs +++ b/beacon_node/http_metrics/tests/tests.rs @@ -38,7 +38,7 @@ async fn returns_200_ok() { }; let (listening_socket, server) = http_metrics::serve(ctx, server_shutdown).unwrap(); - tokio::spawn(async { server.await }); + tokio::spawn(server); let url = format!( "http://{}:{}/metrics", diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index c1b4d7217..07674fb6d 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -2,62 +2,64 @@ name = "lighthouse_network" version = "0.2.0" authors = ["Sigma Prime "] -edition = "2021" +edition = { workspace = true } [dependencies] -discv5 = { version = "0.2.2", features = ["libp2p"] } -unsigned-varint = { version = "0.6.0", features = ["codec"] } -types = { path = "../../consensus/types" } -ssz_types = "0.5.0" -serde = { version = "1.0.116", features = ["derive"] } -serde_derive = "1.0.116" -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -tree_hash = "0.5.0" -tree_hash_derive = "0.5.0" -slog = { version = "2.5.2", features = ["max_level_trace"] } -lighthouse_version = { path = "../../common/lighthouse_version" } -tokio = { version = "1.14.0", features = ["time", "macros"] } -futures = "0.3.7" -error-chain = "0.12.4" -dirs = "3.0.1" -fnv = "1.0.7" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -smallvec = "1.6.1" -tokio-io-timeout = "1.1.1" -lru = "0.7.1" -lru_cache = { path = "../../common/lru_cache" } -parking_lot = "0.12.0" -sha2 = "0.10" -snap = "1.0.1" -hex = "0.4.2" -tokio-util = { version = "0.6.2", features = ["codec", "compat", "time"] } -tiny-keccak = "2.0.2" -task_executor = { path = "../../common/task_executor" } -rand = "0.8.5" -directory = { path = "../../common/directory" } -regex = "1.5.5" -strum = { version = "0.24.0", features = ["derive"] } -superstruct = "0.5.0" -prometheus-client = "0.18.0" -unused_port = { path = "../../common/unused_port" } -delay_map = "0.3.0" +discv5 = { workspace = true } +unsigned-varint = { version = "0.6", features = ["codec"] } +ssz_types = { workspace = true } +types = { workspace = true } +serde = { workspace = true } +serde_derive = "1" +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +slog = { workspace = true } +lighthouse_version = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +error-chain = { workspace = true } +dirs = { workspace = true } +fnv = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +smallvec = { workspace = true } +tokio-io-timeout = "1" +lru = { workspace = true } +lru_cache = { workspace = true } +parking_lot = { workspace = true } +sha2 = { workspace = true } +snap = { workspace = true } +hex = { workspace = true } +tokio-util = { workspace = true } +tiny-keccak = "2" +task_executor = { workspace = true } +rand = { workspace = true } +directory = { workspace = true } +regex = { workspace = true } +strum = { workspace = true } +superstruct = { workspace = true } +prometheus-client = "0.21.0" +unused_port = { workspace = true } +delay_map = { workspace = true } void = "1" +libp2p-quic= { version = "0.9.2", features=["tokio"]} +libp2p-mplex = "0.40.0" [dependencies.libp2p] -version = "0.50.0" +version = "0.52" default-features = false -features = ["websocket", "identify", "mplex", "yamux", "noise", "gossipsub", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa"] +features = ["identify", "yamux", "noise", "gossipsub", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa"] [dev-dependencies] -slog-term = "2.6.0" -slog-async = "2.5.0" -tempfile = "3.1.0" -exit-future = "0.2.0" -void = "1" -quickcheck = "0.9.2" -quickcheck_macros = "0.9.1" +slog-term = { workspace = true } +slog-async = { workspace = true } +tempfile = { workspace = true } +exit-future = { workspace = true } +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } [features] libp2p-websocket = [] + diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 5a11890a2..c3f6b60b0 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -1,15 +1,12 @@ use crate::listen_addr::{ListenAddr, ListenAddress}; -use crate::rpc::config::OutboundRateLimiterConfig; +use crate::rpc::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; use crate::types::GossipKind; use crate::{Enr, PeerIdSerialized}; use directory::{ DEFAULT_BEACON_NODE_DIR, DEFAULT_HARDCODED_NETWORK, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR, }; use discv5::{Discv5Config, Discv5ConfigBuilder}; -use libp2p::gossipsub::{ - FastMessageId, GossipsubConfig, GossipsubConfigBuilder, GossipsubMessage, MessageId, - RawGossipsubMessage, ValidationMode, -}; +use libp2p::gossipsub; use libp2p::Multiaddr; use serde_derive::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -19,11 +16,6 @@ use std::sync::Arc; use std::time::Duration; use types::{ForkContext, ForkName}; -/// The maximum transmit size of gossip messages in bytes pre-merge. -const GOSSIP_MAX_SIZE: usize = 1_048_576; // 1M -/// The maximum transmit size of gossip messages in bytes post-merge. -const GOSSIP_MAX_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M - /// The cache time is set to accommodate the circulation time of an attestation. /// /// The p2p spec declares that we accept attestations within the following range: @@ -38,20 +30,20 @@ const GOSSIP_MAX_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M /// another 500ms for "fudge factor". pub const DUPLICATE_CACHE_TIME: Duration = Duration::from_secs(33 * 12 + 1); -// We treat uncompressed messages as invalid and never use the INVALID_SNAPPY_DOMAIN as in the -// specification. We leave it here for posterity. -// const MESSAGE_DOMAIN_INVALID_SNAPPY: [u8; 4] = [0, 0, 0, 0]; -const MESSAGE_DOMAIN_VALID_SNAPPY: [u8; 4] = [1, 0, 0, 0]; - /// The maximum size of gossip messages. -pub fn gossip_max_size(is_merge_enabled: bool) -> usize { +pub fn gossip_max_size(is_merge_enabled: bool, gossip_max_size: usize) -> usize { if is_merge_enabled { - GOSSIP_MAX_SIZE_POST_MERGE + gossip_max_size } else { - GOSSIP_MAX_SIZE + gossip_max_size / 10 } } +pub struct GossipsubConfigParams { + pub message_domain_valid_snappy: [u8; 4], + pub gossip_max_size: usize, +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] /// Network configuration for lighthouse. @@ -66,24 +58,30 @@ pub struct Config { /// that no discovery address has been set in the CLI args. pub enr_address: (Option, Option), - /// The udp4 port to broadcast to peers in order to reach back for discovery. + /// The udp ipv4 port to broadcast to peers in order to reach back for discovery. pub enr_udp4_port: Option, - /// The tcp4 port to broadcast to peers in order to reach back for libp2p services. + /// The quic ipv4 port to broadcast to peers in order to reach back for libp2p services. + pub enr_quic4_port: Option, + + /// The tcp ipv4 port to broadcast to peers in order to reach back for libp2p services. pub enr_tcp4_port: Option, - /// The udp6 port to broadcast to peers in order to reach back for discovery. + /// The udp ipv6 port to broadcast to peers in order to reach back for discovery. pub enr_udp6_port: Option, - /// The tcp6 port to broadcast to peers in order to reach back for libp2p services. + /// The tcp ipv6 port to broadcast to peers in order to reach back for libp2p services. pub enr_tcp6_port: Option, + /// The quic ipv6 port to broadcast to peers in order to reach back for libp2p services. + pub enr_quic6_port: Option, + /// Target number of connected peers. pub target_peers: usize, /// Gossipsub configuration parameters. #[serde(skip)] - pub gs_config: GossipsubConfig, + pub gs_config: gossipsub::Config, /// Discv5 configuration parameters. #[serde(skip)] @@ -110,6 +108,9 @@ pub struct Config { /// Disables the discovery protocol from starting. pub disable_discovery: bool, + /// Disables quic support. + pub disable_quic_support: bool, + /// Attempt to construct external port mappings with UPnP. pub upnp_enabled: bool, @@ -148,65 +149,86 @@ pub struct Config { /// Configures if/where invalid blocks should be stored. pub invalid_block_storage: Option, + + /// Configuration for the inbound rate limiter (requests received by this node). + pub inbound_rate_limiter_config: Option, } impl Config { /// Sets the listening address to use an ipv4 address. The discv5 ip_mode and table filter are /// adjusted accordingly to ensure addresses that are present in the enr are globally /// reachable. - pub fn set_ipv4_listening_address(&mut self, addr: Ipv4Addr, tcp_port: u16, udp_port: u16) { + pub fn set_ipv4_listening_address( + &mut self, + addr: Ipv4Addr, + tcp_port: u16, + disc_port: u16, + quic_port: u16, + ) { self.listen_addresses = ListenAddress::V4(ListenAddr { addr, - udp_port, + disc_port, + quic_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip4; + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), disc_port); self.discv5_config.table_filter = |enr| enr.ip4().as_ref().map_or(false, is_global_ipv4) } /// Sets the listening address to use an ipv6 address. The discv5 ip_mode and table filter is /// adjusted accordingly to ensure addresses that are present in the enr are globally /// reachable. - pub fn set_ipv6_listening_address(&mut self, addr: Ipv6Addr, tcp_port: u16, udp_port: u16) { + pub fn set_ipv6_listening_address( + &mut self, + addr: Ipv6Addr, + tcp_port: u16, + disc_port: u16, + quic_port: u16, + ) { self.listen_addresses = ListenAddress::V6(ListenAddr { addr, - udp_port, + disc_port, + quic_port, tcp_port, }); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: false, - }; + + self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), disc_port); self.discv5_config.table_filter = |enr| enr.ip6().as_ref().map_or(false, is_global_ipv6) } /// Sets the listening address to use both an ipv4 and ipv6 address. The discv5 ip_mode and /// table filter is adjusted accordingly to ensure addresses that are present in the enr are /// globally reachable. + #[allow(clippy::too_many_arguments)] pub fn set_ipv4_ipv6_listening_addresses( &mut self, v4_addr: Ipv4Addr, tcp4_port: u16, - udp4_port: u16, + disc4_port: u16, + quic4_port: u16, v6_addr: Ipv6Addr, tcp6_port: u16, - udp6_port: u16, + disc6_port: u16, + quic6_port: u16, ) { self.listen_addresses = ListenAddress::DualStack( ListenAddr { addr: v4_addr, - udp_port: udp4_port, + disc_port: disc4_port, + quic_port: quic4_port, tcp_port: tcp4_port, }, ListenAddr { addr: v6_addr, - udp_port: udp6_port, + disc_port: disc6_port, + quic_port: quic6_port, tcp_port: tcp6_port, }, ); + self.discv5_config.listen_config = discv5::ListenConfig::default() + .with_ipv4(v4_addr, disc4_port) + .with_ipv6(v6_addr, disc6_port); - self.discv5_config.ip_mode = discv5::IpMode::Ip6 { - enable_mapped_addresses: true, - }; self.discv5_config.table_filter = |enr| match (&enr.ip4(), &enr.ip6()) { (None, None) => false, (None, Some(ip6)) => is_global_ipv6(ip6), @@ -219,27 +241,32 @@ impl Config { match listen_addr { ListenAddress::V4(ListenAddr { addr, - udp_port, + disc_port, + quic_port, tcp_port, - }) => self.set_ipv4_listening_address(addr, tcp_port, udp_port), + }) => self.set_ipv4_listening_address(addr, tcp_port, disc_port, quic_port), ListenAddress::V6(ListenAddr { addr, - udp_port, + disc_port, + quic_port, tcp_port, - }) => self.set_ipv6_listening_address(addr, tcp_port, udp_port), + }) => self.set_ipv6_listening_address(addr, tcp_port, disc_port, quic_port), ListenAddress::DualStack( ListenAddr { addr: ip4addr, - udp_port: udp4_port, + disc_port: disc4_port, + quic_port: quic4_port, tcp_port: tcp4_port, }, ListenAddr { addr: ip6addr, - udp_port: udp6_port, + disc_port: disc6_port, + quic_port: quic6_port, tcp_port: tcp6_port, }, ) => self.set_ipv4_ipv6_listening_addresses( - ip4addr, tcp4_port, udp4_port, ip6addr, tcp6_port, udp6_port, + ip4addr, tcp4_port, disc4_port, quic4_port, ip6addr, tcp6_port, disc6_port, + quic6_port, ), } } @@ -263,7 +290,7 @@ impl Default for Config { // Note: Using the default config here. Use `gossipsub_config` function for getting // Lighthouse specific configuration for gossipsub. - let gs_config = GossipsubConfigBuilder::default() + let gs_config = gossipsub::ConfigBuilder::default() .build() .expect("valid gossipsub configuration"); @@ -276,9 +303,18 @@ impl Default for Config { .build() .expect("The total rate limit has been specified"), ); + let listen_addresses = ListenAddress::V4(ListenAddr { + addr: Ipv4Addr::UNSPECIFIED, + disc_port: 9000, + quic_port: 9001, + tcp_port: 9000, + }); + + let discv5_listen_config = + discv5::ListenConfig::from_ip(Ipv4Addr::UNSPECIFIED.into(), 9000); // discv5 configuration - let discv5_config = Discv5ConfigBuilder::new() + let discv5_config = Discv5ConfigBuilder::new(discv5_listen_config) .enable_packet_filter() .session_cache_capacity(5000) .request_timeout(Duration::from_secs(1)) @@ -301,15 +337,13 @@ impl Default for Config { // NOTE: Some of these get overridden by the corresponding CLI default values. Config { network_dir, - listen_addresses: ListenAddress::V4(ListenAddr { - addr: Ipv4Addr::UNSPECIFIED, - udp_port: 9000, - tcp_port: 9000, - }), + listen_addresses, enr_address: (None, None), enr_udp4_port: None, + enr_quic4_port: None, enr_tcp4_port: None, enr_udp6_port: None, + enr_quic6_port: None, enr_tcp6_port: None, target_peers: 50, gs_config, @@ -321,6 +355,7 @@ impl Default for Config { disable_peer_scoring: false, client_version: lighthouse_version::version_with_platform(), disable_discovery: false, + disable_quic_support: false, upnp_enabled: true, network_load: 3, private: false, @@ -333,6 +368,7 @@ impl Default for Config { enable_light_client_server: false, outbound_rate_limiter_config: None, invalid_block_storage: None, + inbound_rate_limiter_config: None, } } } @@ -408,16 +444,20 @@ impl From for NetworkLoad { } /// Return a Lighthouse specific `GossipsubConfig` where the `message_id_fn` depends on the current fork. -pub fn gossipsub_config(network_load: u8, fork_context: Arc) -> GossipsubConfig { +pub fn gossipsub_config( + network_load: u8, + fork_context: Arc, + gossipsub_config_params: GossipsubConfigParams, +) -> gossipsub::Config { // The function used to generate a gossipsub message id // We use the first 8 bytes of SHA256(topic, data) for content addressing - let fast_gossip_message_id = |message: &RawGossipsubMessage| { + let fast_gossip_message_id = |message: &gossipsub::RawMessage| { let data = [message.topic.as_str().as_bytes(), &message.data].concat(); - FastMessageId::from(&Sha256::digest(data)[..8]) + gossipsub::FastMessageId::from(&Sha256::digest(&data)[..8]) }; fn prefix( prefix: [u8; 4], - message: &GossipsubMessage, + message: &gossipsub::Message, fork_context: Arc, ) -> Vec { let topic_bytes = message.topic.as_str().as_bytes(); @@ -441,20 +481,23 @@ pub fn gossipsub_config(network_load: u8, fork_context: Arc) -> Gos } } } - + let message_domain_valid_snappy = gossipsub_config_params.message_domain_valid_snappy; let is_merge_enabled = fork_context.fork_exists(ForkName::Merge); - let gossip_message_id = move |message: &GossipsubMessage| { - MessageId::from( + let gossip_message_id = move |message: &gossipsub::Message| { + gossipsub::MessageId::from( &Sha256::digest( - prefix(MESSAGE_DOMAIN_VALID_SNAPPY, message, fork_context.clone()).as_slice(), + prefix(message_domain_valid_snappy, message, fork_context.clone()).as_slice(), )[..20], ) }; let load = NetworkLoad::from(network_load); - GossipsubConfigBuilder::default() - .max_transmit_size(gossip_max_size(is_merge_enabled)) + gossipsub::ConfigBuilder::default() + .max_transmit_size(gossip_max_size( + is_merge_enabled, + gossipsub_config_params.gossip_max_size, + )) .heartbeat_interval(load.heartbeat_interval) .mesh_n(load.mesh_n) .mesh_n_low(load.mesh_n_low) @@ -466,7 +509,7 @@ pub fn gossipsub_config(network_load: u8, fork_context: Arc) -> Gos .max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large .history_gossip(load.history_gossip) .validate_messages() // require validation before propagation - .validation_mode(ValidationMode::Anonymous) + .validation_mode(gossipsub::ValidationMode::Anonymous) .duplicate_cache_time(DUPLICATE_CACHE_TIME) .message_id_fn(gossip_message_id) .fast_message_id_fn(fast_gossip_message_id) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 938e7cfa2..3f46285a8 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -7,7 +7,7 @@ use super::ENR_FILENAME; use crate::types::{Enr, EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use crate::NetworkConfig; use discv5::enr::EnrKey; -use libp2p::core::identity::Keypair; +use libp2p::identity::Keypair; use slog::{debug, warn}; use ssz::{Decode, Encode}; use ssz_types::BitVector; @@ -17,6 +17,8 @@ use std::path::Path; use std::str::FromStr; use types::{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 attestation subnet bitfield. @@ -133,7 +135,7 @@ pub fn build_or_load_enr( // 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 enr_key = CombinedKey::from_libp2p(local_key)?; let mut local_enr = build_enr::(&enr_key, config, enr_fork_id)?; use_or_load_enr(&enr_key, &mut local_enr, config, log)?; @@ -142,7 +144,7 @@ pub fn build_or_load_enr( pub fn create_enr_builder_from_config( config: &NetworkConfig, - enable_tcp: bool, + enable_libp2p: bool, ) -> EnrBuilder { let mut builder = EnrBuilder::new("v4"); let (maybe_ipv4_address, maybe_ipv6_address) = &config.enr_address; @@ -163,7 +165,28 @@ pub fn create_enr_builder_from_config( builder.udp6(udp6_port); } - if enable_tcp { + if enable_libp2p { + // Add QUIC fields to the ENR. + // Since QUIC is used as an alternative transport for the libp2p protocols, + // the related fields should only be added when both QUIC and libp2p are enabled + if !config.disable_quic_support { + // If we are listening on ipv4, add the quic ipv4 port. + if let Some(quic4_port) = config + .enr_quic4_port + .or_else(|| config.listen_addrs().v4().map(|v4_addr| v4_addr.quic_port)) + { + builder.add_value(QUIC_ENR_KEY, &quic4_port); + } + + // If we are listening on ipv6, add the quic ipv6 port. + if let Some(quic6_port) = config + .enr_quic6_port + .or_else(|| config.listen_addrs().v6().map(|v6_addr| v6_addr.quic_port)) + { + builder.add_value(QUIC6_ENR_KEY, &quic6_port); + } + } + // If the ENR port is not set, and we are listening over that ip version, use the listening port instead. let tcp4_port = config .enr_tcp4_port @@ -213,13 +236,20 @@ pub fn build_enr( fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { // take preference over disk_enr address if one is not specified (local_enr.ip4().is_none() || local_enr.ip4() == disk_enr.ip4()) + && + (local_enr.ip6().is_none() || local_enr.ip6() == disk_enr.ip6()) // tcp ports must match && local_enr.tcp4() == disk_enr.tcp4() + && local_enr.tcp6() == disk_enr.tcp6() + // quic ports must match + && local_enr.quic4() == disk_enr.quic4() + && local_enr.quic6() == disk_enr.quic6() // must match on the same fork && local_enr.get(ETH2_ENR_KEY) == disk_enr.get(ETH2_ENR_KEY) // take preference over disk udp port if one is not specified && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) - // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, + && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) + // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, // otherwise we use a new ENR. This will likely only be true for non-validating nodes && local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) diff --git a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs index e9cca6667..2efaa76ac 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr_ext.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr_ext.rs @@ -1,15 +1,20 @@ //! ENR extension trait to support libp2p integration. + use crate::{Enr, Multiaddr, PeerId}; use discv5::enr::{CombinedKey, CombinedPublicKey}; -use libp2p::core::{identity::Keypair, identity::PublicKey, multiaddr::Protocol}; +use libp2p::core::multiaddr::Protocol; +use libp2p::identity::{ed25519, secp256k1, KeyType, Keypair, PublicKey}; use tiny_keccak::{Hasher, Keccak}; +pub const QUIC_ENR_KEY: &str = "quic"; +pub const QUIC6_ENR_KEY: &str = "quic6"; + /// Extend ENR for libp2p types. pub trait EnrExt { /// The libp2p `PeerId` for the record. fn peer_id(&self) -> PeerId; - /// 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`. + /// Returns a list of multiaddrs if the ENR has an `ip` and one of [`tcp`,`udp`,`quic`] key **or** an `ip6` and one of [`tcp6`,`udp6`,`quic6`]. /// The vector remains empty if these fields are not defined. fn multiaddr(&self) -> Vec; @@ -24,6 +29,15 @@ pub trait EnrExt { /// Returns any multiaddrs that contain the TCP protocol. fn multiaddr_tcp(&self) -> Vec; + + /// Returns any QUIC multiaddrs that are registered in this ENR. + fn multiaddr_quic(&self) -> Vec; + + /// Returns the quic port if one is set. + fn quic4(&self) -> Option; + + /// Returns the quic6 port if one is set. + fn quic6(&self) -> Option; } /// Extend ENR CombinedPublicKey for libp2p types. @@ -35,7 +49,10 @@ pub trait CombinedKeyPublicExt { /// Extend ENR CombinedKey for conversion to libp2p keys. pub trait CombinedKeyExt { /// Converts a libp2p key into an ENR combined key. - fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result; + fn from_libp2p(key: Keypair) -> Result; + + /// Converts a [`secp256k1::Keypair`] into and Enr [`CombinedKey`]. + fn from_secp256k1(key: &secp256k1::Keypair) -> CombinedKey; } impl EnrExt for Enr { @@ -44,7 +61,17 @@ impl EnrExt for Enr { self.public_key().as_peer_id() } - /// 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`. + /// Returns the quic port if one is set. + fn quic4(&self) -> Option { + self.get_decodable(QUIC_ENR_KEY).and_then(Result::ok) + } + + /// Returns the quic6 port if one is set. + fn quic6(&self) -> Option { + self.get_decodable(QUIC6_ENR_KEY).and_then(Result::ok) + } + + /// Returns a list of multiaddrs if the ENR has an `ip` and either a `tcp`, `quic` or `udp` key **or** an `ip6` and either a `tcp6` `quic6` or `udp6`. /// The vector remains empty if these fields are not defined. fn multiaddr(&self) -> Vec { let mut multiaddrs: Vec = Vec::new(); @@ -54,6 +81,12 @@ impl EnrExt for Enr { multiaddr.push(Protocol::Udp(udp)); multiaddrs.push(multiaddr); } + if let Some(quic) = self.quic4() { + let mut multiaddr: Multiaddr = ip.into(); + multiaddr.push(Protocol::Udp(quic)); + multiaddr.push(Protocol::QuicV1); + multiaddrs.push(multiaddr); + } if let Some(tcp) = self.tcp4() { let mut multiaddr: Multiaddr = ip.into(); @@ -68,6 +101,13 @@ impl EnrExt for Enr { multiaddrs.push(multiaddr); } + if let Some(quic6) = self.quic6() { + let mut multiaddr: Multiaddr = ip6.into(); + multiaddr.push(Protocol::Udp(quic6)); + multiaddr.push(Protocol::QuicV1); + multiaddrs.push(multiaddr); + } + if let Some(tcp6) = self.tcp6() { let mut multiaddr: Multiaddr = ip6.into(); multiaddr.push(Protocol::Tcp(tcp6)); @@ -88,14 +128,14 @@ impl EnrExt for Enr { if let Some(udp) = self.udp4() { let mut multiaddr: Multiaddr = ip.into(); multiaddr.push(Protocol::Udp(udp)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } if let Some(tcp) = self.tcp4() { let mut multiaddr: Multiaddr = ip.into(); multiaddr.push(Protocol::Tcp(tcp)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } } @@ -103,14 +143,14 @@ impl EnrExt for Enr { if let Some(udp6) = self.udp6() { let mut multiaddr: Multiaddr = ip6.into(); multiaddr.push(Protocol::Udp(udp6)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } if let Some(tcp6) = self.tcp6() { let mut multiaddr: Multiaddr = ip6.into(); multiaddr.push(Protocol::Tcp(tcp6)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } } @@ -128,7 +168,7 @@ impl EnrExt for Enr { if let Some(tcp) = self.tcp4() { let mut multiaddr: Multiaddr = ip.into(); multiaddr.push(Protocol::Tcp(tcp)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } } @@ -136,7 +176,7 @@ impl EnrExt for Enr { if let Some(tcp6) = self.tcp6() { let mut multiaddr: Multiaddr = ip6.into(); multiaddr.push(Protocol::Tcp(tcp6)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } } @@ -154,7 +194,7 @@ impl EnrExt for Enr { if let Some(udp) = self.udp4() { let mut multiaddr: Multiaddr = ip.into(); multiaddr.push(Protocol::Udp(udp)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); multiaddrs.push(multiaddr); } } @@ -162,7 +202,30 @@ impl EnrExt for Enr { if let Some(udp6) = self.udp6() { let mut multiaddr: Multiaddr = ip6.into(); multiaddr.push(Protocol::Udp(udp6)); - multiaddr.push(Protocol::P2p(peer_id.into())); + multiaddr.push(Protocol::P2p(peer_id)); + multiaddrs.push(multiaddr); + } + } + multiaddrs + } + + /// 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(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); } } @@ -170,7 +233,6 @@ 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`. - /// The vector remains empty if these fields are not defined. fn multiaddr_tcp(&self) -> Vec { let mut multiaddrs: Vec = Vec::new(); if let Some(ip) = self.ip4() { @@ -198,19 +260,17 @@ impl CombinedKeyPublicExt for CombinedPublicKey { fn as_peer_id(&self) -> PeerId { match self { Self::Secp256k1(pk) => { - let pk_bytes = pk.to_bytes(); - let libp2p_pk = libp2p::core::PublicKey::Secp256k1( - libp2p::core::identity::secp256k1::PublicKey::decode(&pk_bytes) - .expect("valid public key"), - ); + let pk_bytes = pk.to_sec1_bytes(); + let libp2p_pk: PublicKey = secp256k1::PublicKey::try_from_bytes(&pk_bytes) + .expect("valid public key") + .into(); PeerId::from_public_key(&libp2p_pk) } Self::Ed25519(pk) => { let pk_bytes = pk.to_bytes(); - let libp2p_pk = libp2p::core::PublicKey::Ed25519( - libp2p::core::identity::ed25519::PublicKey::decode(&pk_bytes) - .expect("valid public key"), - ); + let libp2p_pk: PublicKey = ed25519::PublicKey::try_from_bytes(&pk_bytes) + .expect("valid public key") + .into(); PeerId::from_public_key(&libp2p_pk) } } @@ -218,23 +278,32 @@ impl CombinedKeyPublicExt for CombinedPublicKey { } impl CombinedKeyExt for CombinedKey { - fn from_libp2p(key: &libp2p::core::identity::Keypair) -> Result { - match key { - Keypair::Secp256k1(key) => { + fn from_libp2p(key: Keypair) -> Result { + match key.key_type() { + KeyType::Secp256k1 => { + let key = key.try_into_secp256k1().expect("right key type"); let secret = - discv5::enr::k256::ecdsa::SigningKey::from_bytes(&key.secret().to_bytes()) + discv5::enr::k256::ecdsa::SigningKey::from_slice(&key.secret().to_bytes()) .expect("libp2p key must be valid"); Ok(CombinedKey::Secp256k1(secret)) } - Keypair::Ed25519(key) => { - let ed_keypair = - discv5::enr::ed25519_dalek::SecretKey::from_bytes(&key.encode()[..32]) - .expect("libp2p key must be valid"); + KeyType::Ed25519 => { + let key = key.try_into_ed25519().expect("right key type"); + let ed_keypair = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &(key.to_bytes()[..32]) + .try_into() + .expect("libp2p key must be valid"), + ); Ok(CombinedKey::from(ed_keypair)) } - Keypair::Ecdsa(_) => Err("Ecdsa keypairs not supported"), + _ => Err("Unsupported keypair kind"), } } + fn from_secp256k1(key: &secp256k1::Keypair) -> Self { + let secret = discv5::enr::k256::ecdsa::SigningKey::from_slice(&key.secret().to_bytes()) + .expect("libp2p key must be valid"); + CombinedKey::Secp256k1(secret) + } } // helper function to convert a peer_id to a node_id. This is only possible for secp256k1/ed25519 libp2p @@ -244,48 +313,57 @@ pub fn peer_id_to_node_id(peer_id: &PeerId) -> Result { - let uncompressed_key_bytes = &pk.encode_uncompressed()[1..]; + })?; + + match public_key.key_type() { + KeyType::Secp256k1 => { + let pk = public_key + .clone() + .try_into_secp256k1() + .expect("right key type"); + let uncompressed_key_bytes = &pk.to_bytes_uncompressed()[1..]; let mut output = [0_u8; 32]; let mut hasher = Keccak::v256(); hasher.update(uncompressed_key_bytes); hasher.finalize(&mut output); Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) } - PublicKey::Ed25519(pk) => { - let uncompressed_key_bytes = pk.encode(); + KeyType::Ed25519 => { + let pk = public_key + .clone() + .try_into_ed25519() + .expect("right key type"); + let uncompressed_key_bytes = pk.to_bytes(); let mut output = [0_u8; 32]; let mut hasher = Keccak::v256(); hasher.update(&uncompressed_key_bytes); hasher.finalize(&mut output); Ok(discv5::enr::NodeId::parse(&output).expect("Must be correct length")) } - PublicKey::Ecdsa(_) => Err(format!( - "Unsupported public key (Ecdsa) from peer {}", - peer_id - )), + + _ => Err(format!("Unsupported public key from peer {}", peer_id)), } } #[cfg(test)] mod tests { + use super::*; #[test] fn test_secp256k1_peer_id_conversion() { let sk_hex = "df94a73d528434ce2309abb19c16aedb535322797dbd59c157b1e04095900f48"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_bytes(&sk_bytes).unwrap(); + let secret_key = discv5::enr::k256::ecdsa::SigningKey::from_slice(&sk_bytes).unwrap(); - let libp2p_sk = libp2p::identity::secp256k1::SecretKey::from_bytes(sk_bytes).unwrap(); - let secp256k1_kp: libp2p::identity::secp256k1::Keypair = libp2p_sk.into(); - let libp2p_kp = Keypair::Secp256k1(secp256k1_kp); + let libp2p_sk = secp256k1::SecretKey::try_from_bytes(sk_bytes).unwrap(); + let secp256k1_kp: secp256k1::Keypair = libp2p_sk.into(); + let libp2p_kp: Keypair = secp256k1_kp.into(); let peer_id = libp2p_kp.public().to_peer_id(); let enr = discv5::enr::EnrBuilder::new("v4") @@ -300,16 +378,18 @@ mod tests { fn test_ed25519_peer_conversion() { let sk_hex = "4dea8a5072119927e9d243a7d953f2f4bc95b70f110978e2f9bc7a9000e4b261"; let sk_bytes = hex::decode(sk_hex).unwrap(); - let secret = discv5::enr::ed25519_dalek::SecretKey::from_bytes(&sk_bytes).unwrap(); - let public = discv5::enr::ed25519_dalek::PublicKey::from(&secret); - let keypair = discv5::enr::ed25519_dalek::Keypair { secret, public }; + let secret_key = discv5::enr::ed25519_dalek::SigningKey::from_bytes( + &sk_bytes.clone().try_into().unwrap(), + ); - let libp2p_sk = libp2p::identity::ed25519::SecretKey::from_bytes(sk_bytes).unwrap(); - let ed25519_kp: libp2p::identity::ed25519::Keypair = libp2p_sk.into(); - let libp2p_kp = Keypair::Ed25519(ed25519_kp); + let libp2p_sk = ed25519::SecretKey::try_from_bytes(sk_bytes).unwrap(); + let secp256k1_kp: ed25519::Keypair = libp2p_sk.into(); + let libp2p_kp: Keypair = secp256k1_kp.into(); let peer_id = libp2p_kp.public().to_peer_id(); - let enr = discv5::enr::EnrBuilder::new("v4").build(&keypair).unwrap(); + let enr = discv5::enr::EnrBuilder::new("v4") + .build(&secret_key) + .unwrap(); let node_id = peer_id_to_node_id(&peer_id).unwrap(); assert_eq!(enr.node_id(), node_id); diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 13fdf8ed5..4d8807336 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -7,28 +7,28 @@ pub(crate) mod enr; pub mod enr_ext; // Allow external use of the lighthouse ENR builder -use crate::metrics; use crate::service::TARGET_SUBNET_PEERS; use crate::{error, Enr, NetworkConfig, NetworkGlobals, Subnet, SubnetDiscovery}; +use crate::{metrics, ClearDialError}; use discv5::{enr::NodeId, Discv5, Discv5Event}; pub use enr::{ build_enr, create_enr_builder_from_config, load_enr_from_disk, use_or_load_enr, CombinedKey, Eth2Enr, }; pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt}; -pub use libp2p::core::identity::{Keypair, PublicKey}; +pub use libp2p::identity::{Keypair, PublicKey}; use enr::{ATTESTATION_BITFIELD_ENR_KEY, ETH2_ENR_KEY, SYNC_COMMITTEE_BITFIELD_ENR_KEY}; use futures::prelude::*; use futures::stream::FuturesUnordered; -use libp2p::multiaddr::Protocol; use libp2p::swarm::behaviour::{DialFailure, FromSwarm}; -use libp2p::swarm::AddressScore; +use libp2p::swarm::THandlerInEvent; pub use libp2p::{ - core::{connection::ConnectionId, ConnectedPoint, Multiaddr, PeerId}, + core::{ConnectedPoint, Multiaddr}, + identity::PeerId, swarm::{ - dummy::ConnectionHandler, DialError, NetworkBehaviour, NetworkBehaviourAction as NBAction, - NotifyHandler, PollParameters, SubstreamProtocol, + dummy::ConnectionHandler, ConnectionId, DialError, NetworkBehaviour, NotifyHandler, + PollParameters, SubstreamProtocol, ToSwarm, }, }; use lru::LruCache; @@ -74,7 +74,7 @@ const DURATION_DIFFERENCE: Duration = Duration::from_millis(1); /// of the peer if it is specified. #[derive(Debug)] pub struct DiscoveredPeers { - pub peers: HashMap>, + pub peers: HashMap>, } #[derive(Clone, PartialEq)] @@ -191,7 +191,7 @@ pub struct Discovery { impl Discovery { /// NOTE: Creating discovery requires running within a tokio execution environment. pub async fn new( - local_key: &Keypair, + local_key: Keypair, config: &NetworkConfig, network_globals: Arc>, log: &slog::Logger, @@ -207,15 +207,9 @@ impl Discovery { let local_node_id = local_enr.node_id(); info!(log, "ENR Initialised"; "enr" => local_enr.to_base64(), "seq" => local_enr.seq(), "id"=> %local_enr.node_id(), - "ip4" => ?local_enr.ip4(), "udp4"=> ?local_enr.udp4(), "tcp4" => ?local_enr.tcp4(), "tcp6" => ?local_enr.tcp6(), "udp6" => ?local_enr.udp6() + "ip4" => ?local_enr.ip4(), "udp4"=> ?local_enr.udp4(), "tcp4" => ?local_enr.tcp4(), "tcp6" => ?local_enr.tcp6(), "udp6" => ?local_enr.udp6(), + "quic4" => ?local_enr.quic4(), "quic6" => ?local_enr.quic6() ); - let listen_socket = match config.listen_addrs() { - crate::listen_addr::ListenAddress::V4(v4_addr) => v4_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::V6(v6_addr) => v6_addr.udp_socket_addr(), - crate::listen_addr::ListenAddress::DualStack(_v4_addr, v6_addr) => { - v6_addr.udp_socket_addr() - } - }; // convert the keypair into an ENR key let enr_key: CombinedKey = CombinedKey::from_libp2p(local_key)?; @@ -236,7 +230,8 @@ impl Discovery { "peer_id" => %bootnode_enr.peer_id(), "ip" => ?bootnode_enr.ip4(), "udp" => ?bootnode_enr.udp4(), - "tcp" => ?bootnode_enr.tcp4() + "tcp" => ?bootnode_enr.tcp4(), + "quic" => ?bootnode_enr.quic4() ); let repr = bootnode_enr.to_string(); let _ = discv5.add_enr(bootnode_enr).map_err(|e| { @@ -251,10 +246,7 @@ impl Discovery { // Start the discv5 service and obtain an event stream let event_stream = if !config.disable_discovery { - discv5 - .start(listen_socket) - .map_err(|e| e.to_string()) - .await?; + discv5.start().map_err(|e| e.to_string()).await?; debug!(log, "Discovery service started"); EventStream::Awaiting(Box::pin(discv5.event_stream())) } else { @@ -290,7 +282,8 @@ impl Discovery { "peer_id" => %enr.peer_id(), "ip" => ?enr.ip4(), "udp" => ?enr.udp4(), - "tcp" => ?enr.tcp4() + "tcp" => ?enr.tcp4(), + "quic" => ?enr.quic4() ); let _ = discv5.add_enr(enr).map_err(|e| { error!( @@ -392,20 +385,6 @@ impl Discovery { self.discv5.table_entries_enr() } - /// Returns the ENR of a known peer if it exists. - pub fn enr_of_peer(&mut self, peer_id: &PeerId) -> Option { - // first search the local cache - if let Some(enr) = self.cached_enrs.get(peer_id) { - return Some(enr.clone()); - } - // not in the local cache, look in the routing table - if let Ok(node_id) = enr_ext::peer_id_to_node_id(peer_id) { - self.discv5.find_enr(&node_id) - } else { - None - } - } - /// Updates the local ENR TCP port. /// There currently isn't a case to update the address here. We opt for discovery to /// automatically update the external address. @@ -413,7 +392,24 @@ impl Discovery { /// If the external address needs to be modified, use `update_enr_udp_socket. pub fn update_enr_tcp_port(&mut self, port: u16) -> Result<(), String> { self.discv5 - .enr_insert("tcp", &port.to_be_bytes()) + .enr_insert("tcp", &port) + .map_err(|e| format!("{:?}", e))?; + + // replace the global version + *self.network_globals.local_enr.write() = self.discv5.local_enr(); + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); + Ok(()) + } + + // TODO: Group these functions here once the ENR is shared across discv5 and lighthouse and + // Lighthouse can modify the ENR directly. + // This currently doesn't support ipv6. All of these functions should be removed and + // addressed properly in the following issue. + // https://github.com/sigp/lighthouse/issues/4706 + pub fn update_enr_quic_port(&mut self, port: u16) -> Result<(), String> { + self.discv5 + .enr_insert("quic", &port) .map_err(|e| format!("{:?}", e))?; // replace the global version @@ -428,29 +424,12 @@ impl Discovery { /// This is with caution. Discovery should automatically maintain this. This should only be /// used when automatic discovery is disabled. pub fn update_enr_udp_socket(&mut self, socket_addr: SocketAddr) -> Result<(), String> { - match socket_addr { - SocketAddr::V4(socket) => { - self.discv5 - .enr_insert("ip", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } - SocketAddr::V6(socket) => { - self.discv5 - .enr_insert("ip6", &socket.ip().octets()) - .map_err(|e| format!("{:?}", e))?; - self.discv5 - .enr_insert("udp6", &socket.port().to_be_bytes()) - .map_err(|e| format!("{:?}", e))?; - } + const IS_TCP: bool = false; + if self.discv5.update_local_enr_socket(socket_addr, IS_TCP) { + // persist modified enr to disk + enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); } - - // replace the global version *self.network_globals.local_enr.write() = self.discv5.local_enr(); - // persist modified enr to disk - enr::save_enr_to_disk(Path::new(&self.enr_dir), &self.local_enr(), &self.log); Ok(()) } @@ -673,7 +652,7 @@ impl Discovery { if subnet_queries.len() == MAX_SUBNETS_IN_QUERY || self.queued_queries.is_empty() { // This query is for searching for peers of a particular subnet // Drain subnet_queries so we can re-use it as we continue to process the queue - let grouped_queries: Vec = subnet_queries.drain(..).collect(); + let grouped_queries: Vec = std::mem::take(&mut subnet_queries); self.start_subnet_query(grouped_queries); processed = true; } @@ -759,23 +738,6 @@ impl Discovery { target_peers: usize, additional_predicate: impl Fn(&Enr) -> bool + Send + 'static, ) { - // Make sure there are subnet queries included - let contains_queries = match &query { - QueryType::Subnet(queries) => !queries.is_empty(), - QueryType::FindPeers => true, - }; - - if !contains_queries { - debug!( - self.log, - "No subnets included in this request. Skipping discovery request." - ); - return; - } - - // Generate a random target node id. - let random_node = NodeId::random(); - let enr_fork_id = match self.local_enr().eth2() { Ok(v) => v, Err(e) => { @@ -799,7 +761,8 @@ impl Discovery { // Build the future let query_future = self .discv5 - .find_node_predicate(random_node, predicate, target_peers) + // Generate a random target node id. + .find_node_predicate(NodeId::random(), predicate, target_peers) .map(|v| QueryResult { query_type: query, result: v, @@ -813,7 +776,7 @@ impl Discovery { fn process_completed_queries( &mut self, query: QueryResult, - ) -> Option>> { + ) -> Option>> { match query.query_type { QueryType::FindPeers => { self.find_peer_active = false; @@ -823,12 +786,14 @@ impl Discovery { } Ok(r) => { debug!(self.log, "Discovery query completed"; "peers_found" => r.len()); - let mut results: HashMap<_, Option> = HashMap::new(); - r.iter().for_each(|enr| { - // cache the found ENR's - self.cached_enrs.put(enr.peer_id(), enr.clone()); - results.insert(enr.peer_id(), None); - }); + let results = r + .into_iter() + .map(|enr| { + // cache the found ENR's + self.cached_enrs.put(enr.peer_id(), enr.clone()); + (enr, None) + }) + .collect(); return Some(results); } Err(e) => { @@ -876,17 +841,17 @@ impl Discovery { let subnet_predicate = subnet_predicate::(vec![query.subnet], &self.log); - r.iter() + r.clone() + .into_iter() .filter(|enr| subnet_predicate(enr)) - .map(|enr| enr.peer_id()) - .for_each(|peer_id| { + .for_each(|enr| { if let Some(v) = metrics::get_int_counter( &metrics::SUBNET_PEERS_FOUND, &[query_str], ) { v.inc(); } - let other_min_ttl = mapped_results.get_mut(&peer_id); + let other_min_ttl = mapped_results.get_mut(&enr); // map peer IDs to the min_ttl furthest in the future match (query.min_ttl, other_min_ttl) { @@ -904,15 +869,11 @@ impl Discovery { } // update the mapping if we have a specified min_ttl (Some(min_ttl), Some(None)) => { - mapped_results.insert(peer_id, Some(min_ttl)); + mapped_results.insert(enr, Some(min_ttl)); } // first seen min_ttl for this enr - (Some(min_ttl), None) => { - mapped_results.insert(peer_id, Some(min_ttl)); - } - // first seen min_ttl for this enr - (None, None) => { - mapped_results.insert(peer_id, None); + (min_ttl, None) => { + mapped_results.insert(enr, min_ttl); } (None, Some(Some(_))) => {} // Don't replace the existing specific min_ttl (None, Some(None)) => {} // No-op because this is a duplicate @@ -936,7 +897,7 @@ impl Discovery { } /// Drives the queries returning any results from completed queries. - fn poll_queries(&mut self, cx: &mut Context) -> Option>> { + fn poll_queries(&mut self, cx: &mut Context) -> Option>> { while let Poll::Ready(Some(query_result)) = self.active_queries.poll_next_unpin(cx) { let result = self.process_completed_queries(query_result); if result.is_some() { @@ -952,23 +913,35 @@ impl Discovery { impl NetworkBehaviour for Discovery { // Discovery is not a real NetworkBehaviour... type ConnectionHandler = ConnectionHandler; - type OutEvent = DiscoveredPeers; + type ToSwarm = DiscoveredPeers; - fn new_handler(&mut self) -> Self::ConnectionHandler { - ConnectionHandler + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, libp2p::swarm::ConnectionDenied> { + // TODO: we might want to check discovery's banned ips here in the future. + Ok(ConnectionHandler) } - // Handles the libp2p request to obtain multiaddrs for peer_id's in order to dial them. - fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec { - if let Some(enr) = self.enr_of_peer(peer_id) { - // ENR's may have multiple Multiaddrs. The multi-addr associated with the UDP - // port is removed, which is assumed to be associated with the discv5 protocol (and - // therefore irrelevant for other libp2p components). - enr.multiaddr_tcp() - } else { - // PeerId is not known - Vec::new() - } + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + ) -> Result, libp2p::swarm::ConnectionDenied> { + Ok(ConnectionHandler) + } + + fn on_connection_handler_event( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _event: void::Void, + ) { } // Main execution loop to drive the behaviour @@ -976,7 +949,7 @@ impl NetworkBehaviour for Discovery { &mut self, cx: &mut Context, _: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll>> { if !self.started { return Poll::Pending; } @@ -987,7 +960,7 @@ impl NetworkBehaviour for Discovery { // Drive the queries and return any results from completed queries if let Some(peers) = self.poll_queries(cx) { // return the result to the peer manager - return Poll::Ready(NBAction::GenerateEvent(DiscoveredPeers { peers })); + return Poll::Ready(ToSwarm::GenerateEvent(DiscoveredPeers { peers })); } // Process the server event stream @@ -1044,28 +1017,8 @@ impl NetworkBehaviour for Discovery { // update network globals *self.network_globals.local_enr.write() = enr; // A new UDP socket has been detected. - // Build a multiaddr to report to libp2p - let addr = match socket_addr.ip() { - IpAddr::V4(v4_addr) => { - self.network_globals.listen_port_tcp4().map(|tcp4_port| { - Multiaddr::from(v4_addr).with(Protocol::Tcp(tcp4_port)) - }) - } - IpAddr::V6(v6_addr) => { - self.network_globals.listen_port_tcp6().map(|tcp6_port| { - Multiaddr::from(v6_addr).with(Protocol::Tcp(tcp6_port)) - }) - } - }; - - if let Some(address) = addr { - // NOTE: This doesn't actually track the external TCP port. More sophisticated NAT handling - // should handle this. - return Poll::Ready(NBAction::ReportObservedAddr { - address, - score: AddressScore::Finite(1), - }); - } + // NOTE: We assume libp2p itself can keep track of IP changes and we do + // not inform it about IP changes found via discovery. } Discv5Event::EnrAdded { .. } | Discv5Event::TalkRequest(_) @@ -1092,8 +1045,9 @@ impl NetworkBehaviour for Discovery { | FromSwarm::ExpiredListenAddr(_) | FromSwarm::ListenerError(_) | FromSwarm::ListenerClosed(_) - | FromSwarm::NewExternalAddr(_) - | FromSwarm::ExpiredExternalAddr(_) => { + | FromSwarm::NewExternalAddrCandidate(_) + | FromSwarm::ExternalAddrExpired(_) + | FromSwarm::ExternalAddrConfirmed(_) => { // Ignore events not relevant to discovery } } @@ -1104,20 +1058,16 @@ impl Discovery { fn on_dial_failure(&mut self, peer_id: Option, error: &DialError) { if let Some(peer_id) = peer_id { match error { - DialError::Banned - | DialError::LocalPeerId - | DialError::InvalidPeerId(_) - | DialError::ConnectionIo(_) + DialError::LocalPeerId { .. } + | DialError::Denied { .. } | DialError::NoAddresses | DialError::Transport(_) | DialError::WrongPeerId { .. } => { // set peer as disconnected in discovery DHT - debug!(self.log, "Marking peer disconnected in DHT"; "peer_id" => %peer_id); + debug!(self.log, "Marking peer disconnected in DHT"; "peer_id" => %peer_id, "error" => %ClearDialError(error)); self.disconnect_peer(&peer_id); } - DialError::ConnectionLimit(_) - | DialError::DialPeerConditionFalse(_) - | DialError::Aborted => {} + DialError::DialPeerConditionFalse(_) | DialError::Aborted => {} } } } @@ -1128,6 +1078,7 @@ mod tests { use super::*; use crate::rpc::methods::{MetaData, MetaDataV2}; use enr::EnrBuilder; + use libp2p::identity::secp256k1; use slog::{o, Drain}; use types::{BitVector, MinimalEthSpec, SubnetId}; @@ -1146,16 +1097,14 @@ mod tests { } async fn build_discovery() -> Discovery { - let keypair = libp2p::identity::Keypair::generate_secp256k1(); + let keypair = secp256k1::Keypair::generate(); let mut config = NetworkConfig::default(); config.set_listening_addr(crate::ListenAddress::unused_v4_ports()); - let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); + let enr_key: CombinedKey = CombinedKey::from_secp256k1(&keypair); let enr: Enr = build_enr::(&enr_key, &config, &EnrForkId::default()).unwrap(); let log = build_log(slog::Level::Debug, false); let globals = NetworkGlobals::new( enr, - Some(9000), - None, MetaData::V2(MetaDataV2 { seq_number: 0, attnets: Default::default(), @@ -1165,7 +1114,8 @@ mod tests { false, &log, ); - Discovery::new(&keypair, &config, Arc::new(globals), &log) + let keypair = keypair.into(); + Discovery::new(keypair, &config, Arc::new(globals), &log) .await .unwrap() } @@ -1211,8 +1161,8 @@ mod tests { fn make_enr(subnet_ids: Vec) -> Enr { let mut builder = EnrBuilder::new("v4"); - let keypair = libp2p::identity::Keypair::generate_secp256k1(); - let enr_key: CombinedKey = CombinedKey::from_libp2p(&keypair).unwrap(); + let keypair = secp256k1::Keypair::generate(); + let enr_key: CombinedKey = CombinedKey::from_secp256k1(&keypair); // set the "attnets" field on our ENR let mut bitfield = BitVector::::new(); @@ -1262,6 +1212,6 @@ mod tests { assert_eq!(results.len(), 2); // when a peer belongs to multiple subnet ids, we use the highest ttl. - assert_eq!(results.get(&enr1.peer_id()).unwrap(), &instant1); + assert_eq!(results.get(&enr1).unwrap(), &instant1); } } diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index e324532f7..f79ff8daf 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -1,4 +1,4 @@ -///! The subnet predicate used for searching for a particular subnet. +//! The subnet predicate used for searching for a particular subnet. use super::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use slog::trace; diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index 3d539af3b..7467fb7f0 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -17,6 +17,7 @@ pub mod rpc; pub mod types; pub use config::gossip_max_size; +use libp2p::swarm::DialError; pub use listen_addr::*; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -63,6 +64,46 @@ impl<'de> Deserialize<'de> for PeerIdSerialized { } } +// A wrapper struct that prints a dial error nicely. +struct ClearDialError<'a>(&'a DialError); + +impl<'a> ClearDialError<'a> { + 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; + } + current + } +} + +impl<'a> std::fmt::Display for ClearDialError<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match &self.0 { + DialError::Transport(errors) => { + for (_, transport_error) in errors { + match transport_error { + libp2p::TransportError::MultiaddrNotSupported(multiaddr_error) => { + write!(f, "Multiaddr not supported: {multiaddr_error}")?; + } + libp2p::TransportError::Other(other_error) => { + let inner_error = ClearDialError::most_inner_error(other_error); + write!(f, "Transport error: {inner_error}")?; + } + } + } + Ok(()) + } + DialError::LocalPeerId { .. } => write!(f, "The peer being dialed is the local peer."), + DialError::NoAddresses => write!(f, "No addresses for the peer to dial."), + DialError::DialPeerConditionFalse(_) => write!(f, "PeerCondition evaluation failed."), + DialError::Aborted => write!(f, "Connection aborted."), + DialError::WrongPeerId { .. } => write!(f, "Wrong peer id."), + DialError::Denied { cause } => write!(f, "Connection denied: {:?}", cause), + } + } +} + pub use crate::types::{ error, Enr, EnrSyncCommitteeBitfield, GossipTopic, NetworkGlobals, PubsubMessage, Subnet, SubnetDiscovery, diff --git a/beacon_node/lighthouse_network/src/listen_addr.rs b/beacon_node/lighthouse_network/src/listen_addr.rs index 20d87d403..53f7d9dac 100644 --- a/beacon_node/lighthouse_network/src/listen_addr.rs +++ b/beacon_node/lighthouse_network/src/listen_addr.rs @@ -6,14 +6,23 @@ use serde::{Deserialize, Serialize}; /// A listening address composed by an Ip, an UDP port and a TCP port. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ListenAddr { + /// The IP address we will listen on. pub addr: Ip, - pub udp_port: u16, + /// The UDP port that discovery will listen on. + pub disc_port: u16, + /// The UDP port that QUIC will listen on. + pub quic_port: u16, + /// The TCP port that libp2p will listen on. pub tcp_port: u16, } impl + Clone> ListenAddr { - pub fn udp_socket_addr(&self) -> SocketAddr { - (self.addr.clone().into(), self.udp_port).into() + pub fn discovery_socket_addr(&self) -> SocketAddr { + (self.addr.clone().into(), self.disc_port).into() + } + + pub fn quic_socket_addr(&self) -> SocketAddr { + (self.addr.clone().into(), self.quic_port).into() } pub fn tcp_socket_addr(&self) -> SocketAddr { @@ -46,22 +55,41 @@ impl ListenAddress { } } - /// Returns the TCP addresses. - pub fn tcp_addresses(&self) -> impl Iterator + '_ { - let v4_multiaddr = self + /// Returns the addresses the Swarm will listen on, given the setup. + pub fn libp2p_addresses(&self) -> impl Iterator { + let v4_tcp_multiaddr = self .v4() .map(|v4_addr| Multiaddr::from(v4_addr.addr).with(Protocol::Tcp(v4_addr.tcp_port))); - let v6_multiaddr = self + + let v4_quic_multiaddr = self.v4().map(|v4_addr| { + Multiaddr::from(v4_addr.addr) + .with(Protocol::Udp(v4_addr.quic_port)) + .with(Protocol::QuicV1) + }); + + let v6_quic_multiaddr = self.v6().map(|v6_addr| { + Multiaddr::from(v6_addr.addr) + .with(Protocol::Udp(v6_addr.quic_port)) + .with(Protocol::QuicV1) + }); + + let v6_tcp_multiaddr = self .v6() .map(|v6_addr| Multiaddr::from(v6_addr.addr).with(Protocol::Tcp(v6_addr.tcp_port))); - v4_multiaddr.into_iter().chain(v6_multiaddr) + + v4_tcp_multiaddr + .into_iter() + .chain(v4_quic_multiaddr) + .chain(v6_quic_multiaddr) + .chain(v6_tcp_multiaddr) } #[cfg(test)] pub fn unused_v4_ports() -> Self { ListenAddress::V4(ListenAddr { addr: Ipv4Addr::UNSPECIFIED, - udp_port: unused_port::unused_udp4_port().unwrap(), + disc_port: unused_port::unused_udp4_port().unwrap(), + quic_port: unused_port::unused_udp4_port().unwrap(), tcp_port: unused_port::unused_tcp4_port().unwrap(), }) } @@ -70,7 +98,8 @@ impl ListenAddress { pub fn unused_v6_ports() -> Self { ListenAddress::V6(ListenAddr { addr: Ipv6Addr::UNSPECIFIED, - udp_port: unused_port::unused_udp6_port().unwrap(), + disc_port: unused_port::unused_udp6_port().unwrap(), + quic_port: unused_port::unused_udp6_port().unwrap(), tcp_port: unused_port::unused_tcp6_port().unwrap(), }) } @@ -84,12 +113,14 @@ impl slog::KV for ListenAddress { ) -> slog::Result { if let Some(v4_addr) = self.v4() { serializer.emit_arguments("ip4_address", &format_args!("{}", v4_addr.addr))?; - serializer.emit_u16("udp4_port", v4_addr.udp_port)?; + serializer.emit_u16("disc4_port", v4_addr.disc_port)?; + serializer.emit_u16("quic4_port", v4_addr.quic_port)?; serializer.emit_u16("tcp4_port", v4_addr.tcp_port)?; } if let Some(v6_addr) = self.v6() { serializer.emit_arguments("ip6_address", &format_args!("{}", v6_addr.addr))?; - serializer.emit_u16("udp6_port", v6_addr.udp_port)?; + serializer.emit_u16("disc6_port", v6_addr.disc_port)?; + serializer.emit_u16("quic6_port", v6_addr.quic_port)?; serializer.emit_u16("tcp6_port", v6_addr.tcp_port)?; } slog::Result::Ok(()) diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index 58cc99201..ae02b689d 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -14,6 +14,16 @@ lazy_static! { "Count of libp2p peers currently connected" ); + pub static ref TCP_PEERS_CONNECTED: Result = try_create_int_gauge( + "libp2p_tcp_peers", + "Count of libp2p peers currently connected via TCP" + ); + + pub static ref QUIC_PEERS_CONNECTED: Result = try_create_int_gauge( + "libp2p_quic_peers", + "Count of libp2p peers currently connected via QUIC" + ); + pub static ref PEER_CONNECT_EVENT_COUNT: Result = try_create_int_counter( "libp2p_peer_connect_event_total", "Count of libp2p peer connect events (not the current number of connected peers)" diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index b2096013b..3c9b29238 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -1,5 +1,6 @@ //! Implementation of Lighthouse's peer management system. +use crate::discovery::enr_ext::EnrExt; use crate::rpc::{GoodbyeReason, MetaData, Protocol, RPCError, RPCResponseErrorCode}; use crate::service::TARGET_SUBNET_PEERS; use crate::{error, metrics, Gossipsub}; @@ -13,7 +14,6 @@ use peerdb::{client::ClientKind, BanOperation, BanResult, ScoreUpdateResult}; use rand::seq::SliceRandom; use slog::{debug, error, trace, warn}; use smallvec::SmallVec; -use std::collections::BTreeMap; use std::{ sync::Arc, time::{Duration, Instant}, @@ -21,7 +21,8 @@ use std::{ use strum::IntoEnumIterator; use types::{EthSpec, SyncSubnetId}; -pub use libp2p::core::{identity::Keypair, Multiaddr}; +pub use libp2p::core::Multiaddr; +pub use libp2p::identity::Keypair; #[allow(clippy::mutable_key_type)] // PeerId in hashmaps are no longer permitted by clippy pub mod peerdb; @@ -77,7 +78,7 @@ pub struct PeerManager { /// The target number of peers we would like to connect to. target_peers: usize, /// Peers queued to be dialed. - peers_to_dial: BTreeMap>, + peers_to_dial: Vec, /// The number of temporarily banned peers. This is used to prevent instantaneous /// reconnection. // NOTE: This just prevents re-connections. The state of the peer is otherwise unaffected. A @@ -311,16 +312,12 @@ impl PeerManager { /// Peers that have been returned by discovery requests that are suitable for dialing are /// returned here. /// - /// NOTE: By dialing `PeerId`s and not multiaddrs, libp2p requests the multiaddr associated - /// with a new `PeerId` which involves a discovery routing table lookup. We could dial the - /// multiaddr here, however this could relate to duplicate PeerId's etc. If the lookup - /// proves resource constraining, we should switch to multiaddr dialling here. + /// This function decides whether or not to dial these peers. #[allow(clippy::mutable_key_type)] - pub fn peers_discovered(&mut self, results: HashMap>) -> Vec { - let mut to_dial_peers = Vec::with_capacity(4); - + pub fn peers_discovered(&mut self, results: HashMap>) { + let mut to_dial_peers = 0; let connected_or_dialing = self.network_globals.connected_or_dialing_peers(); - for (peer_id, min_ttl) in results { + for (enr, min_ttl) in results { // There are two conditions in deciding whether to dial this peer. // 1. If we are less than our max connections. Discovery queries are executed to reach // our target peers, so its fine to dial up to our max peers (which will get pruned @@ -329,10 +326,8 @@ impl PeerManager { // considered a priority. We have pre-allocated some extra priority slots for these // peers as specified by PRIORITY_PEER_EXCESS. Therefore we dial these peers, even // if we are already at our max_peer limit. - if (min_ttl.is_some() - && connected_or_dialing + to_dial_peers.len() < self.max_priority_peers() - || connected_or_dialing + to_dial_peers.len() < self.max_peers()) - && self.network_globals.peers.read().should_dial(&peer_id) + if min_ttl.is_some() && connected_or_dialing + to_dial_peers < self.max_priority_peers() + || connected_or_dialing + to_dial_peers < self.max_peers() { // This should be updated with the peer dialing. In fact created once the peer is // dialed @@ -340,16 +335,16 @@ impl PeerManager { self.network_globals .peers .write() - .update_min_ttl(&peer_id, min_ttl); + .update_min_ttl(&enr.peer_id(), min_ttl); } - to_dial_peers.push(peer_id); + debug!(self.log, "Dialing discovered peer"; "peer_id" => %enr.peer_id()); + self.dial_peer(enr); + to_dial_peers += 1; } } // Queue another discovery if we need to - self.maintain_peer_count(to_dial_peers.len()); - - to_dial_peers + self.maintain_peer_count(to_dial_peers); } /// A STATUS message has been received from a peer. This resets the status timer. @@ -405,9 +400,16 @@ impl PeerManager { /* Notifications from the Swarm */ - // A peer is being dialed. - pub fn dial_peer(&mut self, peer_id: &PeerId, enr: Option) { - self.peers_to_dial.insert(*peer_id, enr); + /// A peer is being dialed. + pub fn dial_peer(&mut self, peer: Enr) { + if self + .network_globals + .peers + .read() + .should_dial(&peer.peer_id()) + { + self.peers_to_dial.push(peer); + } } /// Reports if a peer is banned or not. @@ -968,6 +970,7 @@ impl PeerManager { macro_rules! prune_peers { ($filter: expr) => { + let filter = $filter; for (peer_id, info) in self .network_globals .peers @@ -975,7 +978,7 @@ impl PeerManager { .worst_connected_peers() .iter() .filter(|(_, info)| { - !info.has_future_duty() && !info.is_trusted() && $filter(*info) + !info.has_future_duty() && !info.is_trusted() && filter(*info) }) { if peers_to_prune.len() @@ -1266,7 +1269,7 @@ impl PeerManager { ); } - let mut score_peers: &mut (f64, usize) = avg_score_per_client + let score_peers: &mut (f64, usize) = avg_score_per_client .entry(peer_info.client().kind.to_string()) .or_default(); score_peers.0 += peer_info.score().score(); @@ -2194,7 +2197,7 @@ mod tests { } impl Arbitrary for PeerCondition { - fn arbitrary(g: &mut G) -> Self { + fn arbitrary(g: &mut Gen) -> Self { let attestation_net_bitfield = { let len = ::SubnetBitfieldLength::to_usize(); let mut bitfield = Vec::with_capacity(len); 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 24de83a61..fedb876bb 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -1,38 +1,45 @@ +//! Implementation of [`NetworkBehaviour`] for the [`PeerManager`]. + use std::task::{Context, Poll}; use futures::StreamExt; -use libp2p::core::ConnectedPoint; +use libp2p::core::{multiaddr, ConnectedPoint}; +use libp2p::identity::PeerId; use libp2p::swarm::behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm}; use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use libp2p::swarm::dummy::ConnectionHandler; -use libp2p::swarm::{NetworkBehaviour, NetworkBehaviourAction, PollParameters}; -use libp2p::PeerId; +use libp2p::swarm::{ConnectionId, NetworkBehaviour, PollParameters, ToSwarm}; use slog::{debug, error}; use types::EthSpec; -use crate::metrics; +use crate::discovery::enr_ext::EnrExt; use crate::rpc::GoodbyeReason; use crate::types::SyncState; +use crate::{metrics, ClearDialError}; use super::peerdb::BanResult; use super::{ConnectingType, PeerManager, PeerManagerEvent, ReportSource}; impl NetworkBehaviour for PeerManager { type ConnectionHandler = ConnectionHandler; - - type OutEvent = PeerManagerEvent; + type ToSwarm = PeerManagerEvent; /* Required trait members */ - fn new_handler(&mut self) -> Self::ConnectionHandler { - ConnectionHandler + fn on_connection_handler_event( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _event: libp2p::swarm::THandlerOutEvent, + ) { + // no events from the dummy handler } fn poll( &mut self, cx: &mut Context<'_>, _params: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll> { // perform the heartbeat when necessary while self.heartbeat.poll_tick(cx).is_ready() { self.heartbeat(); @@ -84,19 +91,29 @@ impl NetworkBehaviour for PeerManager { } if !self.events.is_empty() { - return Poll::Ready(NetworkBehaviourAction::GenerateEvent(self.events.remove(0))); + return Poll::Ready(ToSwarm::GenerateEvent(self.events.remove(0))); } else { self.events.shrink_to_fit(); } - if let Some((peer_id, maybe_enr)) = self.peers_to_dial.pop_first() { - self.inject_peer_connection(&peer_id, ConnectingType::Dialing, maybe_enr); - let handler = self.new_handler(); - return Poll::Ready(NetworkBehaviourAction::Dial { + if let Some(enr) = self.peers_to_dial.pop() { + let peer_id = enr.peer_id(); + self.inject_peer_connection(&peer_id, ConnectingType::Dialing, Some(enr.clone())); + let quic_multiaddrs = enr.multiaddr_quic(); + if !quic_multiaddrs.is_empty() { + debug!(self.log, "Dialing QUIC supported peer"; "peer_id"=> %peer_id, "quic_multiaddrs" => ?quic_multiaddrs); + } + + // Prioritize Quic connections over Tcp ones. + let multiaddrs = quic_multiaddrs + .into_iter() + .chain(enr.multiaddr_tcp()) + .collect(); + return Poll::Ready(ToSwarm::Dial { opts: DialOpts::peer_id(peer_id) .condition(PeerCondition::Disconnected) + .addresses(multiaddrs) .build(), - handler, }); } @@ -110,13 +127,33 @@ impl NetworkBehaviour for PeerManager { endpoint, other_established, .. - }) => self.on_connection_established(peer_id, endpoint, other_established), + }) => { + // NOTE: We still need to handle the [`ConnectionEstablished`] because the + // [`NetworkBehaviour::handle_established_inbound_connection`] and + // [`NetworkBehaviour::handle_established_outbound_connection`] are fallible. This + // means another behaviour can kill the connection early, and we can't assume a + // peer as connected until this event is received. + self.on_connection_established(peer_id, endpoint, other_established) + } FromSwarm::ConnectionClosed(ConnectionClosed { peer_id, + endpoint, + remaining_established, .. - }) => self.on_connection_closed(peer_id, remaining_established), - FromSwarm::DialFailure(DialFailure { peer_id, .. }) => self.on_dial_failure(peer_id), + }) => self.on_connection_closed(peer_id, endpoint, remaining_established), + FromSwarm::DialFailure(DialFailure { + peer_id, + error, + connection_id: _, + }) => { + debug!(self.log, "Failed to dial peer"; "peer_id"=> ?peer_id, "error" => %ClearDialError(error)); + self.on_dial_failure(peer_id); + } + FromSwarm::ExternalAddrConfirmed(_) => { + // TODO: we likely want to check this against our assumed external tcp + // address + } FromSwarm::AddressChange(_) | FromSwarm::ListenFailure(_) | FromSwarm::NewListener(_) @@ -124,13 +161,35 @@ impl NetworkBehaviour for PeerManager { | FromSwarm::ExpiredListenAddr(_) | FromSwarm::ListenerError(_) | FromSwarm::ListenerClosed(_) - | FromSwarm::NewExternalAddr(_) - | FromSwarm::ExpiredExternalAddr(_) => { + | FromSwarm::NewExternalAddrCandidate(_) + | FromSwarm::ExternalAddrExpired(_) => { // The rest of the events we ignore since they are handled in their associated // `SwarmEvent` } } } + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &libp2p::Multiaddr, + _remote_addr: &libp2p::Multiaddr, + ) -> Result, libp2p::swarm::ConnectionDenied> { + // TODO: we might want to check if we accept this peer or not in the future. + Ok(ConnectionHandler) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &libp2p::Multiaddr, + _role_override: libp2p::core::Endpoint, + ) -> Result, libp2p::swarm::ConnectionDenied> { + // TODO: we might want to check if we accept this peer or not in the future. + Ok(ConnectionHandler) + } } impl PeerManager { @@ -140,7 +199,11 @@ impl PeerManager { endpoint: &ConnectedPoint, other_established: usize, ) { - debug!(self.log, "Connection established"; "peer_id" => %peer_id, "connection" => ?endpoint.to_endpoint()); + debug!(self.log, "Connection established"; "peer_id" => %peer_id, + "multiaddr" => %endpoint.get_remote_address(), + "connection" => ?endpoint.to_endpoint() + ); + if other_established == 0 { self.events.push(PeerManagerEvent::MetaData(peer_id)); } @@ -150,6 +213,34 @@ impl PeerManager { metrics::check_nat(); } + // increment prometheus metrics + if self.metrics_enabled { + let remote_addr = match endpoint { + ConnectedPoint::Dialer { address, .. } => address, + ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + }; + match remote_addr.iter().find(|proto| { + matches!( + proto, + multiaddr::Protocol::QuicV1 | multiaddr::Protocol::Tcp(_) + ) + }) { + Some(multiaddr::Protocol::QuicV1) => { + metrics::inc_gauge(&metrics::QUIC_PEERS_CONNECTED); + } + Some(multiaddr::Protocol::Tcp(_)) => { + metrics::inc_gauge(&metrics::TCP_PEERS_CONNECTED); + } + Some(_) => unreachable!(), + None => { + error!(self.log, "Connection established via unknown transport"; "addr" => %remote_addr) + } + }; + + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + } + // Check to make sure the peer is not supposed to be banned match self.ban_status(&peer_id) { // TODO: directly emit the ban event? @@ -201,14 +292,15 @@ impl PeerManager { self.events .push(PeerManagerEvent::PeerConnectedOutgoing(peer_id)); } - } - - // increment prometheus metrics - self.update_connected_peer_metrics(); - metrics::inc_counter(&metrics::PEER_CONNECT_EVENT_COUNT); + }; } - fn on_connection_closed(&mut self, peer_id: PeerId, remaining_established: usize) { + fn on_connection_closed( + &mut self, + peer_id: PeerId, + endpoint: &ConnectedPoint, + remaining_established: usize, + ) { if remaining_established > 0 { return; } @@ -234,9 +326,31 @@ impl PeerManager { // reference so that peer manager can track this peer. self.inject_disconnect(&peer_id); + let remote_addr = match endpoint { + ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + ConnectedPoint::Dialer { address, .. } => address, + }; + // Update the prometheus metrics - self.update_connected_peer_metrics(); - metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT); + if self.metrics_enabled { + match remote_addr.iter().find(|proto| { + matches!( + proto, + multiaddr::Protocol::QuicV1 | multiaddr::Protocol::Tcp(_) + ) + }) { + Some(multiaddr::Protocol::QuicV1) => { + metrics::dec_gauge(&metrics::QUIC_PEERS_CONNECTED); + } + Some(multiaddr::Protocol::Tcp(_)) => { + metrics::dec_gauge(&metrics::TCP_PEERS_CONNECTED); + } + // If it's an unknown protocol we already logged when connection was established. + _ => {} + }; + self.update_connected_peer_metrics(); + metrics::inc_counter(&metrics::PEER_DISCONNECT_EVENT_COUNT); + } } /// A dial attempt has failed. diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 52f0bbd9d..4a1efe8f2 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,16 +1,11 @@ -use crate::{ - metrics, - multiaddr::{Multiaddr, Protocol}, - types::Subnet, - Enr, Gossipsub, PeerId, -}; +use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, Gossipsub, PeerId}; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use rand::seq::SliceRandom; use score::{PeerAction, ReportSource, Score, ScoreState}; use slog::{crit, debug, error, trace, warn}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::time::Instant; use sync_status::SyncStatus; use types::EthSpec; @@ -764,28 +759,10 @@ impl PeerDB { | PeerConnectionStatus::Dialing { .. } => {} } - // Add the seen ip address and port to the peer's info - let socket_addr = match seen_address.iter().fold( - (None, None), - |(found_ip, found_port), protocol| match protocol { - Protocol::Ip4(ip) => (Some(ip.into()), found_port), - Protocol::Ip6(ip) => (Some(ip.into()), found_port), - Protocol::Tcp(port) => (found_ip, Some(port)), - _ => (found_ip, found_port), - }, - ) { - (Some(ip), Some(port)) => Some(SocketAddr::new(ip, port)), - (Some(_ip), None) => { - crit!(self.log, "Connected peer has an IP but no TCP port"; "peer_id" => %peer_id); - None - } - _ => None, - }; - // Update the connection state match direction { - ConnectionDirection::Incoming => info.connect_ingoing(socket_addr), - ConnectionDirection::Outgoing => info.connect_outgoing(socket_addr), + ConnectionDirection::Incoming => info.connect_ingoing(Some(seen_address)), + ConnectionDirection::Outgoing => info.connect_outgoing(Some(seen_address)), } } @@ -1274,6 +1251,7 @@ impl BannedPeersCount { #[cfg(test)] mod tests { use super::*; + use libp2p::core::multiaddr::Protocol; use libp2p::core::Multiaddr; use slog::{o, Drain}; use std::net::{Ipv4Addr, Ipv6Addr}; 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 555266d0e..44c54511d 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 @@ -2,15 +2,15 @@ use super::client::Client; use super::score::{PeerAction, Score, ScoreState}; use super::sync_status::SyncStatus; use crate::discovery::Eth2Enr; -use crate::Multiaddr; use crate::{rpc::MetaData, types::Subnet}; use discv5::Enr; +use libp2p::core::multiaddr::{Multiaddr, Protocol}; use serde::{ ser::{SerializeStruct, Serializer}, Serialize, }; use std::collections::HashSet; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::time::Instant; use strum::AsRefStr; use types::EthSpec; @@ -29,9 +29,9 @@ pub struct PeerInfo { /// The known listening addresses of this peer. This is given by identify and can be arbitrary /// (including local IPs). listening_addresses: Vec, - /// This is addresses we have physically seen and this is what we use for banning/un-banning + /// These are the multiaddrs we have physically seen and is what we use for banning/un-banning /// peers. - seen_addresses: HashSet, + seen_multiaddrs: HashSet, /// The current syncing state of the peer. The state may be determined after it's initial /// connection. sync_status: SyncStatus, @@ -60,7 +60,7 @@ impl Default for PeerInfo { client: Client::default(), connection_status: Default::default(), listening_addresses: Vec::new(), - seen_addresses: HashSet::new(), + seen_multiaddrs: HashSet::new(), subnets: HashSet::new(), sync_status: SyncStatus::Unknown, meta_data: None, @@ -227,15 +227,21 @@ impl PeerInfo { } /// Returns the seen addresses of the peer. - pub fn seen_addresses(&self) -> impl Iterator + '_ { - self.seen_addresses.iter() + pub fn seen_multiaddrs(&self) -> impl Iterator + '_ { + self.seen_multiaddrs.iter() } /// Returns a list of seen IP addresses for the peer. pub fn seen_ip_addresses(&self) -> impl Iterator + '_ { - self.seen_addresses - .iter() - .map(|socket_addr| socket_addr.ip()) + self.seen_multiaddrs.iter().filter_map(|multiaddr| { + multiaddr.iter().find_map(|protocol| { + match protocol { + Protocol::Ip4(ip) => Some(ip.into()), + Protocol::Ip6(ip) => Some(ip.into()), + _ => None, // Only care for IP addresses + } + }) + }) } /// Returns the connection status of the peer. @@ -415,7 +421,7 @@ impl PeerInfo { /// Modifies the status to Connected and increases the number of ingoing /// connections by one - pub(super) fn connect_ingoing(&mut self, seen_address: Option) { + pub(super) fn connect_ingoing(&mut self, seen_multiaddr: Option) { match &mut self.connection_status { Connected { n_in, .. } => *n_in += 1, Disconnected { .. } @@ -428,14 +434,14 @@ impl PeerInfo { } } - if let Some(socket_addr) = seen_address { - self.seen_addresses.insert(socket_addr); + if let Some(multiaddr) = seen_multiaddr { + self.seen_multiaddrs.insert(multiaddr); } } /// Modifies the status to Connected and increases the number of outgoing /// connections by one - pub(super) fn connect_outgoing(&mut self, seen_address: Option) { + pub(super) fn connect_outgoing(&mut self, seen_multiaddr: Option) { match &mut self.connection_status { Connected { n_out, .. } => *n_out += 1, Disconnected { .. } @@ -447,8 +453,8 @@ impl PeerInfo { self.connection_direction = Some(ConnectionDirection::Outgoing); } } - if let Some(ip_addr) = seen_address { - self.seen_addresses.insert(ip_addr); + if let Some(multiaddr) = seen_multiaddr { + self.seen_multiaddrs.insert(multiaddr); } } diff --git a/beacon_node/lighthouse_network/src/rpc/codec/base.rs b/beacon_node/lighthouse_network/src/rpc/codec/base.rs index 6c6ce2da3..943d4a3bc 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/base.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/base.rs @@ -214,13 +214,15 @@ mod tests { let mut buf = BytesMut::new(); buf.extend_from_slice(&message); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + 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 mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, - max_rpc_size(&fork_context), + max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize), fork_context, ); @@ -249,13 +251,15 @@ mod tests { // Insert length-prefix uvi_codec.encode(len, &mut dst).unwrap(); - let snappy_protocol_id = - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy); + 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 mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new( snappy_protocol_id, - max_rpc_size(&fork_context), + max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize), fork_context, ); @@ -277,12 +281,14 @@ mod tests { dst } - let protocol_id = - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy); + let protocol_id = ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy); // Response limits let fork_context = Arc::new(fork_context(ForkName::Base)); - let max_rpc_size = max_rpc_size(&fork_context); + + let chain_spec = Spec::default_spec(); + + let max_rpc_size = max_rpc_size(&fork_context, chain_spec.max_chunk_size as usize); let limit = protocol_id.rpc_response_limits::(&fork_context); let mut max = encode_len(limit.max + 1); let mut codec = SSZSnappyOutboundCodec::::new( diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 28fea40a2..f1d94da7e 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -1,9 +1,9 @@ +use crate::rpc::methods::*; use crate::rpc::{ codec::base::OutboundCodec, - protocol::{Encoding, Protocol, ProtocolId, RPCError, Version, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, + protocol::{Encoding, ProtocolId, RPCError, SupportedProtocol, ERROR_TYPE_MAX, ERROR_TYPE_MIN}, }; use crate::rpc::{InboundRequest, OutboundRequest, RPCCodedResponse, RPCResponse}; -use crate::{rpc::methods::*, EnrSyncCommitteeBitfield}; use libp2p::bytes::BytesMut; use snap::read::FrameDecoder; use snap::write::FrameEncoder; @@ -76,27 +76,14 @@ impl Encoder> for SSZSnappyInboundCodec< RPCResponse::MetaData(res) => // Encode the correct version of the MetaData response based on the negotiated version. { - match self.protocol.version { - Version::V1 => MetaData::::V1(MetaDataV1 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - }) - .as_ssz_bytes(), - Version::V2 => { - // `res` is of type MetaDataV2, return the ssz bytes - if res.syncnets().is_ok() { - res.as_ssz_bytes() - } else { - // `res` is of type MetaDataV1, create a MetaDataV2 by adding a default syncnets field - // Note: This code path is redundant as `res` would be always of type MetaDataV2 - MetaData::::V2(MetaDataV2 { - seq_number: *res.seq_number(), - attnets: res.attnets().clone(), - syncnets: EnrSyncCommitteeBitfield::::default(), - }) - .as_ssz_bytes() - } - } + match self.protocol.versioned_protocol { + SupportedProtocol::MetaDataV1 => res.metadata_v1().as_ssz_bytes(), + // We always send V2 metadata responses from the behaviour + // No change required. + SupportedProtocol::MetaDataV2 => res.metadata_v2().as_ssz_bytes(), + _ => unreachable!( + "We only send metadata responses on negotiating metadata requests" + ), } } }, @@ -139,8 +126,11 @@ impl Decoder for SSZSnappyInboundCodec { type Error = RPCError; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if self.protocol.message_name == Protocol::MetaData { - return Ok(Some(InboundRequest::MetaData(PhantomData))); + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV1 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))); + } + if self.protocol.versioned_protocol == SupportedProtocol::MetaDataV2 { + return Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))); } let length = match handle_length(&mut self.inner, &mut self.len, src)? { Some(len) => len, @@ -152,8 +142,8 @@ impl Decoder for SSZSnappyInboundCodec { let ssz_limits = self.protocol.rpc_request_limits(); if ssz_limits.is_out_of_bounds(length, self.max_packet_size) { return Err(RPCError::InvalidData(format!( - "RPC request length is out of bounds, length {}", - length + "RPC request length for protocol {:?} is out of bounds, length {}", + self.protocol.versioned_protocol, length ))); } // Calculate worst case compression length for given uncompressed length @@ -170,11 +160,7 @@ impl Decoder for SSZSnappyInboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_request(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_request(self.protocol.message_name, &decoded_buffer), - } + handle_rpc_request(self.protocol.versioned_protocol, &decoded_buffer) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -228,11 +214,16 @@ impl Encoder> for SSZSnappyOutboundCodec< let bytes = match item { OutboundRequest::Status(req) => req.as_ssz_bytes(), OutboundRequest::Goodbye(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRange(req) => req.as_ssz_bytes(), - OutboundRequest::BlocksByRoot(req) => req.block_roots.as_ssz_bytes(), + OutboundRequest::BlocksByRange(r) => match r { + OldBlocksByRangeRequest::V1(req) => req.as_ssz_bytes(), + OldBlocksByRangeRequest::V2(req) => req.as_ssz_bytes(), + }, + OutboundRequest::BlocksByRoot(r) => match r { + BlocksByRootRequest::V1(req) => req.block_roots.as_ssz_bytes(), + BlocksByRootRequest::V2(req) => req.block_roots.as_ssz_bytes(), + }, OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode - OutboundRequest::LightClientBootstrap(req) => req.as_ssz_bytes(), }; // SSZ encoded bytes should be within `max_packet_size` if bytes.len() > self.max_packet_size { @@ -311,15 +302,10 @@ impl Decoder for SSZSnappyOutboundCodec { let n = reader.get_ref().get_ref().position(); self.len = None; let _read_bytes = src.split_to(n as usize); - - match self.protocol.version { - Version::V1 => handle_v1_response(self.protocol.message_name, &decoded_buffer), - Version::V2 => handle_v2_response( - self.protocol.message_name, - &decoded_buffer, - &mut self.fork_name, - ), - } + // Safe to `take` from `self.fork_name` as we have all the bytes we need to + // decode an ssz object at this point. + let fork_name = self.fork_name.take(); + handle_rpc_response(self.protocol.versioned_protocol, &decoded_buffer, fork_name) } Err(e) => handle_error(e, reader.get_ref().get_ref().position(), max_compressed_len), } @@ -456,181 +442,150 @@ fn handle_length( } } -/// Decodes a `Version::V1` `InboundRequest` from the byte stream. +/// Decodes an `InboundRequest` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with // length = length-prefix received in the beginning of the stream. -fn handle_v1_request( - protocol: Protocol, +fn handle_rpc_request( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], ) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(InboundRequest::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - Protocol::Goodbye => Ok(Some(InboundRequest::Goodbye( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(InboundRequest::Status( + StatusMessage::from_ssz_bytes(decoded_buffer)?, + ))), + SupportedProtocol::GoodbyeV1 => Ok(Some(InboundRequest::Goodbye( GoodbyeReason::from_ssz_bytes(decoded_buffer)?, ))), - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, + SupportedProtocol::BlocksByRangeV2 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2::from_ssz_bytes(decoded_buffer)?), ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::Ping => Ok(Some(InboundRequest::Ping(Ping { + SupportedProtocol::BlocksByRangeV1 => Ok(Some(InboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1::from_ssz_bytes(decoded_buffer)?), + ))), + SupportedProtocol::BlocksByRootV2 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V2(BlocksByRootRequestV2 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(InboundRequest::BlocksByRoot( + BlocksByRootRequest::V1(BlocksByRootRequestV1 { + block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, + }), + ))), + SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), - Protocol::LightClientBootstrap => Ok(Some(InboundRequest::LightClientBootstrap( - LightClientBootstrapRequest { + SupportedProtocol::LightClientBootstrapV1 => Ok(Some( + InboundRequest::LightClientBootstrap(LightClientBootstrapRequest { root: Hash256::from_ssz_bytes(decoded_buffer)?, - }, - ))), + }), + )), // MetaData requests return early from InboundUpgrade and do not reach the decoder. // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV2 => { if !decoded_buffer.is_empty() { Err(RPCError::InternalError( "Metadata requests shouldn't reach decoder", )) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v2()))) } } - } -} - -/// Decodes a `Version::V2` `InboundRequest` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v2_request( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::BlocksByRange => Ok(Some(InboundRequest::BlocksByRange( - OldBlocksByRangeRequest::from_ssz_bytes(decoded_buffer)?, - ))), - Protocol::BlocksByRoot => Ok(Some(InboundRequest::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from_ssz_bytes(decoded_buffer)?, - }))), - // MetaData requests return early from InboundUpgrade and do not reach the decoder. - // Handle this case just for completeness. - Protocol::MetaData => { + SupportedProtocol::MetaDataV1 => { if !decoded_buffer.is_empty() { Err(RPCError::InvalidData("Metadata request".to_string())) } else { - Ok(Some(InboundRequest::MetaData(PhantomData))) + Ok(Some(InboundRequest::MetaData(MetadataRequest::new_v1()))) } } - _ => Err(RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("{} does not support version 2", protocol), - )), } } -/// Decodes a `Version::V1` `RPCResponse` from the byte stream. +/// Decodes a `RPCResponse` from the byte stream. /// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. -fn handle_v1_response( - protocol: Protocol, - decoded_buffer: &[u8], -) -> Result>, RPCError> { - match protocol { - Protocol::Status => Ok(Some(RPCResponse::Status(StatusMessage::from_ssz_bytes( - decoded_buffer, - )?))), - // This case should be unreachable as `Goodbye` has no response. - Protocol::Goodbye => Err(RPCError::InvalidData( - "Goodbye RPC message has no valid response".to_string(), - )), - Protocol::BlocksByRange => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::BlocksByRoot => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - Protocol::Ping => Ok(Some(RPCResponse::Pong(Ping { - data: u64::from_ssz_bytes(decoded_buffer)?, - }))), - Protocol::MetaData => Ok(Some(RPCResponse::MetaData(MetaData::V1( - MetaDataV1::from_ssz_bytes(decoded_buffer)?, - )))), - Protocol::LightClientBootstrap => Ok(Some(RPCResponse::LightClientBootstrap( - LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, - ))), - } -} - -/// Decodes a `Version::V2` `RPCResponse` from the byte stream. -/// `decoded_buffer` should be an ssz-encoded bytestream with -// length = length-prefix received in the beginning of the stream. +/// length = length-prefix received in the beginning of the stream. /// /// For BlocksByRange/BlocksByRoot reponses, decodes the appropriate response /// according to the received `ForkName`. -fn handle_v2_response( - protocol: Protocol, +fn handle_rpc_response( + versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], - fork_name: &mut Option, + fork_name: Option, ) -> Result>, RPCError> { - // MetaData does not contain context_bytes - if let Protocol::MetaData = protocol { - Ok(Some(RPCResponse::MetaData(MetaData::V2( + match versioned_protocol { + SupportedProtocol::StatusV1 => Ok(Some(RPCResponse::Status( + StatusMessage::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(), + )), + SupportedProtocol::BlocksByRangeV1 => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::BlocksByRootV1 => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { + data: u64::from_ssz_bytes(decoded_buffer)?, + }))), + SupportedProtocol::MetaDataV1 => Ok(Some(RPCResponse::MetaData(MetaData::V1( + MetaDataV1::from_ssz_bytes(decoded_buffer)?, + )))), + SupportedProtocol::LightClientBootstrapV1 => Ok(Some(RPCResponse::LightClientBootstrap( + LightClientBootstrap::from_ssz_bytes(decoded_buffer)?, + ))), + // MetaData V2 responses have no context bytes, so behave similarly to V1 responses + SupportedProtocol::MetaDataV2 => Ok(Some(RPCResponse::MetaData(MetaData::V2( MetaDataV2::from_ssz_bytes(decoded_buffer)?, - )))) - } else { - let fork_name = fork_name.take().ok_or_else(|| { - RPCError::ErrorResponse( - RPCResponseErrorCode::InvalidRequest, - format!("No context bytes provided for {} response", protocol), - ) - })?; - match protocol { - Protocol::BlocksByRange => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), + )))), + SupportedProtocol::BlocksByRangeV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRange(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - Protocol::BlocksByRoot => match fork_name { - ForkName::Altair => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Base => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), - )))), - ForkName::Merge => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes( - decoded_buffer, - )?), - )))), - ForkName::Capella => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( - SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( - decoded_buffer, - )?), - )))), - }, - _ => Err(RPCError::ErrorResponse( + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, - "Invalid v2 request".to_string(), + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), )), - } + }, + SupportedProtocol::BlocksByRootV2 => match fork_name { + Some(ForkName::Altair) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Altair(SignedBeaconBlockAltair::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Base) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Merge) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Merge(SignedBeaconBlockMerge::from_ssz_bytes(decoded_buffer)?), + )))), + Some(ForkName::Capella) => Ok(Some(RPCResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Capella(SignedBeaconBlockCapella::from_ssz_bytes( + decoded_buffer, + )?), + )))), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, } } @@ -660,8 +615,8 @@ mod tests { }; use std::sync::Arc; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockMerge, EmptyBlock, Epoch, - ForkContext, FullPayload, Hash256, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockMerge, ChainSpec, EmptyBlock, + Epoch, ForkContext, FullPayload, Hash256, Signature, SignedBeaconBlock, Slot, }; use snap::write::FrameEncoder; @@ -703,7 +658,7 @@ mod tests { } /// Merge block with length < max_rpc_size. - fn merge_block_small(fork_context: &ForkContext) -> SignedBeaconBlock { + fn merge_block_small(fork_context: &ForkContext, spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockMerge<_, FullPayload> = BeaconBlockMerge::empty(&Spec::default_spec()); let tx = VariableList::from(vec![0; 1024]); @@ -712,14 +667,14 @@ mod tests { block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Merge(block); - assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context)); + assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context, spec.max_chunk_size as usize)); SignedBeaconBlock::from_block(block, Signature::empty()) } /// Merge block with length > MAX_RPC_SIZE. /// The max limit for a merge block is in the order of ~16GiB which wouldn't fit in memory. /// Hence, we generate a merge block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. - fn merge_block_large(fork_context: &ForkContext) -> SignedBeaconBlock { + fn merge_block_large(fork_context: &ForkContext, spec: &ChainSpec) -> SignedBeaconBlock { let mut block: BeaconBlockMerge<_, FullPayload> = BeaconBlockMerge::empty(&Spec::default_spec()); let tx = VariableList::from(vec![0; 1024]); @@ -728,7 +683,7 @@ mod tests { block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Merge(block); - assert!(block.ssz_bytes_len() > max_rpc_size(fork_context)); + assert!(block.ssz_bytes_len() > max_rpc_size(fork_context, spec.max_chunk_size as usize)); SignedBeaconBlock::from_block(block, Signature::empty()) } @@ -742,18 +697,20 @@ mod tests { } } - fn bbrange_request() -> OldBlocksByRangeRequest { - OldBlocksByRangeRequest { - start_slot: 0, - count: 10, - step: 1, - } + fn bbrange_request_v1() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new_v1(0, 10, 1) } - fn bbroot_request() -> BlocksByRootRequest { - BlocksByRootRequest { - block_roots: VariableList::from(vec![Hash256::zero()]), - } + fn bbrange_request_v2() -> OldBlocksByRangeRequest { + OldBlocksByRangeRequest::new(0, 10, 1) + } + + fn bbroot_request_v1() -> BlocksByRootRequest { + BlocksByRootRequest::new_v1(vec![Hash256::zero()].into()) + } + + fn bbroot_request_v2() -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![Hash256::zero()].into()) } fn ping_message() -> Ping { @@ -777,14 +734,14 @@ mod tests { /// Encodes the given protocol response as bytes. fn encode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, + spec: &ChainSpec, ) -> Result { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context); + let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); let mut buf = BytesMut::new(); let mut snappy_inbound_codec = @@ -824,14 +781,14 @@ mod tests { /// Attempts to decode the given protocol bytes as an rpc response fn decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: &mut BytesMut, fork_name: ForkName, + spec: &ChainSpec, ) -> Result>, RPCError> { - let snappy_protocol_id = ProtocolId::new(protocol, version, Encoding::SSZSnappy); + let snappy_protocol_id = ProtocolId::new(protocol, Encoding::SSZSnappy); let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context); + let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); let mut snappy_outbound_codec = SSZSnappyOutboundCodec::::new(snappy_protocol_id, max_packet_size, fork_context); // decode message just as snappy message @@ -840,63 +797,60 @@ mod tests { /// Encodes the provided protocol message as bytes and tries to decode the encoding bytes. fn encode_then_decode_response( - protocol: Protocol, - version: Version, + protocol: SupportedProtocol, message: RPCCodedResponse, fork_name: ForkName, + spec: &ChainSpec, ) -> Result>, RPCError> { - let mut encoded = encode_response(protocol, version.clone(), message, fork_name)?; - decode_response(protocol, version, &mut encoded, fork_name) + let mut encoded = encode_response(protocol, message, fork_name, spec)?; + decode_response(protocol, &mut encoded, fork_name, spec) } /// Verifies that requests we send are encoded in a way that we would correctly decode too. - fn encode_then_decode_request(req: OutboundRequest, fork_name: ForkName) { + fn encode_then_decode_request( + req: OutboundRequest, + fork_name: ForkName, + spec: &ChainSpec, + ) { let fork_context = Arc::new(fork_context(fork_name)); - let max_packet_size = max_rpc_size(&fork_context); - for protocol in req.supported_protocols() { - // Encode a request we send - let mut buf = BytesMut::new(); - let mut outbound_codec = SSZSnappyOutboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); - outbound_codec.encode(req.clone(), &mut buf).unwrap(); + let max_packet_size = max_rpc_size(&fork_context, spec.max_chunk_size as usize); + let protocol = ProtocolId::new(req.versioned_protocol(), Encoding::SSZSnappy); + // Encode a request we send + let mut buf = BytesMut::new(); + let mut outbound_codec = SSZSnappyOutboundCodec::::new( + protocol.clone(), + max_packet_size, + fork_context.clone(), + ); + outbound_codec.encode(req.clone(), &mut buf).unwrap(); - let mut inbound_codec = SSZSnappyInboundCodec::::new( - protocol.clone(), - max_packet_size, - fork_context.clone(), - ); + let mut inbound_codec = + SSZSnappyInboundCodec::::new(protocol.clone(), max_packet_size, fork_context); - let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { - panic!( - "Should correctly decode the request {} over protocol {:?} and fork {}", - req, protocol, fork_name - ) - }); - match req.clone() { - OutboundRequest::Status(status) => { - assert_eq!(decoded, InboundRequest::Status(status)) - } - OutboundRequest::Goodbye(goodbye) => { - assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) - } - OutboundRequest::BlocksByRange(bbrange) => { - assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) - } - OutboundRequest::BlocksByRoot(bbroot) => { - assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) - } - OutboundRequest::Ping(ping) => { - assert_eq!(decoded, InboundRequest::Ping(ping)) - } - OutboundRequest::MetaData(metadata) => { - assert_eq!(decoded, InboundRequest::MetaData(metadata)) - } - OutboundRequest::LightClientBootstrap(bootstrap) => { - assert_eq!(decoded, InboundRequest::LightClientBootstrap(bootstrap)) - } + let decoded = inbound_codec.decode(&mut buf).unwrap().unwrap_or_else(|| { + panic!( + "Should correctly decode the request {} over protocol {:?} and fork {}", + req, protocol, fork_name + ) + }); + match req { + OutboundRequest::Status(status) => { + assert_eq!(decoded, InboundRequest::Status(status)) + } + OutboundRequest::Goodbye(goodbye) => { + assert_eq!(decoded, InboundRequest::Goodbye(goodbye)) + } + OutboundRequest::BlocksByRange(bbrange) => { + assert_eq!(decoded, InboundRequest::BlocksByRange(bbrange)) + } + OutboundRequest::BlocksByRoot(bbroot) => { + assert_eq!(decoded, InboundRequest::BlocksByRoot(bbroot)) + } + OutboundRequest::Ping(ping) => { + assert_eq!(decoded, InboundRequest::Ping(ping)) + } + OutboundRequest::MetaData(metadata) => { + assert_eq!(decoded, InboundRequest::MetaData(metadata)) } } } @@ -904,32 +858,34 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v1() { + let chain_spec = Spec::default_spec(); + assert_eq!( encode_then_decode_response( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, RPCCodedResponse::Success(RPCResponse::Status(status_message())), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::Status(status_message()))) ); assert_eq!( encode_then_decode_response( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::Pong(ping_message()))) ); assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRange(Arc::new( empty_base_block() @@ -939,10 +895,10 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V1, + SupportedProtocol::BlocksByRangeV1, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::SSZDecodeError(_) @@ -952,10 +908,10 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRoot( Arc::new(empty_base_block()) @@ -965,10 +921,10 @@ mod tests { assert!( matches!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V1, + SupportedProtocol::BlocksByRootV1, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::SSZDecodeError(_) @@ -978,20 +934,10 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, - RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), - ForkName::Base, - ), - Ok(Some(RPCResponse::MetaData(metadata()))), - ); - - assert_eq!( - encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::MetaData(metadata()))), ); @@ -999,10 +945,10 @@ mod tests { // A MetaDataV2 still encodes as a MetaDataV1 since version is Version::V1 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V1, + SupportedProtocol::MetaDataV1, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::MetaData(metadata()))), ); @@ -1011,40 +957,14 @@ mod tests { // Test RPCResponse encoding/decoding for V1 messages #[test] fn test_encode_then_decode_v2() { - assert!( - matches!( - encode_then_decode_response( - Protocol::Status, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Status(status_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "status does not have V2 message" - ); - - assert!( - matches!( - encode_then_decode_response( - Protocol::Ping, - Version::V2, - RPCCodedResponse::Success(RPCResponse::Pong(ping_message())), - ForkName::Base, - ) - .unwrap_err(), - RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), - ), - "ping does not have V2 message" - ); + let chain_spec = Spec::default_spec(); assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRange(Arc::new( empty_base_block() @@ -1056,10 +976,10 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRange(Arc::new( empty_base_block() @@ -1068,25 +988,25 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(altair_block()))), ForkName::Altair, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRange(Arc::new(altair_block())))) ); - let merge_block_small = merge_block_small(&fork_context(ForkName::Merge)); - let merge_block_large = merge_block_large(&fork_context(ForkName::Merge)); + let merge_block_small = merge_block_small(&fork_context(ForkName::Merge), &chain_spec); + let merge_block_large = merge_block_large(&fork_context(ForkName::Merge), &chain_spec); assert_eq!( encode_then_decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new( merge_block_small.clone() ))), ForkName::Merge, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRange(Arc::new( merge_block_small.clone() @@ -1100,10 +1020,10 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded, ForkName::Merge, + &chain_spec, ) .unwrap_err(), RPCError::InvalidData(_) @@ -1113,10 +1033,10 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRoot( Arc::new(empty_base_block()) @@ -1128,10 +1048,10 @@ mod tests { // the current_fork's rpc limit assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRoot( Arc::new(empty_base_block()) @@ -1140,22 +1060,22 @@ mod tests { assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRoot(Arc::new(altair_block())))) ); assert_eq!( encode_then_decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new( merge_block_small.clone() ))), ForkName::Merge, + &chain_spec, ), Ok(Some(RPCResponse::BlocksByRoot(Arc::new(merge_block_small)))) ); @@ -1167,10 +1087,10 @@ mod tests { assert!( matches!( decode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, &mut encoded, ForkName::Merge, + &chain_spec, ) .unwrap_err(), RPCError::InvalidData(_) @@ -1181,20 +1101,20 @@ mod tests { // A MetaDataV1 still encodes as a MetaDataV2 since version is Version::V2 assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Base, + &chain_spec, ), Ok(Some(RPCResponse::MetaData(metadata_v2()))) ); assert_eq!( encode_then_decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata_v2())), ForkName::Altair, + &chain_spec, ), Ok(Some(RPCResponse::MetaData(metadata_v2()))) ); @@ -1205,12 +1125,14 @@ mod tests { fn test_context_bytes_v2() { let fork_context = fork_context(ForkName::Altair); + let chain_spec = Spec::default_spec(); + // Removing context bytes for v2 messages should error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ) .unwrap(); @@ -1218,20 +1140,20 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, - ForkName::Base + ForkName::Base, + &chain_spec, ) .unwrap_err(), RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), )); let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Base, + &chain_spec, ) .unwrap(); @@ -1239,10 +1161,10 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut encoded_bytes, - ForkName::Base + ForkName::Base, + &chain_spec, ) .unwrap_err(), RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), @@ -1250,10 +1172,10 @@ mod tests { // Trying to decode a base block with altair context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, RPCCodedResponse::Success(RPCResponse::BlocksByRange(Arc::new(empty_base_block()))), ForkName::Altair, + &chain_spec, ) .unwrap(); @@ -1264,10 +1186,10 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, - ForkName::Altair + ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::SSZDecodeError(_), @@ -1275,10 +1197,10 @@ mod tests { // Trying to decode an altair block with base context bytes should give ssz decoding error let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(altair_block()))), ForkName::Altair, + &chain_spec, ) .unwrap(); @@ -1288,10 +1210,10 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, - ForkName::Altair + ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::SSZDecodeError(_), @@ -1302,28 +1224,28 @@ mod tests { encoded_bytes.extend_from_slice(&fork_context.to_context_bytes(ForkName::Altair).unwrap()); encoded_bytes.extend_from_slice( &encode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, RPCCodedResponse::Success(RPCResponse::MetaData(metadata())), ForkName::Altair, + &chain_spec, ) .unwrap(), ); assert!(decode_response( - Protocol::MetaData, - Version::V2, + SupportedProtocol::MetaDataV2, &mut encoded_bytes, - ForkName::Altair + 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( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, + &chain_spec, ) .unwrap(); @@ -1333,10 +1255,10 @@ mod tests { assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut wrong_fork_bytes, - ForkName::Altair + ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::ErrorResponse(RPCResponseErrorCode::InvalidRequest, _), @@ -1344,10 +1266,10 @@ mod tests { // Sending bytes less than context bytes length should wait for more bytes by returning `Ok(None)` let mut encoded_bytes = encode_response( - Protocol::BlocksByRoot, - Version::V2, + SupportedProtocol::BlocksByRootV2, RPCCodedResponse::Success(RPCResponse::BlocksByRoot(Arc::new(empty_base_block()))), ForkName::Altair, + &chain_spec, ) .unwrap(); @@ -1355,10 +1277,10 @@ mod tests { assert_eq!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut part, - ForkName::Altair + ForkName::Altair, + &chain_spec, ), Ok(None) ) @@ -1370,13 +1292,19 @@ mod tests { OutboundRequest::Ping(ping_message()), OutboundRequest::Status(status_message()), OutboundRequest::Goodbye(GoodbyeReason::Fault), - OutboundRequest::BlocksByRange(bbrange_request()), - OutboundRequest::BlocksByRoot(bbroot_request()), - OutboundRequest::MetaData(PhantomData::), + OutboundRequest::BlocksByRange(bbrange_request_v1()), + OutboundRequest::BlocksByRange(bbrange_request_v2()), + OutboundRequest::BlocksByRoot(bbroot_request_v1()), + OutboundRequest::BlocksByRoot(bbroot_request_v2()), + OutboundRequest::MetaData(MetadataRequest::new_v1()), + OutboundRequest::MetaData(MetadataRequest::new_v2()), ]; + + let chain_spec = Spec::default_spec(); + for req in requests.iter() { for fork_name in ForkName::list_all() { - encode_then_decode_request(req.clone(), fork_name); + encode_then_decode_request(req.clone(), fork_name, &chain_spec); } } } @@ -1430,9 +1358,16 @@ mod tests { assert_eq!(writer.get_ref().len(), 42); dst.extend_from_slice(writer.get_ref()); + let chain_spec = Spec::default_spec(); // 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response( + SupportedProtocol::StatusV1, + &mut dst, + ForkName::Base, + &chain_spec + ) + .unwrap_err(), RPCError::InvalidData(_) )); } @@ -1487,13 +1422,15 @@ mod tests { assert_eq!(writer.get_ref().len(), 8103); dst.extend_from_slice(writer.get_ref()); + let chain_spec = Spec::default_spec(); + // 10 (for stream identifier) + 176156 + 8103 = 184269 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`. assert!(matches!( decode_response( - Protocol::BlocksByRange, - Version::V2, + SupportedProtocol::BlocksByRangeV2, &mut dst, - ForkName::Altair + ForkName::Altair, + &chain_spec, ) .unwrap_err(), RPCError::InvalidData(_) @@ -1521,8 +1458,12 @@ mod tests { let mut uvi_codec: Uvi = Uvi::default(); let mut dst = BytesMut::with_capacity(1024); + let chain_spec = Spec::default_spec(); + // Insert length-prefix - uvi_codec.encode(MAX_RPC_SIZE + 1, &mut dst).unwrap(); + uvi_codec + .encode(chain_spec.max_chunk_size as usize + 1, &mut dst) + .unwrap(); // Insert snappy stream identifier dst.extend_from_slice(stream_identifier); @@ -1534,7 +1475,13 @@ mod tests { dst.extend_from_slice(writer.get_ref()); assert!(matches!( - decode_response(Protocol::Status, Version::V1, &mut dst, ForkName::Base).unwrap_err(), + decode_response( + SupportedProtocol::StatusV1, + &mut dst, + ForkName::Base, + &chain_spec + ) + .unwrap_err(), RPCError::InvalidData(_) )); } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index bea0929fb..a0f3acaf7 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -58,18 +58,41 @@ impl FromStr for ProtocolQuota { } } -/// Configurations for the rate limiter applied to outbound requests (made by the node itself). +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default)] +pub struct OutboundRateLimiterConfig(pub RateLimiterConfig); + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug, Default)] +pub struct InboundRateLimiterConfig(pub RateLimiterConfig); + +impl FromStr for OutboundRateLimiterConfig { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + RateLimiterConfig::from_str(s).map(Self) + } +} + +impl FromStr for InboundRateLimiterConfig { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + RateLimiterConfig::from_str(s).map(Self) + } +} + +/// Configurations for the rate limiter. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct OutboundRateLimiterConfig { +pub struct RateLimiterConfig { pub(super) ping_quota: Quota, pub(super) meta_data_quota: Quota, pub(super) status_quota: Quota, pub(super) goodbye_quota: Quota, pub(super) blocks_by_range_quota: Quota, pub(super) blocks_by_root_quota: Quota, + pub(super) light_client_bootstrap_quota: Quota, } -impl OutboundRateLimiterConfig { +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); @@ -77,22 +100,24 @@ impl OutboundRateLimiterConfig { pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(methods::MAX_REQUEST_BLOCKS, 10); pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); } -impl Default for OutboundRateLimiterConfig { +impl Default for RateLimiterConfig { fn default() -> Self { - OutboundRateLimiterConfig { + RateLimiterConfig { ping_quota: Self::DEFAULT_PING_QUOTA, meta_data_quota: Self::DEFAULT_META_DATA_QUOTA, status_quota: Self::DEFAULT_STATUS_QUOTA, goodbye_quota: Self::DEFAULT_GOODBYE_QUOTA, blocks_by_range_quota: Self::DEFAULT_BLOCKS_BY_RANGE_QUOTA, blocks_by_root_quota: Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA, + light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, } } } -impl Debug for OutboundRateLimiterConfig { +impl Debug for RateLimiterConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { macro_rules! fmt_q { ($quota:expr) => { @@ -104,7 +129,7 @@ impl Debug for OutboundRateLimiterConfig { }; } - f.debug_struct("OutboundRateLimiterConfig") + f.debug_struct("RateLimiterConfig") .field("ping", fmt_q!(&self.ping_quota)) .field("metadata", fmt_q!(&self.meta_data_quota)) .field("status", fmt_q!(&self.status_quota)) @@ -119,7 +144,7 @@ impl Debug for OutboundRateLimiterConfig { /// the default values. Protocol specified more than once use only the first given Quota. /// /// The expected format is a ';' separated list of [`ProtocolQuota`]. -impl FromStr for OutboundRateLimiterConfig { +impl FromStr for RateLimiterConfig { type Err = &'static str; fn from_str(s: &str) -> Result { @@ -129,6 +154,8 @@ impl FromStr for OutboundRateLimiterConfig { let mut goodbye_quota = None; let mut blocks_by_range_quota = None; let mut blocks_by_root_quota = None; + let mut light_client_bootstrap_quota = None; + for proto_def in s.split(';') { let ProtocolQuota { protocol, quota } = proto_def.parse()?; let quota = Some(quota); @@ -139,10 +166,12 @@ impl FromStr for OutboundRateLimiterConfig { Protocol::BlocksByRoot => blocks_by_root_quota = blocks_by_root_quota.or(quota), Protocol::Ping => ping_quota = ping_quota.or(quota), Protocol::MetaData => meta_data_quota = meta_data_quota.or(quota), - Protocol::LightClientBootstrap => return Err("Lighthouse does not send LightClientBootstrap requests. Quota should not be set."), + Protocol::LightClientBootstrap => { + light_client_bootstrap_quota = light_client_bootstrap_quota.or(quota) + } } } - Ok(OutboundRateLimiterConfig { + Ok(RateLimiterConfig { ping_quota: ping_quota.unwrap_or(Self::DEFAULT_PING_QUOTA), meta_data_quota: meta_data_quota.unwrap_or(Self::DEFAULT_META_DATA_QUOTA), status_quota: status_quota.unwrap_or(Self::DEFAULT_STATUS_QUOTA), @@ -151,6 +180,8 @@ impl FromStr for OutboundRateLimiterConfig { .unwrap_or(Self::DEFAULT_BLOCKS_BY_RANGE_QUOTA), blocks_by_root_quota: blocks_by_root_quota .unwrap_or(Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA), + light_client_bootstrap_quota: light_client_bootstrap_quota + .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), }) } } diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index a1743c15f..36a5abc08 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -3,21 +3,19 @@ use super::methods::{GoodbyeReason, RPCCodedResponse, RPCResponseErrorCode, ResponseTermination}; use super::outbound::OutboundRequestContainer; -use super::protocol::{max_rpc_size, InboundRequest, Protocol, RPCError, RPCProtocol}; +use super::protocol::{InboundOutput, InboundRequest, Protocol, RPCError, RPCProtocol}; use super::{RPCReceived, RPCSend, ReqId}; use crate::rpc::outbound::{OutboundFramed, OutboundRequest}; use crate::rpc::protocol::InboundFramed; use fnv::FnvHashMap; use futures::prelude::*; use futures::{Sink, SinkExt}; -use libp2p::core::upgrade::{ - InboundUpgrade, NegotiationError, OutboundUpgrade, ProtocolError, UpgradeError, -}; use libp2p::swarm::handler::{ - ConnectionHandler, ConnectionHandlerEvent, ConnectionHandlerUpgrErr, KeepAlive, + ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, + FullyNegotiatedInbound, FullyNegotiatedOutbound, KeepAlive, StreamUpgradeError, SubstreamProtocol, }; -use libp2p::swarm::NegotiatedSubstream; +use libp2p::swarm::Stream; use slog::{crit, debug, trace, warn}; use smallvec::SmallVec; use std::{ @@ -31,9 +29,6 @@ use tokio::time::{sleep_until, Instant as TInstant, Sleep}; use tokio_util::time::{delay_queue, DelayQueue}; use types::{EthSpec, ForkContext}; -/// The time (in seconds) before a substream that is awaiting a response from the user times out. -pub const RESPONSE_TIMEOUT: u64 = 10; - /// The number of times to retry an outbound upgrade in the case of IO errors. const IO_ERROR_RETRIES: u8 = 3; @@ -47,7 +42,7 @@ const MAX_INBOUND_SUBSTREAMS: usize = 32; #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] pub struct SubstreamId(usize); -type InboundSubstream = InboundFramed; +type InboundSubstream = InboundFramed; /// Events the handler emits to the behaviour. pub type HandlerEvent = Result, HandlerErr>; @@ -131,6 +126,9 @@ where /// Logger for handling RPC streams log: slog::Logger, + + /// Timeout that will me used for inbound and outbound responses. + resp_timeout: Duration, } enum HandlerState { @@ -195,12 +193,12 @@ pub enum OutboundSubstreamState { /// handler because GOODBYE requests can be handled and responses dropped instantly. RequestPendingResponse { /// The framed negotiated substream. - substream: Box>, + substream: Box>, /// Keeps track of the actual request sent. request: OutboundRequest, }, /// Closing an outbound substream> - Closing(Box>), + Closing(Box>), /// Temporary state during processing Poisoned, } @@ -213,6 +211,7 @@ where listen_protocol: SubstreamProtocol, ()>, fork_context: Arc, log: &slog::Logger, + resp_timeout: Duration, ) -> Self { RPCHandler { listen_protocol, @@ -231,6 +230,7 @@ where fork_context, waker: None, log: log.clone(), + resp_timeout, } } @@ -245,7 +245,7 @@ where while let Some((id, req)) = self.dial_queue.pop() { self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })); } @@ -269,7 +269,7 @@ where } _ => self.events_out.push(Err(HandlerErr::Outbound { error: RPCError::Disconnected, - proto: req.protocol(), + proto: req.versioned_protocol().protocol(), id, })), } @@ -315,8 +315,8 @@ where TSpec: EthSpec, Id: ReqId, { - type InEvent = RPCSend; - type OutEvent = HandlerEvent; + type FromBehaviour = RPCSend; + type ToBehaviour = HandlerEvent; type Error = RPCError; type InboundProtocol = RPCProtocol; type OutboundProtocol = OutboundRequestContainer; @@ -327,121 +327,7 @@ where self.listen_protocol.clone() } - fn inject_fully_negotiated_outbound( - &mut self, - out: >::Output, - request_info: Self::OutboundOpenInfo, - ) { - self.dial_negotiated -= 1; - let (id, request) = request_info; - let proto = request.protocol(); - - // accept outbound connections only if the handler is not deactivated - if matches!(self.state, HandlerState::Deactivated) { - self.events_out.push(Err(HandlerErr::Outbound { - error: RPCError::Disconnected, - proto, - id, - })); - } - - // add the stream to substreams if we expect a response, otherwise drop the stream. - let expected_responses = request.expected_responses(); - if expected_responses > 0 { - // new outbound request. Store the stream and tag the output. - let delay_key = self.outbound_substreams_delay.insert( - self.current_outbound_substream_id, - Duration::from_secs(RESPONSE_TIMEOUT), - ); - let awaiting_stream = OutboundSubstreamState::RequestPendingResponse { - substream: Box::new(out), - request, - }; - let expected_responses = if expected_responses > 1 { - // Currently enforced only for multiple responses - Some(expected_responses) - } else { - None - }; - if self - .outbound_substreams - .insert( - self.current_outbound_substream_id, - OutboundInfo { - state: awaiting_stream, - delay_key, - proto, - remaining_chunks: expected_responses, - req_id: id, - }, - ) - .is_some() - { - crit!(self.log, "Duplicate outbound substream id"; "id" => self.current_outbound_substream_id); - } - self.current_outbound_substream_id.0 += 1; - } - } - - fn inject_fully_negotiated_inbound( - &mut self, - substream: >::Output, - _info: Self::InboundOpenInfo, - ) { - // only accept new peer requests when active - if !matches!(self.state, HandlerState::Active) { - return; - } - - let (req, substream) = substream; - let expected_responses = req.expected_responses(); - - // store requests that expect responses - if expected_responses > 0 { - if self.inbound_substreams.len() < MAX_INBOUND_SUBSTREAMS { - // Store the stream and tag the output. - let delay_key = self.inbound_substreams_delay.insert( - self.current_inbound_substream_id, - Duration::from_secs(RESPONSE_TIMEOUT), - ); - let awaiting_stream = InboundState::Idle(substream); - self.inbound_substreams.insert( - self.current_inbound_substream_id, - InboundInfo { - state: awaiting_stream, - pending_items: VecDeque::with_capacity(std::cmp::min( - expected_responses, - 128, - ) as usize), - delay_key: Some(delay_key), - protocol: req.protocol(), - request_start_time: Instant::now(), - remaining_chunks: expected_responses, - }, - ); - } else { - self.events_out.push(Err(HandlerErr::Inbound { - id: self.current_inbound_substream_id, - proto: req.protocol(), - error: RPCError::HandlerRejected, - })); - return self.shutdown(None); - } - } - - // If we received a goodbye, shutdown the connection. - if let InboundRequest::Goodbye(_) = req { - self.shutdown(None); - } - - self.events_out.push(Ok(RPCReceived::Request( - self.current_inbound_substream_id, - req, - ))); - self.current_inbound_substream_id.0 += 1; - } - - fn inject_event(&mut self, rpc_event: Self::InEvent) { + fn on_behaviour_event(&mut self, rpc_event: Self::FromBehaviour) { match rpc_event { RPCSend::Request(id, req) => self.send_request(id, req), RPCSend::Response(inbound_id, response) => self.send_response(inbound_id, response), @@ -453,56 +339,6 @@ where } } - fn inject_dial_upgrade_error( - &mut self, - request_info: Self::OutboundOpenInfo, - error: ConnectionHandlerUpgrErr< - >::Error, - >, - ) { - let (id, req) = request_info; - if let ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(RPCError::IoError(_))) = error - { - self.outbound_io_error_retries += 1; - if self.outbound_io_error_retries < IO_ERROR_RETRIES { - self.send_request(id, req); - return; - } - } - - // This dialing is now considered failed - self.dial_negotiated -= 1; - - self.outbound_io_error_retries = 0; - // map the error - let error = match error { - ConnectionHandlerUpgrErr::Timer => RPCError::InternalError("Timer failed"), - ConnectionHandlerUpgrErr::Timeout => RPCError::NegotiationTimeout, - ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Apply(e)) => e, - ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select(NegotiationError::Failed)) => { - RPCError::UnsupportedProtocol - } - ConnectionHandlerUpgrErr::Upgrade(UpgradeError::Select( - NegotiationError::ProtocolError(e), - )) => match e { - ProtocolError::IoError(io_err) => RPCError::IoError(io_err.to_string()), - ProtocolError::InvalidProtocol => { - RPCError::InternalError("Protocol was deemed invalid") - } - ProtocolError::InvalidMessage | ProtocolError::TooManyProtocols => { - // Peer is sending invalid data during the negotiation phase, not - // participating in the protocol - RPCError::InvalidData("Invalid message during negotiation".to_string()) - } - }, - }; - self.events_out.push(Err(HandlerErr::Outbound { - error, - proto: req.protocol(), - id, - })); - } - fn connection_keep_alive(&self) -> KeepAlive { // Check that we don't have outbound items pending for dialing, nor dialing, nor // established. Also check that there are no established inbound substreams. @@ -535,7 +371,7 @@ where ConnectionHandlerEvent< Self::OutboundProtocol, Self::OutboundOpenInfo, - Self::OutEvent, + Self::ToBehaviour, Self::Error, >, > { @@ -548,7 +384,9 @@ where } // return any events that need to be reported if !self.events_out.is_empty() { - return Poll::Ready(ConnectionHandlerEvent::Custom(self.events_out.remove(0))); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + self.events_out.remove(0), + )); } else { self.events_out.shrink_to_fit(); } @@ -612,7 +450,9 @@ where error: RPCError::StreamTimeout, }; // notify the user - return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Err( + outbound_err, + ))); } else { crit!(self.log, "timed out substream not in the books"; "stream_id" => outbound_id.get_ref()); } @@ -714,7 +554,7 @@ where // 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, Duration::from_secs(RESPONSE_TIMEOUT)); + .reset(delay_key, self.resp_timeout); } // The stream may be currently idle. Attempt to process more @@ -848,7 +688,7 @@ where }; substream_entry.remaining_chunks = Some(remaining_chunks); self.outbound_substreams_delay - .reset(delay_key, Duration::from_secs(RESPONSE_TIMEOUT)); + .reset(delay_key, self.resp_timeout); } } else { // either this is a single response request or this response closes the @@ -872,7 +712,7 @@ where }), }; - return Poll::Ready(ConnectionHandlerEvent::Custom(received)); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(received)); } Poll::Ready(None) => { // stream closed @@ -887,7 +727,7 @@ where // notify the application error if request.expected_responses() > 1 { // return an end of stream result - return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Ok( RPCReceived::EndOfStream(request_id, request.stream_termination()), ))); } @@ -895,10 +735,12 @@ where // else we return an error, stream should not have closed early. let outbound_err = HandlerErr::Outbound { id: request_id, - proto: request.protocol(), + proto: request.versioned_protocol().protocol(), error: RPCError::IncompleteStream, }; - return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Err( + outbound_err, + ))); } Poll::Pending => { entry.get_mut().state = @@ -914,7 +756,9 @@ where error: e, }; entry.remove_entry(); - return Poll::Ready(ConnectionHandlerEvent::Custom(Err(outbound_err))); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Err( + outbound_err, + ))); } }, OutboundSubstreamState::Closing(mut substream) => { @@ -940,7 +784,7 @@ where }; if let Some(termination) = termination { - return Poll::Ready(ConnectionHandlerEvent::Custom(Ok( + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Ok( RPCReceived::EndOfStream(request_id, termination), ))); } @@ -967,7 +811,7 @@ where OutboundRequestContainer { req: req.clone(), fork_context: self.fork_context.clone(), - max_rpc_size: max_rpc_size(&self.fork_context), + max_rpc_size: self.listen_protocol().upgrade().max_rpc_size, }, (), ) @@ -989,6 +833,205 @@ where Poll::Pending } + + fn on_connection_event( + &mut self, + event: ConnectionEvent< + Self::InboundProtocol, + Self::OutboundProtocol, + Self::InboundOpenInfo, + Self::OutboundOpenInfo, + >, + ) { + match event { + ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound { + protocol, + info: _, + }) => self.on_fully_negotiated_inbound(protocol), + ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound { + protocol, + info, + }) => self.on_fully_negotiated_outbound(protocol, info), + ConnectionEvent::DialUpgradeError(DialUpgradeError { info, error }) => { + self.on_dial_upgrade_error(info, error) + } + ConnectionEvent::ListenUpgradeError(libp2p::swarm::handler::ListenUpgradeError { + info: _, + error: _, /* RPCError */ + }) => { + // This is going to be removed in the next libp2p release. I think its fine to do + // nothing. + } + ConnectionEvent::LocalProtocolsChange(_) => { + // This shouldn't effect this handler, we will still negotiate streams if we support + // the protocol as usual. + } + ConnectionEvent::RemoteProtocolsChange(_) => { + // This shouldn't effect this handler, we will still negotiate streams if we support + // the protocol as usual. + } + ConnectionEvent::AddressChange(_) => { + // We dont care about these changes as they have no bearing on our RPC internal + // logic. + } + } + } +} + +impl RPCHandler +where + Id: ReqId, + TSpec: EthSpec, +{ + fn on_fully_negotiated_inbound(&mut self, substream: InboundOutput) { + // only accept new peer requests when active + if !matches!(self.state, HandlerState::Active) { + return; + } + + let (req, substream) = substream; + let expected_responses = req.expected_responses(); + + // store requests that expect responses + if expected_responses > 0 { + if self.inbound_substreams.len() < MAX_INBOUND_SUBSTREAMS { + // Store the stream and tag the output. + let delay_key = self + .inbound_substreams_delay + .insert(self.current_inbound_substream_id, self.resp_timeout); + let awaiting_stream = InboundState::Idle(substream); + self.inbound_substreams.insert( + self.current_inbound_substream_id, + InboundInfo { + state: awaiting_stream, + pending_items: VecDeque::with_capacity(std::cmp::min( + expected_responses, + 128, + ) as usize), + delay_key: Some(delay_key), + protocol: req.versioned_protocol().protocol(), + request_start_time: Instant::now(), + remaining_chunks: expected_responses, + }, + ); + } else { + self.events_out.push(Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto: req.versioned_protocol().protocol(), + error: RPCError::HandlerRejected, + })); + return self.shutdown(None); + } + } + + // If we received a goodbye, shutdown the connection. + if let InboundRequest::Goodbye(_) = req { + self.shutdown(None); + } + + self.events_out.push(Ok(RPCReceived::Request( + self.current_inbound_substream_id, + req, + ))); + self.current_inbound_substream_id.0 += 1; + } + + fn on_fully_negotiated_outbound( + &mut self, + substream: OutboundFramed, + (id, request): (Id, OutboundRequest), + ) { + self.dial_negotiated -= 1; + // Reset any io-retries counter. + self.outbound_io_error_retries = 0; + + let proto = request.versioned_protocol().protocol(); + + // accept outbound connections only if the handler is not deactivated + if matches!(self.state, HandlerState::Deactivated) { + self.events_out.push(Err(HandlerErr::Outbound { + error: RPCError::Disconnected, + proto, + id, + })); + } + + // add the stream to substreams if we expect a response, otherwise drop the stream. + let expected_responses = request.expected_responses(); + if expected_responses > 0 { + // 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); + let awaiting_stream = OutboundSubstreamState::RequestPendingResponse { + substream: Box::new(substream), + request, + }; + let expected_responses = if expected_responses > 1 { + // Currently enforced only for multiple responses + Some(expected_responses) + } else { + None + }; + if self + .outbound_substreams + .insert( + self.current_outbound_substream_id, + OutboundInfo { + state: awaiting_stream, + delay_key, + proto, + remaining_chunks: expected_responses, + req_id: id, + }, + ) + .is_some() + { + crit!(self.log, "Duplicate outbound substream id"; "id" => self.current_outbound_substream_id); + } + self.current_outbound_substream_id.0 += 1; + } + } + fn on_dial_upgrade_error( + &mut self, + request_info: (Id, OutboundRequest), + error: StreamUpgradeError, + ) { + let (id, req) = request_info; + + // map the error + let error = match error { + StreamUpgradeError::Timeout => RPCError::NegotiationTimeout, + StreamUpgradeError::Apply(RPCError::IoError(e)) => { + self.outbound_io_error_retries += 1; + if self.outbound_io_error_retries < IO_ERROR_RETRIES { + self.send_request(id, req); + return; + } + RPCError::IoError(e) + } + StreamUpgradeError::NegotiationFailed => RPCError::UnsupportedProtocol, + StreamUpgradeError::Io(io_err) => { + self.outbound_io_error_retries += 1; + if self.outbound_io_error_retries < IO_ERROR_RETRIES { + self.send_request(id, req); + return; + } + RPCError::IoError(io_err.to_string()) + } + StreamUpgradeError::Apply(other) => other, + }; + + // This dialing is now considered failed + self.dial_negotiated -= 1; + + self.outbound_io_error_retries = 0; + self.events_out.push(Err(HandlerErr::Outbound { + error, + proto: req.versioned_protocol().protocol(), + id, + })); + } } impl slog::Value for SubstreamId { diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 5da595c3d..af0ba2510 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -3,11 +3,13 @@ use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use regex::bytes::Regex; use serde::Serialize; +use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{ typenum::{U1024, U256}, VariableList, }; +use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; @@ -85,6 +87,30 @@ pub struct Ping { pub data: u64, } +/// The METADATA request structure. +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Clone, Debug, PartialEq, Serialize),) +)] +#[derive(Clone, Debug, PartialEq)] +pub struct MetadataRequest { + _phantom_data: PhantomData, +} + +impl MetadataRequest { + pub fn new_v1() -> Self { + Self::V1(MetadataRequestV1 { + _phantom_data: PhantomData, + }) + } + + pub fn new_v2() -> Self { + Self::V2(MetadataRequestV2 { + _phantom_data: PhantomData, + }) + } +} + /// The METADATA response structure. #[superstruct( variants(V1, V2), @@ -93,9 +119,8 @@ pub struct Ping { serde(bound = "T: EthSpec", deny_unknown_fields), ) )] -#[derive(Clone, Debug, PartialEq, Serialize, Encode)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(bound = "T: EthSpec")] -#[ssz(enum_behaviour = "transparent")] pub struct MetaData { /// A sequential counter indicating when data gets modified. pub seq_number: u64, @@ -106,6 +131,38 @@ pub struct MetaData { pub syncnets: EnrSyncCommitteeBitfield, } +impl MetaData { + /// Returns a V1 MetaData response from self. + pub fn metadata_v1(&self) -> Self { + match self { + md @ MetaData::V1(_) => md.clone(), + MetaData::V2(metadata) => MetaData::V1(MetaDataV1 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + }), + } + } + + /// Returns a V2 MetaData response from self by filling unavailable fields with default. + pub fn metadata_v2(&self) -> Self { + match self { + MetaData::V1(metadata) => MetaData::V2(MetaDataV2 { + seq_number: metadata.seq_number, + attnets: metadata.attnets.clone(), + syncnets: Default::default(), + }), + md @ MetaData::V2(_) => md.clone(), + } + } + + pub fn as_ssz_bytes(&self) -> Vec { + match self { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + } + } +} + /// The reason given for a `Goodbye` message. /// /// Note: any unknown `u64::into(n)` will resolve to `Goodbye::Unknown` for any unknown `n`, @@ -197,7 +254,11 @@ impl ssz::Decode for GoodbyeReason { } /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct BlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -206,8 +267,23 @@ pub struct BlocksByRangeRequest { pub count: u64, } +impl BlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64) -> Self { + Self::V2(BlocksByRangeRequestV2 { start_slot, count }) + } + + pub fn new_v1(start_slot: u64, count: u64) -> Self { + Self::V1(BlocksByRangeRequestV1 { start_slot, count }) + } +} + /// Request a number of beacon block roots from a peer. -#[derive(Encode, Decode, Clone, Debug, PartialEq)] +#[superstruct( + variants(V1, V2), + variant_attributes(derive(Encode, Decode, Clone, Debug, PartialEq)) +)] +#[derive(Clone, Debug, PartialEq)] pub struct OldBlocksByRangeRequest { /// The starting slot to request blocks. pub start_slot: u64, @@ -223,13 +299,43 @@ pub struct OldBlocksByRangeRequest { pub step: u64, } +impl OldBlocksByRangeRequest { + /// The default request is V2 + pub fn new(start_slot: u64, count: u64, step: u64) -> Self { + Self::V2(OldBlocksByRangeRequestV2 { + start_slot, + count, + step, + }) + } + + pub fn new_v1(start_slot: u64, count: u64, step: u64) -> Self { + Self::V1(OldBlocksByRangeRequestV1 { + start_slot, + count, + step, + }) + } +} + /// Request a number of beacon block bodies from a peer. +#[superstruct(variants(V1, V2), variant_attributes(derive(Clone, Debug, PartialEq)))] #[derive(Clone, Debug, PartialEq)] pub struct BlocksByRootRequest { /// The list of beacon block bodies being requested. pub block_roots: VariableList, } +impl BlocksByRootRequest { + pub fn new(block_roots: VariableList) -> Self { + Self::V2(BlocksByRootRequestV2 { block_roots }) + } + + pub fn new_v1(block_roots: VariableList) -> Self { + Self::V1(BlocksByRootRequestV1 { block_roots }) + } +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages @@ -438,7 +544,12 @@ impl std::fmt::Display for GoodbyeReason { impl std::fmt::Display for BlocksByRangeRequest { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Start Slot: {}, Count: {}", self.start_slot, self.count) + write!( + f, + "Start Slot: {}, Count: {}", + self.start_slot(), + self.count() + ) } } @@ -447,7 +558,9 @@ impl std::fmt::Display for OldBlocksByRangeRequest { write!( f, "Start Slot: {}, Count: {}, Step: {}", - self.start_slot, self.count, self.step + self.start_slot(), + self.count(), + self.step() ) } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 31569b820..14f77e4ba 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -6,11 +6,11 @@ use futures::future::FutureExt; use handler::{HandlerEvent, RPCHandler}; -use libp2p::core::connection::ConnectionId; use libp2p::swarm::{ - handler::ConnectionHandler, NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, - PollParameters, SubstreamProtocol, + handler::ConnectionHandler, ConnectionId, NetworkBehaviour, NotifyHandler, PollParameters, + ToSwarm, }; +use libp2p::swarm::{FromSwarm, SubstreamProtocol, THandlerInEvent}; use libp2p::PeerId; use rate_limiter::{RPCRateLimiter as RateLimiter, RateLimitedErr}; use slog::{crit, debug, o}; @@ -22,7 +22,7 @@ use types::{EthSpec, ForkContext}; pub(crate) use handler::HandlerErr; pub(crate) use methods::{MetaData, MetaDataV1, MetaDataV2, Ping, RPCCodedResponse, RPCResponse}; -pub(crate) use protocol::{InboundRequest, RPCProtocol}; +pub(crate) use protocol::InboundRequest; pub use handler::SubstreamId; pub use methods::{ @@ -32,7 +32,8 @@ pub use methods::{ pub(crate) use outbound::OutboundRequest; pub use protocol::{max_rpc_size, Protocol, RPCError}; -use self::config::OutboundRateLimiterConfig; +use self::config::{InboundRateLimiterConfig, OutboundRateLimiterConfig}; +use self::protocol::RPCProtocol; use self::self_limiter::SelfRateLimiter; pub(crate) mod codec; @@ -105,14 +106,19 @@ pub struct RPCMessage { pub event: HandlerEvent, } -type BehaviourAction = - NetworkBehaviourAction, RPCHandler>; +type BehaviourAction = ToSwarm, RPCSend>; + +pub struct NetworkParams { + pub max_chunk_size: usize, + pub ttfb_timeout: Duration, + pub resp_timeout: Duration, +} /// Implements the libp2p `NetworkBehaviour` trait and therefore manages network-level /// logic. pub struct RPC { /// Rate limiter - limiter: RateLimiter, + limiter: Option, /// Rate limiter for our own requests. self_limiter: Option>, /// Queue of events to be processed. @@ -121,43 +127,39 @@ pub struct RPC { enable_light_client_server: bool, /// Slog logger for RPC behaviour. log: slog::Logger, + /// Networking constant values + network_params: NetworkParams, } impl RPC { pub fn new( fork_context: Arc, enable_light_client_server: bool, + inbound_rate_limiter_config: Option, outbound_rate_limiter_config: Option, log: slog::Logger, + network_params: NetworkParams, ) -> Self { let log = log.new(o!("service" => "libp2p_rpc")); - let limiter = RateLimiter::builder() - .n_every(Protocol::MetaData, 2, Duration::from_secs(5)) - .n_every(Protocol::Ping, 2, Duration::from_secs(10)) - .n_every(Protocol::Status, 5, Duration::from_secs(15)) - .one_every(Protocol::Goodbye, Duration::from_secs(10)) - .one_every(Protocol::LightClientBootstrap, Duration::from_secs(10)) - .n_every( - Protocol::BlocksByRange, - methods::MAX_REQUEST_BLOCKS, - Duration::from_secs(10), - ) - .n_every(Protocol::BlocksByRoot, 128, Duration::from_secs(10)) - .build() - .expect("Configuration parameters are valid"); + let inbound_limiter = inbound_rate_limiter_config.map(|config| { + debug!(log, "Using inbound rate limiting params"; "config" => ?config); + RateLimiter::new_with_config(config.0) + .expect("Inbound limiter configuration parameters are valid") + }); let self_limiter = outbound_rate_limiter_config.map(|config| { SelfRateLimiter::new(config, log.clone()).expect("Configuration parameters are valid") }); RPC { - limiter, + limiter: inbound_limiter, self_limiter, events: Vec::new(), fork_context, enable_light_client_server, log, + network_params, } } @@ -170,7 +172,7 @@ impl RPC { id: (ConnectionId, SubstreamId), event: RPCCodedResponse, ) { - self.events.push(NetworkBehaviourAction::NotifyHandler { + self.events.push(ToSwarm::NotifyHandler { peer_id, handler: NotifyHandler::One(id.0), event: RPCSend::Response(id.1, event), @@ -190,7 +192,7 @@ impl RPC { } } } else { - NetworkBehaviourAction::NotifyHandler { + ToSwarm::NotifyHandler { peer_id, handler: NotifyHandler::Any, event: RPCSend::Request(request_id, req), @@ -203,7 +205,7 @@ impl RPC { /// Lighthouse wishes to disconnect from this peer by sending a Goodbye message. This /// gracefully terminates the RPC behaviour with a goodbye message. pub fn shutdown(&mut self, peer_id: PeerId, id: Id, reason: GoodbyeReason) { - self.events.push(NetworkBehaviourAction::NotifyHandler { + self.events.push(ToSwarm::NotifyHandler { peer_id, handler: NotifyHandler::Any, event: RPCSend::Shutdown(id, reason), @@ -217,83 +219,156 @@ where Id: ReqId, { type ConnectionHandler = RPCHandler; - type OutEvent = RPCMessage; + type ToSwarm = RPCMessage; - fn new_handler(&mut self) -> Self::ConnectionHandler { - RPCHandler::new( - SubstreamProtocol::new( - RPCProtocol { - fork_context: self.fork_context.clone(), - max_rpc_size: max_rpc_size(&self.fork_context), - enable_light_client_server: self.enable_light_client_server, - phantom: PhantomData, - }, - (), - ), + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + peer_id: PeerId, + _local_addr: &libp2p::Multiaddr, + _remote_addr: &libp2p::Multiaddr, + ) -> Result, libp2p::swarm::ConnectionDenied> { + let protocol = SubstreamProtocol::new( + RPCProtocol { + fork_context: self.fork_context.clone(), + max_rpc_size: max_rpc_size(&self.fork_context, self.network_params.max_chunk_size), + enable_light_client_server: self.enable_light_client_server, + phantom: PhantomData, + ttfb_timeout: self.network_params.ttfb_timeout, + }, + (), + ); + // NOTE: this is needed because PeerIds have interior mutability. + let peer_repr = peer_id.to_string(); + let log = self.log.new(slog::o!("peer_id" => peer_repr)); + let handler = RPCHandler::new( + protocol, self.fork_context.clone(), - &self.log, - ) + &log, + self.network_params.resp_timeout, + ); + + Ok(handler) } - fn inject_event( + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + peer_id: PeerId, + _addr: &libp2p::Multiaddr, + _role_override: libp2p::core::Endpoint, + ) -> Result, libp2p::swarm::ConnectionDenied> { + let protocol = SubstreamProtocol::new( + RPCProtocol { + fork_context: self.fork_context.clone(), + max_rpc_size: max_rpc_size(&self.fork_context, self.network_params.max_chunk_size), + enable_light_client_server: self.enable_light_client_server, + phantom: PhantomData, + ttfb_timeout: self.network_params.ttfb_timeout, + }, + (), + ); + + // NOTE: this is needed because PeerIds have interior mutability. + let peer_repr = peer_id.to_string(); + let log = self.log.new(slog::o!("peer_id" => peer_repr)); + let handler = RPCHandler::new( + protocol, + self.fork_context.clone(), + &log, + self.network_params.resp_timeout, + ); + + Ok(handler) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionClosed(_) + | FromSwarm::ConnectionEstablished(_) + | FromSwarm::AddressChange(_) + | FromSwarm::DialFailure(_) + | FromSwarm::ListenFailure(_) + | FromSwarm::NewListener(_) + | FromSwarm::NewListenAddr(_) + | FromSwarm::ExpiredListenAddr(_) + | FromSwarm::ListenerError(_) + | FromSwarm::ListenerClosed(_) + | FromSwarm::NewExternalAddrCandidate(_) + | FromSwarm::ExternalAddrExpired(_) + | FromSwarm::ExternalAddrConfirmed(_) => { + // Rpc Behaviour does not act on these swarm events. We use a comprehensive match + // statement to ensure future events are dealt with appropriately. + } + } + } + + fn on_connection_handler_event( &mut self, peer_id: PeerId, conn_id: ConnectionId, - event: ::OutEvent, + event: ::ToBehaviour, ) { if let Ok(RPCReceived::Request(ref id, ref req)) = event { - // check if the request is conformant to the quota - match self.limiter.allows(&peer_id, req) { - Ok(()) => { - // send the event to the user - self.events - .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + if let Some(limiter) = self.limiter.as_mut() { + // check if the request is conformant to the quota + match limiter.allows(&peer_id, req) { + Ok(()) => { + // send the event to the user + self.events.push(ToSwarm::GenerateEvent(RPCMessage { peer_id, conn_id, event, })) - } - Err(RateLimitedErr::TooLarge) => { - // we set the batch sizes, so this is a coding/config err for most protocols - let protocol = req.protocol(); - if matches!(protocol, Protocol::BlocksByRange) { - debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); - } else { - crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); } - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, *id), - RPCCodedResponse::Error( - RPCResponseErrorCode::RateLimited, - "Rate limited. Request too large".into(), - ), - ); - } - Err(RateLimitedErr::TooSoon(wait_time)) => { - debug!(self.log, "Request exceeds the rate limit"; + Err(RateLimitedErr::TooLarge) => { + // we set the batch sizes, so this is a coding/config err for most protocols + let protocol = req.versioned_protocol().protocol(); + if matches!(protocol, Protocol::BlocksByRange) { + debug!(self.log, "Blocks by range request will never be processed"; "request" => %req); + } else { + crit!(self.log, "Request size too large to ever be processed"; "protocol" => %protocol); + } + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + "Rate limited. Request too large".into(), + ), + ); + } + Err(RateLimitedErr::TooSoon(wait_time)) => { + debug!(self.log, "Request exceeds the rate limit"; "request" => %req, "peer_id" => %peer_id, "wait_time_ms" => wait_time.as_millis()); - // send an error code to the peer. - // the handler upon receiving the error code will send it back to the behaviour - self.send_response( - peer_id, - (conn_id, *id), - RPCCodedResponse::Error( - RPCResponseErrorCode::RateLimited, - format!("Wait {:?}", wait_time).into(), - ), - ); + // send an error code to the peer. + // the handler upon receiving the error code will send it back to the behaviour + self.send_response( + peer_id, + (conn_id, *id), + RPCCodedResponse::Error( + RPCResponseErrorCode::RateLimited, + format!("Wait {:?}", wait_time).into(), + ), + ); + } } - } - } else { - self.events - .push(NetworkBehaviourAction::GenerateEvent(RPCMessage { + } else { + // No rate limiting, send the event to the user + self.events.push(ToSwarm::GenerateEvent(RPCMessage { peer_id, conn_id, event, - })); + })) + } + } else { + self.events.push(ToSwarm::GenerateEvent(RPCMessage { + peer_id, + conn_id, + event, + })); } } @@ -301,9 +376,11 @@ where &mut self, cx: &mut Context, _: &mut impl PollParameters, - ) -> Poll> { + ) -> Poll>> { // let the rate limiter prune. - let _ = self.limiter.poll_unpin(cx); + if let Some(limiter) = self.limiter.as_mut() { + let _ = limiter.poll_unpin(cx); + } if let Some(self_limiter) = self.self_limiter.as_mut() { if let Poll::Ready(event) = self_limiter.poll_ready(cx) { @@ -332,7 +409,7 @@ where serializer.emit_arguments("peer_id", &format_args!("{}", self.peer_id))?; let (msg_kind, protocol) = match &self.event { Ok(received) => match received { - RPCReceived::Request(_, req) => ("request", req.protocol()), + RPCReceived::Request(_, req) => ("request", req.versioned_protocol().protocol()), RPCReceived::Response(_, res) => ("response", res.protocol()), RPCReceived::EndOfStream(_, end) => ( "end_of_stream", diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 774303800..d12f36686 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -1,11 +1,8 @@ -use std::marker::PhantomData; - use super::methods::*; -use super::protocol::Protocol; use super::protocol::ProtocolId; +use super::protocol::SupportedProtocol; use super::RPCError; use crate::rpc::protocol::Encoding; -use crate::rpc::protocol::Version; use crate::rpc::{ codec::{base::BaseOutboundCodec, ssz_snappy::SSZSnappyOutboundCodec, OutboundCodec}, methods::ResponseTermination, @@ -38,9 +35,8 @@ pub enum OutboundRequest { Goodbye(GoodbyeReason), BlocksByRange(OldBlocksByRangeRequest), BlocksByRoot(BlocksByRootRequest), - LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } impl UpgradeInfo for OutboundRequestContainer { @@ -59,36 +55,29 @@ impl OutboundRequest { match self { // add more protocols when versions/encodings are supported OutboundRequest::Status(_) => vec![ProtocolId::new( - Protocol::Status, - Version::V1, + SupportedProtocol::StatusV1, Encoding::SSZSnappy, )], OutboundRequest::Goodbye(_) => vec![ProtocolId::new( - Protocol::Goodbye, - Version::V1, + SupportedProtocol::GoodbyeV1, Encoding::SSZSnappy, )], OutboundRequest::BlocksByRange(_) => vec![ - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy), ], OutboundRequest::BlocksByRoot(_) => vec![ - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::BlocksByRootV1, Encoding::SSZSnappy), ], OutboundRequest::Ping(_) => vec![ProtocolId::new( - Protocol::Ping, - Version::V1, + SupportedProtocol::PingV1, Encoding::SSZSnappy, )], OutboundRequest::MetaData(_) => vec![ - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::MetaDataV1, Encoding::SSZSnappy), ], - // Note: This match arm is technically unreachable as we only respond to light client requests - // that we generate from the beacon state. - // We do not make light client rpc requests from the beacon node - OutboundRequest::LightClientBootstrap(_) => vec![], } } /* These functions are used in the handler for stream management */ @@ -98,24 +87,31 @@ impl OutboundRequest { match self { OutboundRequest::Status(_) => 1, OutboundRequest::Goodbye(_) => 0, - OutboundRequest::BlocksByRange(req) => req.count, - OutboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + OutboundRequest::BlocksByRange(req) => *req.count(), + OutboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, - OutboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - OutboundRequest::Status(_) => Protocol::Status, - OutboundRequest::Goodbye(_) => Protocol::Goodbye, - OutboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - OutboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - OutboundRequest::Ping(_) => Protocol::Ping, - OutboundRequest::MetaData(_) => Protocol::MetaData, - OutboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + OutboundRequest::Status(_) => SupportedProtocol::StatusV1, + OutboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + OutboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + OutboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + OutboundRequest::Ping(_) => SupportedProtocol::PingV1, + OutboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, } } @@ -127,7 +123,6 @@ impl OutboundRequest { // variants that have `multiple_responses()` can have values. OutboundRequest::BlocksByRange(_) => ResponseTermination::BlocksByRange, OutboundRequest::BlocksByRoot(_) => ResponseTermination::BlocksByRoot, - OutboundRequest::LightClientBootstrap(_) => unreachable!(), OutboundRequest::Status(_) => unreachable!(), OutboundRequest::Goodbye(_) => unreachable!(), OutboundRequest::Ping(_) => unreachable!(), @@ -185,9 +180,6 @@ impl std::fmt::Display for OutboundRequest { OutboundRequest::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req), OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), OutboundRequest::MetaData(_) => write!(f, "MetaData request"), - OutboundRequest::LightClientBootstrap(bootstrap) => { - write!(f, "Lightclient Bootstrap: {}", bootstrap.root) - } } } } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index a8423e47b..f2a39470b 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -7,7 +7,7 @@ use crate::rpc::{ use futures::future::BoxFuture; use futures::prelude::{AsyncRead, AsyncWrite}; use futures::{FutureExt, StreamExt}; -use libp2p::core::{InboundUpgrade, ProtocolName, UpgradeInfo}; +use libp2p::core::{InboundUpgrade, UpgradeInfo}; use ssz::Encode; use ssz_types::VariableList; use std::io; @@ -72,7 +72,7 @@ lazy_static! { /// The `BeaconBlockMerge` block has an `ExecutionPayload` field which has a max size ~16 GiB for future proofing. /// We calculate the value from its fields instead of constructing the block and checking the length. /// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network - /// with `MAX_RPC_SIZE_POST_MERGE`. + /// with `max_chunk_size`. pub static ref SIGNED_BEACON_BLOCK_MERGE_MAX: usize = // Size of a full altair block *SIGNED_BEACON_BLOCK_ALTAIR_MAX @@ -109,25 +109,18 @@ lazy_static! { .len(); } -/// The maximum bytes that can be sent across the RPC pre-merge. -pub(crate) const MAX_RPC_SIZE: usize = 1_048_576; // 1M -/// The maximum bytes that can be sent across the RPC post-merge. -pub(crate) const MAX_RPC_SIZE_POST_MERGE: usize = 10 * 1_048_576; // 10M -pub(crate) const MAX_RPC_SIZE_POST_CAPELLA: usize = 10 * 1_048_576; // 10M /// The protocol prefix the RPC protocol id. const PROTOCOL_PREFIX: &str = "/eth2/beacon_chain/req"; -/// Time allowed for the first byte of a request to arrive before we time out (Time To First Byte). -const TTFB_TIMEOUT: u64 = 5; /// The number of seconds to wait for the first bytes of a request once a protocol has been /// established before the stream is terminated. const REQUEST_TIMEOUT: u64 = 15; /// Returns the maximum bytes that can be sent across the RPC. -pub fn max_rpc_size(fork_context: &ForkContext) -> usize { +pub fn max_rpc_size(fork_context: &ForkContext, max_chunk_size: usize) -> usize { match fork_context.current_fork() { - ForkName::Altair | ForkName::Base => MAX_RPC_SIZE, - ForkName::Merge => MAX_RPC_SIZE_POST_MERGE, - ForkName::Capella => MAX_RPC_SIZE_POST_CAPELLA, + ForkName::Altair | ForkName::Base => max_chunk_size / 10, + ForkName::Merge => max_chunk_size, + ForkName::Capella => max_chunk_size, } } @@ -179,21 +172,74 @@ pub enum Protocol { LightClientBootstrap, } -/// RPC Versions -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Version { - /// Version 1 of RPC - V1, - /// Version 2 of RPC - V2, -} - /// RPC Encondings supported. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { SSZSnappy, } +/// All valid protocol name and version combinations. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SupportedProtocol { + StatusV1, + GoodbyeV1, + BlocksByRangeV1, + BlocksByRangeV2, + BlocksByRootV1, + BlocksByRootV2, + PingV1, + MetaDataV1, + MetaDataV2, + LightClientBootstrapV1, +} + +impl SupportedProtocol { + pub fn version_string(&self) -> &'static str { + match self { + SupportedProtocol::StatusV1 => "1", + SupportedProtocol::GoodbyeV1 => "1", + SupportedProtocol::BlocksByRangeV1 => "1", + SupportedProtocol::BlocksByRangeV2 => "2", + SupportedProtocol::BlocksByRootV1 => "1", + SupportedProtocol::BlocksByRootV2 => "2", + SupportedProtocol::PingV1 => "1", + SupportedProtocol::MetaDataV1 => "1", + SupportedProtocol::MetaDataV2 => "2", + SupportedProtocol::LightClientBootstrapV1 => "1", + } + } + + pub fn protocol(&self) -> Protocol { + match self { + SupportedProtocol::StatusV1 => Protocol::Status, + SupportedProtocol::GoodbyeV1 => Protocol::Goodbye, + SupportedProtocol::BlocksByRangeV1 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRangeV2 => Protocol::BlocksByRange, + SupportedProtocol::BlocksByRootV1 => Protocol::BlocksByRoot, + SupportedProtocol::BlocksByRootV2 => Protocol::BlocksByRoot, + SupportedProtocol::PingV1 => Protocol::Ping, + SupportedProtocol::MetaDataV1 => Protocol::MetaData, + SupportedProtocol::MetaDataV2 => Protocol::MetaData, + SupportedProtocol::LightClientBootstrapV1 => Protocol::LightClientBootstrap, + } + } + + fn currently_supported() -> Vec { + vec![ + ProtocolId::new(Self::StatusV1, Encoding::SSZSnappy), + ProtocolId::new(Self::GoodbyeV1, Encoding::SSZSnappy), + // V2 variants have higher preference then V1 + ProtocolId::new(Self::BlocksByRangeV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRangeV1, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV2, Encoding::SSZSnappy), + ProtocolId::new(Self::BlocksByRootV1, Encoding::SSZSnappy), + ProtocolId::new(Self::PingV1, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV2, Encoding::SSZSnappy), + ProtocolId::new(Self::MetaDataV1, Encoding::SSZSnappy), + ] + } +} + impl std::fmt::Display for Encoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let repr = match self { @@ -203,22 +249,13 @@ impl std::fmt::Display for Encoding { } } -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let repr = match self { - Version::V1 => "1", - Version::V2 => "2", - }; - f.write_str(repr) - } -} - #[derive(Debug, Clone)] pub struct RPCProtocol { pub fork_context: Arc, pub max_rpc_size: usize, pub enable_light_client_server: bool, pub phantom: PhantomData, + pub ttfb_timeout: Duration, } impl UpgradeInfo for RPCProtocol { @@ -227,22 +264,10 @@ impl UpgradeInfo for RPCProtocol { /// The list of supported RPC protocols for Lighthouse. fn protocol_info(&self) -> Self::InfoIter { - let mut supported_protocols = vec![ - ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy), - // V2 variants have higher preference then V1 - ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy), - ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy), - ]; + let mut supported_protocols = SupportedProtocol::currently_supported(); if self.enable_light_client_server { supported_protocols.push(ProtocolId::new( - Protocol::LightClientBootstrap, - Version::V1, + SupportedProtocol::LightClientBootstrapV1, Encoding::SSZSnappy, )); } @@ -272,11 +297,8 @@ impl RpcLimits { /// Tracks the types in a protocol id. #[derive(Clone, Debug)] pub struct ProtocolId { - /// The RPC message type/name. - pub message_name: Protocol, - - /// The version of the RPC. - pub version: Version, + /// The protocol name and version + pub versioned_protocol: SupportedProtocol, /// The encoding of the RPC. pub encoding: Encoding, @@ -285,10 +307,16 @@ pub struct ProtocolId { protocol_id: String, } +impl AsRef for ProtocolId { + fn as_ref(&self) -> &str { + self.protocol_id.as_ref() + } +} + impl ProtocolId { /// Returns min and max size for messages of given protocol id requests. pub fn rpc_request_limits(&self) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -297,9 +325,10 @@ impl ProtocolId { ::ssz_fixed_len(), ::ssz_fixed_len(), ), + // V1 and V2 requests are the same Protocol::BlocksByRange => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + ::ssz_fixed_len(), + ::ssz_fixed_len(), ), Protocol::BlocksByRoot => { RpcLimits::new(*BLOCKS_BY_ROOT_REQUEST_MIN, *BLOCKS_BY_ROOT_REQUEST_MAX) @@ -318,7 +347,7 @@ impl ProtocolId { /// Returns min and max size for messages of given protocol id responses. pub fn rpc_response_limits(&self, fork_context: &ForkContext) -> RpcLimits { - match self.message_name { + match self.versioned_protocol.protocol() { Protocol::Status => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -344,42 +373,40 @@ impl ProtocolId { /// Returns `true` if the given `ProtocolId` should expect `context_bytes` in the /// beginning of the stream, else returns `false`. pub fn has_context_bytes(&self) -> bool { - match self.message_name { - Protocol::BlocksByRange | Protocol::BlocksByRoot => match self.version { - Version::V2 => true, - Version::V1 => false, - }, - Protocol::LightClientBootstrap => match self.version { - Version::V2 | Version::V1 => true, - }, - Protocol::Goodbye | Protocol::Ping | Protocol::Status | Protocol::MetaData => false, + match self.versioned_protocol { + SupportedProtocol::BlocksByRangeV2 + | SupportedProtocol::BlocksByRootV2 + | SupportedProtocol::LightClientBootstrapV1 => true, + SupportedProtocol::StatusV1 + | SupportedProtocol::BlocksByRootV1 + | SupportedProtocol::BlocksByRangeV1 + | SupportedProtocol::PingV1 + | SupportedProtocol::MetaDataV1 + | SupportedProtocol::MetaDataV2 + | SupportedProtocol::GoodbyeV1 => false, } } } /// An RPC protocol ID. impl ProtocolId { - pub fn new(message_name: Protocol, version: Version, encoding: Encoding) -> Self { + pub fn new(versioned_protocol: SupportedProtocol, encoding: Encoding) -> Self { let protocol_id = format!( "{}/{}/{}/{}", - PROTOCOL_PREFIX, message_name, version, encoding + PROTOCOL_PREFIX, + versioned_protocol.protocol(), + versioned_protocol.version_string(), + encoding ); ProtocolId { - message_name, - version, + versioned_protocol, encoding, protocol_id, } } } -impl ProtocolName for ProtocolId { - fn protocol_name(&self) -> &[u8] { - self.protocol_id.as_bytes() - } -} - /* Inbound upgrade */ // The inbound protocol reads the request, decodes it and returns the stream to the protocol @@ -400,7 +427,7 @@ where fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future { async move { - let protocol_name = protocol.message_name; + let versioned_protocol = protocol.versioned_protocol; // convert the socket to tokio compatible socket let socket = socket.compat(); let codec = match protocol.encoding { @@ -414,13 +441,18 @@ where } }; let mut timed_socket = TimeoutStream::new(socket); - timed_socket.set_read_timeout(Some(Duration::from_secs(TTFB_TIMEOUT))); + timed_socket.set_read_timeout(Some(self.ttfb_timeout)); let socket = Framed::new(Box::pin(timed_socket), codec); // MetaData requests should be empty, return the stream - match protocol_name { - Protocol::MetaData => Ok((InboundRequest::MetaData(PhantomData), socket)), + match versioned_protocol { + SupportedProtocol::MetaDataV1 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v1()), socket)) + } + SupportedProtocol::MetaDataV2 => { + Ok((InboundRequest::MetaData(MetadataRequest::new_v2()), socket)) + } _ => { match tokio::time::timeout( Duration::from_secs(REQUEST_TIMEOUT), @@ -448,7 +480,7 @@ pub enum InboundRequest { BlocksByRoot(BlocksByRootRequest), LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), - MetaData(PhantomData), + MetaData(MetadataRequest), } /// Implements the encoding per supported protocol for `RPCRequest`. @@ -460,24 +492,33 @@ impl InboundRequest { match self { InboundRequest::Status(_) => 1, InboundRequest::Goodbye(_) => 0, - InboundRequest::BlocksByRange(req) => req.count, - InboundRequest::BlocksByRoot(req) => req.block_roots.len() as u64, + InboundRequest::BlocksByRange(req) => *req.count(), + InboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, } } - /// Gives the corresponding `Protocol` to this request. - pub fn protocol(&self) -> Protocol { + /// Gives the corresponding `SupportedProtocol` to this request. + pub fn versioned_protocol(&self) -> SupportedProtocol { match self { - InboundRequest::Status(_) => Protocol::Status, - InboundRequest::Goodbye(_) => Protocol::Goodbye, - InboundRequest::BlocksByRange(_) => Protocol::BlocksByRange, - InboundRequest::BlocksByRoot(_) => Protocol::BlocksByRoot, - InboundRequest::Ping(_) => Protocol::Ping, - InboundRequest::MetaData(_) => Protocol::MetaData, - InboundRequest::LightClientBootstrap(_) => Protocol::LightClientBootstrap, + InboundRequest::Status(_) => SupportedProtocol::StatusV1, + InboundRequest::Goodbye(_) => SupportedProtocol::GoodbyeV1, + InboundRequest::BlocksByRange(req) => match req { + OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1, + OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2, + }, + InboundRequest::BlocksByRoot(req) => match req { + BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1, + BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2, + }, + InboundRequest::Ping(_) => SupportedProtocol::PingV1, + InboundRequest::MetaData(req) => match req { + MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, + MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2, + }, + InboundRequest::LightClientBootstrap(_) => SupportedProtocol::LightClientBootstrapV1, } } diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index a1f7b89a2..e1634d711 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -1,3 +1,4 @@ +use super::config::RateLimiterConfig; use crate::rpc::Protocol; use fnv::FnvHashMap; use libp2p::PeerId; @@ -141,29 +142,6 @@ impl RPCRateLimiterBuilder { self } - /// Allow one token every `time_period` to be used for this `protocol`. - /// This produces a hard limit. - pub fn one_every(self, protocol: Protocol, time_period: Duration) -> Self { - self.set_quota( - protocol, - Quota { - replenish_all_every: time_period, - max_tokens: 1, - }, - ) - } - - /// Allow `n` tokens to be use used every `time_period` for this `protocol`. - pub fn n_every(self, protocol: Protocol, n: u64, time_period: Duration) -> Self { - self.set_quota( - protocol, - Quota { - max_tokens: n, - replenish_all_every: time_period, - }, - ) - } - pub fn build(self) -> Result { // get our quotas let ping_quota = self.ping_quota.ok_or("Ping quota not specified")?; @@ -214,7 +192,7 @@ pub trait RateLimiterItem { impl RateLimiterItem for super::InboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { @@ -224,7 +202,7 @@ impl RateLimiterItem for super::InboundRequest { impl RateLimiterItem for super::OutboundRequest { fn protocol(&self) -> Protocol { - self.protocol() + self.versioned_protocol().protocol() } fn expected_responses(&self) -> u64 { @@ -232,6 +210,29 @@ impl RateLimiterItem for super::OutboundRequest { } } impl RPCRateLimiter { + pub fn new_with_config(config: RateLimiterConfig) -> Result { + // Destructure to make sure every configuration value is used. + let RateLimiterConfig { + ping_quota, + meta_data_quota, + status_quota, + goodbye_quota, + blocks_by_range_quota, + blocks_by_root_quota, + light_client_bootstrap_quota, + } = config; + + Self::builder() + .set_quota(Protocol::Ping, ping_quota) + .set_quota(Protocol::MetaData, meta_data_quota) + .set_quota(Protocol::Status, status_quota) + .set_quota(Protocol::Goodbye, goodbye_quota) + .set_quota(Protocol::BlocksByRange, blocks_by_range_quota) + .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) + .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) + .build() + } + /// Get a builder instance. pub fn builder() -> RPCRateLimiterBuilder { RPCRateLimiterBuilder::default() diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index 451c6206f..4348c1ec6 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -52,28 +52,7 @@ impl SelfRateLimiter { /// Creates a new [`SelfRateLimiter`] based on configration values. pub fn new(config: OutboundRateLimiterConfig, log: Logger) -> Result { debug!(log, "Using self rate limiting params"; "config" => ?config); - // Destructure to make sure every configuration value is used. - let OutboundRateLimiterConfig { - ping_quota, - meta_data_quota, - status_quota, - goodbye_quota, - blocks_by_range_quota, - blocks_by_root_quota, - } = config; - - let limiter = RateLimiter::builder() - .set_quota(Protocol::Ping, ping_quota) - .set_quota(Protocol::MetaData, meta_data_quota) - .set_quota(Protocol::Status, status_quota) - .set_quota(Protocol::Goodbye, goodbye_quota) - .set_quota(Protocol::BlocksByRange, blocks_by_range_quota) - .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) - // Manually set the LightClientBootstrap quota, since we use the same rate limiter for - // inbound and outbound requests, and the LightClientBootstrap is an only inbound - // protocol. - .one_every(Protocol::LightClientBootstrap, Duration::from_secs(10)) - .build()?; + let limiter = RateLimiter::new_with_config(config.0)?; Ok(SelfRateLimiter { delayed_requests: Default::default(), @@ -85,7 +64,7 @@ impl SelfRateLimiter { } /// Checks if the rate limiter allows the request. If it's allowed, returns the - /// [`NetworkBehaviourAction`] that should be emitted. When not allowed, the request is delayed + /// [`ToSwarm`] that should be emitted. When not allowed, the request is delayed /// until it can be sent. pub fn allows( &mut self, @@ -93,7 +72,7 @@ impl SelfRateLimiter { request_id: Id, req: OutboundRequest, ) -> Result, Error> { - let protocol = req.protocol(); + let protocol = req.versioned_protocol().protocol(); // First check that there are not already other requests waiting to be sent. if let Some(queued_requests) = self.delayed_requests.get_mut(&(peer_id, protocol)) { queued_requests.push_back(QueuedRequest { req, request_id }); @@ -116,7 +95,7 @@ impl SelfRateLimiter { } /// Auxiliary function to deal with self rate limiting outcomes. If the rate limiter allows the - /// request, the [`NetworkBehaviourAction`] that should be emitted is returned. If the request + /// request, the [`ToSwarm`] that should be emitted is returned. If the request /// should be delayed, it's returned with the duration to wait. fn try_send_request( limiter: &mut RateLimiter, @@ -132,7 +111,7 @@ impl SelfRateLimiter { event: RPCSend::Request(request_id, req), }), Err(e) => { - let protocol = req.protocol(); + let protocol = req.versioned_protocol(); match e { RateLimitedErr::TooLarge => { // this should never happen with default parameters. Let's just send the request. @@ -140,7 +119,7 @@ impl SelfRateLimiter { crit!( log, "Self rate limiting error for a batch that will never fit. Sending request anyway. Check configuration parameters."; - "protocol" => %req.protocol() + "protocol" => %req.versioned_protocol().protocol() ); Ok(BehaviourAction::NotifyHandler { peer_id, @@ -149,7 +128,7 @@ impl SelfRateLimiter { }) } RateLimitedErr::TooSoon(wait_time) => { - debug!(log, "Self rate limiting"; "protocol" => %protocol, "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); + debug!(log, "Self rate limiting"; "protocol" => %protocol.protocol(), "wait_time_ms" => wait_time.as_millis(), "peer_id" => %peer_id); Err((QueuedRequest { req, request_id }, wait_time)) } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index bd3df7976..187c0ab1b 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,13 +1,14 @@ use std::sync::Arc; -use libp2p::core::connection::ConnectionId; +use libp2p::swarm::ConnectionId; use types::light_client_bootstrap::LightClientBootstrap; use types::{EthSpec, SignedBeaconBlock}; use crate::rpc::{ methods::{ BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, - OldBlocksByRangeRequest, RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, + OldBlocksByRangeRequest, OldBlocksByRangeRequestV1, OldBlocksByRangeRequestV2, + RPCCodedResponse, RPCResponse, ResponseTermination, StatusMessage, }, OutboundRequest, SubstreamId, }; @@ -43,14 +44,25 @@ impl std::convert::From for OutboundRequest { fn from(req: Request) -> OutboundRequest { match req { Request::BlocksByRoot(r) => OutboundRequest::BlocksByRoot(r), - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }) => { - OutboundRequest::BlocksByRange(OldBlocksByRangeRequest { - start_slot, - count, - step: 1, - }) + Request::BlocksByRange(r) => match r { + BlocksByRangeRequest::V1(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V1(OldBlocksByRangeRequestV1 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + BlocksByRangeRequest::V2(req) => OutboundRequest::BlocksByRange( + OldBlocksByRangeRequest::V2(OldBlocksByRangeRequestV2 { + start_slot: req.start_slot, + count: req.count, + step: 1, + }), + ), + }, + Request::LightClientBootstrap(_) => { + unreachable!("Lighthouse never makes an outbound light client request") } - Request::LightClientBootstrap(b) => OutboundRequest::LightClientBootstrap(b), Request::Status(s) => OutboundRequest::Status(s), } } diff --git a/beacon_node/lighthouse_network/src/service/behaviour.rs b/beacon_node/lighthouse_network/src/service/behaviour.rs index 7d20b87ad..6c52a07c1 100644 --- a/beacon_node/lighthouse_network/src/service/behaviour.rs +++ b/beacon_node/lighthouse_network/src/service/behaviour.rs @@ -3,21 +3,27 @@ use crate::peer_manager::PeerManager; use crate::rpc::{ReqId, RPC}; use crate::types::SnappyTransform; -use libp2p::gossipsub::subscription_filter::{ - MaxCountSubscriptionFilter, WhitelistSubscriptionFilter, -}; -use libp2p::gossipsub::Gossipsub as BaseGossipsub; -use libp2p::identify::Behaviour as Identify; +use libp2p::gossipsub; +use libp2p::identify; use libp2p::swarm::NetworkBehaviour; use types::EthSpec; use super::api_types::RequestId; -pub type SubscriptionFilter = MaxCountSubscriptionFilter; -pub type Gossipsub = BaseGossipsub; +pub type SubscriptionFilter = + gossipsub::MaxCountSubscriptionFilter; +pub type Gossipsub = gossipsub::Behaviour; #[derive(NetworkBehaviour)] -pub(crate) struct Behaviour { +pub(crate) struct Behaviour +where + AppReqId: ReqId, + TSpec: EthSpec, +{ + /// Peers banned. + pub banned_peers: libp2p::allow_block_list::Behaviour, + /// Keep track of active and pending connections to enforce hard limits. + pub connection_limits: libp2p::connection_limits::Behaviour, /// The routing pub-sub mechanism for eth2. pub gossipsub: Gossipsub, /// The Eth2 RPC specified in the wire-0 protocol. @@ -27,7 +33,7 @@ pub(crate) struct Behaviour { /// Keep regular connection to peers and disconnect if absent. // NOTE: The id protocol is used for initial interop. This will be removed by mainnet. /// Provides IP addresses and peer information. - pub identify: Identify, + pub identify: identify::Behaviour, /// The peer manager that keeps track of peer's reputation and status. pub peer_manager: PeerManager, } 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 88becd686..b058fc0ff 100644 --- a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs +++ b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs @@ -1,7 +1,8 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::{error, TopicHash}; use libp2p::gossipsub::{ - GossipsubConfig, IdentTopic as Topic, PeerScoreParams, PeerScoreThresholds, TopicScoreParams, + Config as GossipsubConfig, 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 f815e3bd3..e4e11f29c 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1,6 +1,6 @@ use self::behaviour::Behaviour; use self::gossip_cache::GossipCache; -use crate::config::{gossipsub_config, NetworkLoad}; +use crate::config::{gossipsub_config, GossipsubConfigParams, NetworkLoad}; use crate::discovery::{ subnet_predicate, DiscoveredPeers, Discovery, FIND_NODE_QUERY_CLOSEST_PEERS, }; @@ -9,6 +9,7 @@ use crate::peer_manager::{ ConnectionDirection, PeerManager, PeerManagerEvent, }; use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR, PRIORITY_PEER_EXCESS}; +use crate::rpc::methods::MetadataRequest; use crate::rpc::*; use crate::service::behaviour::BehaviourEvent; pub use crate::service::behaviour::Gossipsub; @@ -23,21 +24,17 @@ use api_types::{PeerRequestId, Request, RequestId, Response}; use futures::stream::StreamExt; use gossipsub_scoring_parameters::{lighthouse_gossip_thresholds, PeerScoreSettings}; use libp2p::bandwidth::BandwidthSinks; -use libp2p::gossipsub::error::PublishError; -use libp2p::gossipsub::metrics::Config as GossipsubMetricsConfig; -use libp2p::gossipsub::subscription_filter::MaxCountSubscriptionFilter; use libp2p::gossipsub::{ - GossipsubEvent, IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, + self, IdentTopic as Topic, MessageAcceptance, MessageAuthenticity, MessageId, PublishError, }; -use libp2p::identify::{Behaviour as Identify, Config as IdentifyConfig, Event as IdentifyEvent}; +use libp2p::identify; use libp2p::multiaddr::{Multiaddr, Protocol as MProtocol}; -use libp2p::swarm::{ConnectionLimits, Swarm, SwarmBuilder, SwarmEvent}; +use libp2p::swarm::{Swarm, SwarmBuilder, SwarmEvent}; use libp2p::PeerId; use slog::{crit, debug, info, o, trace, warn}; use std::path::PathBuf; use std::pin::Pin; use std::{ - marker::PhantomData, sync::Arc, task::{Context, Poll}, }; @@ -66,10 +63,6 @@ pub enum NetworkEvent { PeerConnectedIncoming(PeerId), /// A peer has disconnected. PeerDisconnected(PeerId), - /// The peer needs to be banned. - PeerBanned(PeerId), - /// The peer has been unbanned. - PeerUnbanned(PeerId), /// An RPC Request that was sent failed. RPCFailed { /// The id of the failed request. @@ -163,8 +156,6 @@ impl Network { let meta_data = utils::load_or_build_metadata(&config.network_dir, &log); let globals = NetworkGlobals::new( enr, - config.listen_addrs().v4().map(|v4_addr| v4_addr.tcp_port), - config.listen_addrs().v6().map(|v6_addr| v6_addr.tcp_port), meta_data, config .trusted_peers @@ -229,7 +220,7 @@ impl Network { let update_gossipsub_scores = tokio::time::interval(params.decay_interval); let possible_fork_digests = ctx.fork_context.all_fork_digests(); - let filter = MaxCountSubscriptionFilter { + let filter = gossipsub::MaxCountSubscriptionFilter { filter: utils::create_whitelist_filter( possible_fork_digests, ctx.chain_spec.attestation_subnet_count, @@ -239,12 +230,20 @@ impl Network { max_subscriptions_per_request: 150, // 148 in theory = (64 attestation + 4 sync committee + 6 core topics) * 2 }; - config.gs_config = gossipsub_config(config.network_load, ctx.fork_context.clone()); + let gossipsub_config_params = GossipsubConfigParams { + message_domain_valid_snappy: ctx.chain_spec.message_domain_valid_snappy, + gossip_max_size: ctx.chain_spec.gossip_max_size as usize, + }; + config.gs_config = gossipsub_config( + config.network_load, + ctx.fork_context.clone(), + gossipsub_config_params, + ); // If metrics are enabled for gossipsub build the configuration let gossipsub_metrics = ctx .gossipsub_registry - .map(|registry| (registry, GossipsubMetricsConfig::default())); + .map(|registry| (registry, Default::default())); let snappy_transform = SnappyTransform::new(config.gs_config.max_transmit_size()); let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform( @@ -263,35 +262,48 @@ impl Network { (gossipsub, update_gossipsub_scores) }; + let network_params = NetworkParams { + max_chunk_size: ctx.chain_spec.max_chunk_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(), log.clone(), + network_params, ); let discovery = { // Build and start the discovery sub-behaviour - let mut discovery = - Discovery::new(&local_keypair, &config, network_globals.clone(), &log).await?; + let mut discovery = Discovery::new( + local_keypair.clone(), + &config, + network_globals.clone(), + &log, + ) + .await?; // start searching for peers discovery.discover_peers(FIND_NODE_QUERY_CLOSEST_PEERS); discovery }; let identify = { + let local_public_key = local_keypair.public(); let identify_config = if config.private { - IdentifyConfig::new( + identify::Config::new( "".into(), - local_keypair.public(), // Still send legitimate public key + local_public_key, // Still send legitimate public key ) .with_cache_size(0) } else { - IdentifyConfig::new("eth2/1.0.0".into(), local_keypair.public()) + identify::Config::new("eth2/1.0.0".into(), local_public_key) .with_agent_version(lighthouse_version::version_with_platform()) .with_cache_size(0) }; - Identify::new(identify_config) + identify::Behaviour::new(identify_config) }; let peer_manager = { @@ -304,31 +316,8 @@ impl Network { PeerManager::new(peer_manager_cfg, network_globals.clone(), &log)? }; - let behaviour = { - Behaviour { - gossipsub, - eth2_rpc, - discovery, - identify, - peer_manager, - } - }; - - let (swarm, bandwidth) = { - // Set up the transport - tcp/ws with noise and mplex - let (transport, bandwidth) = build_transport(local_keypair.clone()) - .map_err(|e| format!("Failed to build transport: {:?}", e))?; - - // use the executor for libp2p - struct Executor(task_executor::TaskExecutor); - impl libp2p::swarm::Executor for Executor { - fn exec(&self, f: Pin + Send>>) { - self.0.spawn(f, "libp2p"); - } - } - - // sets up the libp2p connection limits - let limits = ConnectionLimits::default() + let connection_limits = { + let limits = libp2p::connection_limits::ConnectionLimits::default() .with_max_pending_incoming(Some(5)) .with_max_pending_outgoing(Some(16)) .with_max_established_incoming(Some( @@ -345,6 +334,39 @@ impl Network { )) .with_max_established_per_peer(Some(MAX_CONNECTIONS_PER_PEER)); + libp2p::connection_limits::Behaviour::new(limits) + }; + + let banned_peers = libp2p::allow_block_list::Behaviour::default(); + + let behaviour = { + Behaviour { + banned_peers, + gossipsub, + eth2_rpc, + discovery, + identify, + peer_manager, + connection_limits, + } + }; + + let (swarm, bandwidth) = { + // Set up the transport - tcp/ws with noise and mplex + let (transport, bandwidth) = + build_transport(local_keypair.clone(), !config.disable_quic_support) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; + + // use the executor for libp2p + struct Executor(task_executor::TaskExecutor); + impl libp2p::swarm::Executor for Executor { + fn exec(&self, f: Pin + Send>>) { + self.0.spawn(f, "libp2p"); + } + } + + // sets up the libp2p connection limits + ( SwarmBuilder::with_executor( transport, @@ -353,8 +375,7 @@ impl Network { Executor(executor), ) .notify_handler_buffer_size(std::num::NonZeroUsize::new(7).expect("Not zero")) - .connection_event_buffer_size(64) - .connection_limits(limits) + .per_connection_event_buffer_size(4) .build(), bandwidth, ) @@ -389,13 +410,20 @@ impl Network { async fn start(&mut self, config: &crate::NetworkConfig) -> error::Result<()> { let enr = self.network_globals.local_enr(); info!(self.log, "Libp2p Starting"; "peer_id" => %enr.peer_id(), "bandwidth_config" => format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name)); - debug!(self.log, "Attempting to open listening ports"; config.listen_addrs(), "discovery_enabled" => !config.disable_discovery); + debug!(self.log, "Attempting to open listening ports"; config.listen_addrs(), "discovery_enabled" => !config.disable_discovery, "quic_enabled" => !config.disable_quic_support); + + for listen_multiaddr in config.listen_addrs().libp2p_addresses() { + // If QUIC is disabled, ignore listening on QUIC ports + if config.disable_quic_support + && listen_multiaddr.iter().any(|v| v == MProtocol::QuicV1) + { + continue; + } - for listen_multiaddr in config.listen_addrs().tcp_addresses() { match self.swarm.listen_on(listen_multiaddr.clone()) { Ok(_) => { let mut log_address = listen_multiaddr; - log_address.push(MProtocol::P2p(enr.peer_id().into())); + log_address.push(MProtocol::P2p(enr.peer_id())); info!(self.log, "Listening established"; "address" => %log_address); } Err(err) => { @@ -432,6 +460,20 @@ impl Network { boot_nodes.dedup(); for bootnode_enr in boot_nodes { + // If QUIC is enabled, attempt QUIC connections first + if !config.disable_quic_support { + for quic_multiaddr in &bootnode_enr.multiaddr_quic() { + if !self + .network_globals + .peers + .read() + .is_connected_or_dialing(&bootnode_enr.peer_id()) + { + dial(quic_multiaddr.clone()); + } + } + } + for multiaddr in &bootnode_enr.multiaddr() { // ignore udp multiaddr if it exists let components = multiaddr.iter().collect::>(); @@ -492,7 +534,7 @@ impl Network { &mut self.swarm.behaviour_mut().discovery } /// Provides IP addresses and peer information. - pub fn identify_mut(&mut self) -> &mut Identify { + 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. @@ -513,7 +555,7 @@ impl Network { &self.swarm.behaviour().discovery } /// Provides IP addresses and peer information. - pub fn identify(&self) -> &Identify { + pub fn identify(&self) -> &identify::Behaviour { &self.swarm.behaviour().identify } /// The peer manager that keeps track of peer's reputation and status. @@ -943,16 +985,25 @@ impl Network { /// Sends a METADATA request to a peer. fn send_meta_data_request(&mut self, peer_id: PeerId) { - let event = OutboundRequest::MetaData(PhantomData); + // We always prefer sending V2 requests + let event = OutboundRequest::MetaData(MetadataRequest::new_v2()); self.eth2_rpc_mut() .send_request(peer_id, RequestId::Internal, event); } /// Sends a METADATA response to a peer. - fn send_meta_data_response(&mut self, id: PeerRequestId, peer_id: PeerId) { - let event = RPCCodedResponse::Success(RPCResponse::MetaData( - self.network_globals.local_metadata.read().clone(), - )); + fn send_meta_data_response( + &mut self, + req: MetadataRequest, + id: PeerRequestId, + peer_id: PeerId, + ) { + let metadata = self.network_globals.local_metadata.read().clone(); + let metadata = match req { + MetadataRequest::V1(_) => metadata.metadata_v1(), + MetadataRequest::V2(_) => metadata, + }; + let event = RPCCodedResponse::Success(RPCResponse::MetaData(metadata)); self.eth2_rpc_mut().send_response(peer_id, id, event); } @@ -1005,39 +1056,39 @@ impl Network { } } - /// Dial cached enrs in discovery service that are in the given `subnet_id` and aren't + /// Dial cached Enrs in discovery service that are in the given `subnet_id` and aren't /// in Connected, Dialing or Banned state. fn dial_cached_enrs_in_subnet(&mut self, subnet: Subnet) { let predicate = subnet_predicate::(vec![subnet], &self.log); - let peers_to_dial: Vec = self + let peers_to_dial: Vec = self .discovery() .cached_enrs() - .filter_map(|(peer_id, enr)| { - let peers = self.network_globals.peers.read(); - if predicate(enr) && peers.should_dial(peer_id) { - Some(*peer_id) + .filter_map(|(_peer_id, enr)| { + if predicate(enr) { + Some(enr.clone()) } else { None } }) .collect(); - for peer_id in peers_to_dial { - debug!(self.log, "Dialing cached ENR peer"; "peer_id" => %peer_id); - // Remove the ENR from the cache to prevent continual re-dialing on disconnects - self.discovery_mut().remove_cached_enr(&peer_id); - // For any dial event, inform the peer manager - let enr = self.discovery_mut().enr_of_peer(&peer_id); - self.peer_manager_mut().dial_peer(&peer_id, enr); + // Remove the ENR from the cache to prevent continual re-dialing on disconnects + for enr in peers_to_dial { + debug!(self.log, "Dialing cached ENR peer"; "peer_id" => %enr.peer_id()); + self.discovery_mut().remove_cached_enr(&enr.peer_id()); + self.peer_manager_mut().dial_peer(enr); } } /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. - fn inject_gs_event(&mut self, event: GossipsubEvent) -> Option> { + fn inject_gs_event( + &mut self, + event: gossipsub::Event, + ) -> Option> { match event { - GossipsubEvent::Message { + gossipsub::Event::Message { propagation_source, message_id: id, message: gs_msg, @@ -1067,7 +1118,7 @@ impl Network { } } } - GossipsubEvent::Subscribed { peer_id, topic } => { + gossipsub::Event::Subscribed { peer_id, topic } => { if let Ok(topic) = GossipTopic::decode(topic.as_str()) { if let Some(subnet_id) = topic.subnet_id() { self.network_globals @@ -1108,7 +1159,7 @@ impl Network { } } } - GossipsubEvent::Unsubscribed { peer_id, topic } => { + gossipsub::Event::Unsubscribed { peer_id, topic } => { if let Some(subnet_id) = subnet_from_topic_hash(&topic) { self.network_globals .peers @@ -1116,7 +1167,7 @@ impl Network { .remove_subscription(&peer_id, &subnet_id); } } - GossipsubEvent::GossipsubNotSupported { peer_id } => { + gossipsub::Event::GossipsubNotSupported { peer_id } => { debug!(self.log, "Peer does not support gossipsub"; "peer_id" => %peer_id); self.peer_manager_mut().report_peer( &peer_id, @@ -1195,9 +1246,9 @@ impl Network { self.pong(peer_request_id, peer_id); None } - InboundRequest::MetaData(_) => { + InboundRequest::MetaData(req) => { // send the requested meta-data - self.send_meta_data_response((handler_id, id), peer_id); + self.send_meta_data_response(req, (handler_id, id), peer_id); None } InboundRequest::Goodbye(reason) => { @@ -1224,13 +1275,9 @@ impl Network { Some(event) } InboundRequest::BlocksByRange(req) => { - let methods::OldBlocksByRangeRequest { - start_slot, - mut count, - step, - } = req; // Still disconnect the peer if the request is naughty. - if step == 0 { + let mut count = *req.count(); + if *req.step() == 0 { self.peer_manager_mut().handle_rpc_error( &peer_id, Protocol::BlocksByRange, @@ -1242,14 +1289,18 @@ impl Network { return None; } // return just one block in case the step parameter is used. https://github.com/ethereum/consensus-specs/pull/2856 - if step > 1 { + if *req.step() > 1 { count = 1; } - let event = self.build_request( - peer_request_id, - peer_id, - Request::BlocksByRange(BlocksByRangeRequest { start_slot, count }), - ); + let request = match req { + methods::OldBlocksByRangeRequest::V1(req) => Request::BlocksByRange( + BlocksByRangeRequest::new_v1(req.start_slot, count), + ), + methods::OldBlocksByRangeRequest::V2(req) => Request::BlocksByRange( + BlocksByRangeRequest::new(req.start_slot, count), + ), + }; + let event = self.build_request(peer_request_id, peer_id, request); Some(event) } InboundRequest::BlocksByRoot(req) => { @@ -1311,29 +1362,13 @@ impl Network { } } - /// Handle a discovery event. - fn inject_discovery_event( - &mut self, - event: DiscoveredPeers, - ) -> Option> { - let DiscoveredPeers { peers } = event; - let to_dial_peers = self.peer_manager_mut().peers_discovered(peers); - for peer_id in to_dial_peers { - debug!(self.log, "Dialing discovered peer"; "peer_id" => %peer_id); - // For any dial event, inform the peer manager - let enr = self.discovery_mut().enr_of_peer(&peer_id); - self.peer_manager_mut().dial_peer(&peer_id, enr); - } - None - } - /// Handle an identify event. fn inject_identify_event( &mut self, - event: IdentifyEvent, + event: identify::Event, ) -> Option> { match event { - IdentifyEvent::Received { peer_id, mut info } => { + identify::Event::Received { peer_id, mut info } => { if info.listen_addrs.len() > MAX_IDENTIFY_ADDRESSES { debug!( self.log, @@ -1344,9 +1379,9 @@ impl Network { // send peer info to the peer manager. self.peer_manager_mut().identify(&peer_id, &info); } - IdentifyEvent::Sent { .. } => {} - IdentifyEvent::Error { .. } => {} - IdentifyEvent::Pushed { .. } => {} + identify::Event::Sent { .. } => {} + identify::Event::Error { .. } => {} + identify::Event::Pushed { .. } => {} } None } @@ -1367,14 +1402,17 @@ impl Network { Some(NetworkEvent::PeerDisconnected(peer_id)) } PeerManagerEvent::Banned(peer_id, associated_ips) => { - self.swarm.ban_peer_id(peer_id); + self.swarm.behaviour_mut().banned_peers.block_peer(peer_id); self.discovery_mut().ban_peer(&peer_id, associated_ips); - Some(NetworkEvent::PeerBanned(peer_id)) + None } PeerManagerEvent::UnBanned(peer_id, associated_ips) => { - self.swarm.unban_peer_id(peer_id); + self.swarm + .behaviour_mut() + .banned_peers + .unblock_peer(peer_id); self.discovery_mut().unban_peer(&peer_id, associated_ips); - Some(NetworkEvent::PeerUnbanned(peer_id)) + None } PeerManagerEvent::Status(peer_id) => { // it's time to status. We don't keep a beacon chain reference here, so we inform @@ -1421,17 +1459,27 @@ impl Network { let maybe_event = match swarm_event { SwarmEvent::Behaviour(behaviour_event) => match behaviour_event { // Handle sub-behaviour events. + BehaviourEvent::BannedPeers(void) => void::unreachable(void), BehaviourEvent::Gossipsub(ge) => self.inject_gs_event(ge), BehaviourEvent::Eth2Rpc(re) => self.inject_rpc_event(re), - BehaviourEvent::Discovery(de) => self.inject_discovery_event(de), + // Inform the peer manager about discovered peers. + // + // The peer manager will subsequently decide which peers need to be dialed and then dial + // them. + BehaviourEvent::Discovery(DiscoveredPeers { peers }) => { + self.peer_manager_mut().peers_discovered(peers); + None + } BehaviourEvent::Identify(ie) => self.inject_identify_event(ie), BehaviourEvent::PeerManager(pe) => self.inject_pm_event(pe), + BehaviourEvent::ConnectionLimits(le) => void::unreachable(le), }, SwarmEvent::ConnectionEstablished { .. } => None, SwarmEvent::ConnectionClosed { .. } => None, SwarmEvent::IncomingConnection { local_addr, send_back_addr, + connection_id: _, } => { trace!(self.log, "Incoming connection"; "our_addr" => %local_addr, "from" => %send_back_addr); None @@ -1440,19 +1488,41 @@ impl Network { local_addr, send_back_addr, error, + connection_id: _, } => { - debug!(self.log, "Failed incoming connection"; "our_addr" => %local_addr, "from" => %send_back_addr, "error" => %error); + let error_repr = match error { + libp2p::swarm::ListenError::Aborted => { + "Incoming connection aborted".to_string() + } + 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::Denied { cause } => { + format!("Connection was denied with cause: {cause:?}") + } + libp2p::swarm::ListenError::Transport(t) => match t { + libp2p::TransportError::MultiaddrNotSupported(m) => { + format!("Transport error: Multiaddr not supported: {m}") + } + libp2p::TransportError::Other(e) => { + format!("Transport error: other: {e}") + } + }, + }; + debug!(self.log, "Failed incoming connection"; "our_addr" => %local_addr, "from" => %send_back_addr, "error" => error_repr); None } - SwarmEvent::OutgoingConnectionError { peer_id, error } => { - debug!(self.log, "Failed to dial address"; "peer_id" => ?peer_id, "error" => %error); - None - } - SwarmEvent::BannedPeer { - peer_id, - endpoint: _, + SwarmEvent::OutgoingConnectionError { + peer_id: _, + error: _, + connection_id: _, } => { - debug!(self.log, "Banned peer connection rejected"; "peer_id" => %peer_id); + // The Behaviour event is more general than the swarm event here. It includes + // connection failures. So we use that log for now, in the peer manager + // behaviour implementation. None } SwarmEvent::NewListenAddr { address, .. } => { @@ -1481,7 +1551,7 @@ impl Network { None } } - SwarmEvent::Dialing(_) => None, + SwarmEvent::Dialing { .. } => None, }; if let Some(ev) = maybe_event { diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 625df65ee..b02a47fef 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -4,13 +4,13 @@ use crate::types::{ error, EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipEncoding, GossipKind, }; use crate::{GossipTopic, NetworkConfig}; -use libp2p::bandwidth::{BandwidthLogging, BandwidthSinks}; -use libp2p::core::{ - identity::Keypair, multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed, -}; -use libp2p::gossipsub::subscription_filter::WhitelistSubscriptionFilter; -use libp2p::gossipsub::IdentTopic as Topic; -use libp2p::{core, noise, PeerId, Transport}; +use futures::future::Either; +use libp2p::bandwidth::BandwidthSinks; +use libp2p::core::{multiaddr::Multiaddr, muxing::StreamMuxerBox, transport::Boxed}; +use libp2p::gossipsub; +use libp2p::identity::{secp256k1, Keypair}; +use libp2p::{core, noise, yamux, PeerId, Transport, TransportExt}; +use libp2p_quic; use prometheus_client::registry::Registry; use slog::{debug, warn}; use ssz::Decode; @@ -39,43 +39,49 @@ pub struct Context<'a> { type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; -/// The implementation supports TCP/IP, WebSockets over TCP/IP, noise as the encryption layer, and -/// mplex as the multiplexing layer. +/// The implementation supports TCP/IP, QUIC (experimental) over UDP, noise as the encryption layer, and +/// mplex/yamux as the multiplexing layer (when using TCP). pub fn build_transport( local_private_key: Keypair, + quic_support: bool, ) -> std::io::Result<(BoxedTransport, Arc)> { - let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)); - let transport = libp2p::dns::TokioDnsConfig::system(tcp)?; - #[cfg(feature = "libp2p-websocket")] - let transport = { - let trans_clone = transport.clone(); - transport.or_transport(libp2p::websocket::WsConfig::new(trans_clone)) - }; - - let (transport, bandwidth) = BandwidthLogging::new(transport); - // mplex config - let mut mplex_config = libp2p::mplex::MplexConfig::new(); + let mut mplex_config = libp2p_mplex::MplexConfig::new(); mplex_config.set_max_buffer_size(256); - mplex_config.set_max_buffer_behaviour(libp2p::mplex::MaxBufferBehaviour::Block); + mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::Block); // yamux config - let mut yamux_config = libp2p::yamux::YamuxConfig::default(); - yamux_config.set_window_update_mode(libp2p::yamux::WindowUpdateMode::on_read()); + let mut yamux_config = yamux::Config::default(); + yamux_config.set_window_update_mode(yamux::WindowUpdateMode::on_read()); - // Authentication - Ok(( - transport - .upgrade(core::upgrade::Version::V1) - .authenticate(generate_noise_config(&local_private_key)) - .multiplex(core::upgrade::SelectUpgrade::new( - yamux_config, - mplex_config, - )) - .timeout(Duration::from_secs(10)) - .boxed(), - bandwidth, - )) + // Creates the TCP transport layer + let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)); + + let (transport, bandwidth) = if quic_support { + // Enables Quic + // The default quic configuration suits us for now. + let quic_config = libp2p_quic::Config::new(&local_private_key); + tcp.or_transport(libp2p_quic::tokio::Transport::new(quic_config)) + .map(|either_output, _| match either_output { + Either::Left((peer_id, muxer)) => (peer_id, StreamMuxerBox::new(muxer)), + Either::Right((peer_id, muxer)) => (peer_id, StreamMuxerBox::new(muxer)), + }) + .with_bandwidth_logging() + } else { + tcp.with_bandwidth_logging() + }; + + // // Enables DNS over the transport. + let transport = libp2p::dns::TokioDnsConfig::system(transport)?.boxed(); + + Ok((transport, bandwidth)) } // Useful helper functions for debugging. Currently not used in the client. @@ -94,10 +100,10 @@ fn keypair_from_hex(hex_bytes: &str) -> error::Result { #[allow(dead_code)] fn keypair_from_bytes(mut bytes: Vec) -> error::Result { - libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut bytes) + secp256k1::SecretKey::try_from_bytes(&mut bytes) .map(|secret| { - let keypair: libp2p::core::identity::secp256k1::Keypair = secret.into(); - Keypair::Secp256k1(keypair) + let keypair: secp256k1::Keypair = secret.into(); + keypair.into() }) .map_err(|e| format!("Unable to parse p2p secret key: {:?}", e).into()) } @@ -115,12 +121,10 @@ pub fn load_private_key(config: &NetworkConfig, log: &slog::Logger) -> Keypair { Err(_) => debug!(log, "Could not read network key file"), Ok(_) => { // only accept secp256k1 keys for now - if let Ok(secret_key) = - libp2p::core::identity::secp256k1::SecretKey::from_bytes(&mut key_bytes) - { - let kp: libp2p::core::identity::secp256k1::Keypair = secret_key.into(); + if let Ok(secret_key) = secp256k1::SecretKey::try_from_bytes(&mut key_bytes) { + let kp: secp256k1::Keypair = secret_key.into(); debug!(log, "Loaded network key from disk."); - return Keypair::Secp256k1(kp); + return kp.into(); } else { debug!(log, "Network key file is not a valid secp256k1 key"); } @@ -129,34 +133,27 @@ pub fn load_private_key(config: &NetworkConfig, log: &slog::Logger) -> Keypair { } // if a key could not be loaded from disk, generate a new one and save it - let local_private_key = Keypair::generate_secp256k1(); - if let Keypair::Secp256k1(key) = local_private_key.clone() { - let _ = std::fs::create_dir_all(&config.network_dir); - match File::create(network_key_f.clone()) - .and_then(|mut f| f.write_all(&key.secret().to_bytes())) - { - Ok(_) => { - debug!(log, "New network key generated and written to disk"); - } - Err(e) => { - warn!( - log, - "Could not write node key to file: {:?}. error: {}", network_key_f, e - ); - } + 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())) + { + Ok(_) => { + debug!(log, "New network key generated and written to disk"); + } + Err(e) => { + warn!( + log, + "Could not write node key to file: {:?}. error: {}", network_key_f, e + ); } } - local_private_key + local_private_key.into() } /// Generate authenticated XX Noise config from identity keys -fn generate_noise_config( - identity_keypair: &Keypair, -) -> noise::NoiseAuthenticated { - let static_dh_keys = noise::Keypair::::new() - .into_authentic(identity_keypair) - .expect("signing can fail only once during starting a node"); - noise::NoiseConfig::xx(static_dh_keys).into_authenticated() +fn generate_noise_config(identity_keypair: &Keypair) -> noise::Config { + noise::Config::new(identity_keypair).expect("signing can fail only once during starting a node") } /// For a multiaddr that ends with a peer id, this strips this suffix. Rust-libp2p @@ -236,11 +233,11 @@ pub(crate) fn create_whitelist_filter( possible_fork_digests: Vec<[u8; 4]>, attestation_subnet_count: u64, sync_committee_subnet_count: u64, -) -> WhitelistSubscriptionFilter { +) -> gossipsub::WhitelistSubscriptionFilter { let mut possible_hashes = HashSet::new(); for fork_digest in possible_fork_digests { let mut add = |kind| { - let topic: Topic = + let topic: gossipsub::IdentTopic = GossipTopic::new(kind, GossipEncoding::SSZSnappy, fork_digest).into(); possible_hashes.insert(topic.hash()); }; @@ -262,7 +259,7 @@ pub(crate) fn create_whitelist_filter( add(SyncCommitteeMessage(SyncSubnetId::new(id))); } } - WhitelistSubscriptionFilter(possible_hashes) + gossipsub::WhitelistSubscriptionFilter(possible_hashes) } /// Persist metadata to disk @@ -272,9 +269,11 @@ pub(crate) fn save_metadata_to_disk( log: &slog::Logger, ) { let _ = std::fs::create_dir_all(dir); - match File::create(dir.join(METADATA_FILENAME)) - .and_then(|mut f| f.write_all(&metadata.as_ssz_bytes())) - { + let metadata_bytes = match metadata { + MetaData::V1(md) => md.as_ssz_bytes(), + MetaData::V2(md) => md.as_ssz_bytes(), + }; + match File::create(dir.join(METADATA_FILENAME)).and_then(|mut f| f.write_all(&metadata_bytes)) { Ok(_) => { debug!(log, "Metadata written to disk"); } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 295616f36..b2b605e8a 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -16,10 +16,6 @@ pub struct NetworkGlobals { pub peer_id: RwLock, /// Listening multiaddrs. pub listen_multiaddrs: RwLock>, - /// The TCP port that the libp2p service is listening on over Ipv4. - listen_port_tcp4: Option, - /// The TCP port that the libp2p service is listening on over Ipv6. - listen_port_tcp6: Option, /// The collection of known peers. pub peers: RwLock>, // The local meta data of our node. @@ -35,8 +31,6 @@ pub struct NetworkGlobals { impl NetworkGlobals { pub fn new( enr: Enr, - listen_port_tcp4: Option, - listen_port_tcp6: Option, local_metadata: MetaData, trusted_peers: Vec, disable_peer_scoring: bool, @@ -46,8 +40,6 @@ impl NetworkGlobals { local_enr: RwLock::new(enr.clone()), peer_id: RwLock::new(enr.peer_id()), listen_multiaddrs: RwLock::new(Vec::new()), - listen_port_tcp4, - listen_port_tcp6, local_metadata: RwLock::new(local_metadata), peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring, log)), gossipsub_subscriptions: RwLock::new(HashSet::new()), @@ -72,16 +64,6 @@ impl NetworkGlobals { self.listen_multiaddrs.read().clone() } - /// Returns the libp2p TCP port that this node has been configured to listen on. - pub fn listen_port_tcp4(&self) -> Option { - self.listen_port_tcp4 - } - - /// Returns the UDP discovery port that this node has been configured to listen on. - pub fn listen_port_tcp6(&self) -> Option { - self.listen_port_tcp6 - } - /// Returns the number of libp2p connected peers. pub fn connected_peers(&self) -> usize { self.peers.read().connected_peer_ids().count() @@ -134,14 +116,11 @@ impl NetworkGlobals { log: &slog::Logger, ) -> NetworkGlobals { use crate::CombinedKeyExt; - let keypair = libp2p::identity::Keypair::generate_secp256k1(); - let enr_key: discv5::enr::CombinedKey = - discv5::enr::CombinedKey::from_libp2p(&keypair).unwrap(); + let keypair = libp2p::identity::secp256k1::Keypair::generate(); + let enr_key: discv5::enr::CombinedKey = discv5::enr::CombinedKey::from_secp256k1(&keypair); let enr = discv5::enr::EnrBuilder::new("v4").build(&enr_key).unwrap(); NetworkGlobals::new( enr, - Some(9000), - None, MetaData::V2(MetaDataV2 { seq_number: 0, attnets: Default::default(), diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index bb0397de1..06732ac99 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -2,7 +2,7 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic}; use crate::TopicHash; -use libp2p::gossipsub::{DataTransform, GossipsubMessage, RawGossipsubMessage}; +use libp2p::gossipsub; use snap::raw::{decompress_len, Decoder, Encoder}; use ssz::{Decode, Encode}; use std::boxed::Box; @@ -56,12 +56,12 @@ impl SnappyTransform { } } -impl DataTransform for SnappyTransform { +impl gossipsub::DataTransform for SnappyTransform { // Provides the snappy decompression from RawGossipsubMessages fn inbound_transform( &self, - raw_message: RawGossipsubMessage, - ) -> Result { + raw_message: gossipsub::RawMessage, + ) -> Result { // check the length of the raw bytes let len = decompress_len(&raw_message.data)?; if len > self.max_size_per_message { @@ -75,7 +75,7 @@ impl DataTransform for SnappyTransform { let decompressed_data = decoder.decompress_vec(&raw_message.data)?; // Build the GossipsubMessage struct - Ok(GossipsubMessage { + Ok(gossipsub::Message { source: raw_message.source, data: decompressed_data, sequence_number: raw_message.sequence_number, diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 64714cbc0..7b437fe7a 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -1,5 +1,5 @@ #![cfg(test)] -use libp2p::gossipsub::GossipsubConfigBuilder; +use libp2p::gossipsub; use lighthouse_network::service::Network as LibP2PService; use lighthouse_network::Enr; use lighthouse_network::EnrExt; @@ -13,7 +13,6 @@ use tokio::runtime::Runtime; use types::{ ChainSpec, EnrForkId, Epoch, EthSpec, ForkContext, ForkName, Hash256, MinimalEthSpec, Slot, }; -use unused_port::unused_tcp4_port; type E = MinimalEthSpec; type ReqId = usize; @@ -68,20 +67,24 @@ pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { } } -pub fn build_config(port: u16, mut boot_nodes: Vec) -> NetworkConfig { +pub fn build_config(mut boot_nodes: Vec) -> NetworkConfig { let mut config = NetworkConfig::default(); + + // Find unused ports by using the 0 port. + let port = 0; + + let random_path: u16 = rand::random(); let path = TempBuilder::new() - .prefix(&format!("libp2p_test{}", port)) + .prefix(&format!("libp2p_test_{}", random_path)) .tempdir() .unwrap(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, port, port); - config.enr_udp4_port = Some(port); + 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(); // Reduce gossipsub heartbeat parameters - config.gs_config = GossipsubConfigBuilder::from(config.gs_config) + config.gs_config = gossipsub::ConfigBuilder::from(config.gs_config) .heartbeat_initial_delay(Duration::from_millis(500)) .heartbeat_interval(Duration::from_millis(500)) .build() @@ -94,9 +97,9 @@ pub async fn build_libp2p_instance( boot_nodes: Vec, log: slog::Logger, fork_name: ForkName, + spec: &ChainSpec, ) -> Libp2pInstance { - let port = unused_tcp4_port().unwrap(); - let config = build_config(port, boot_nodes); + let config = build_config(boot_nodes); // launch libp2p service let (signal, exit) = exit_future::signal(); @@ -106,7 +109,7 @@ pub async fn build_libp2p_instance( config: &config, enr_fork_id: EnrForkId::default(), fork_context: Arc::new(fork_context(fork_name)), - chain_spec: &ChainSpec::minimal(), + chain_spec: spec, gossipsub_registry: None, }; Libp2pInstance( @@ -123,6 +126,12 @@ pub fn get_enr(node: &LibP2PService) -> Enr { node.local_enr() } +// Protocol for the node pair connection. +pub enum Protocol { + Tcp, + Quic, +} + // Constructs a pair of nodes with separate loggers. The sender dials the receiver. // This returns a (sender, receiver) pair. #[allow(dead_code)] @@ -130,40 +139,66 @@ pub async fn build_node_pair( rt: Weak, log: &slog::Logger, fork_name: ForkName, + spec: &ChainSpec, + protocol: Protocol, ) -> (Libp2pInstance, Libp2pInstance) { let sender_log = log.new(o!("who" => "sender")); let receiver_log = log.new(o!("who" => "receiver")); - let mut sender = build_libp2p_instance(rt.clone(), vec![], sender_log, fork_name).await; - let mut receiver = build_libp2p_instance(rt, vec![], receiver_log, fork_name).await; - - let receiver_multiaddr = receiver.local_enr().multiaddr()[1].clone(); + let mut sender = build_libp2p_instance(rt.clone(), vec![], sender_log, fork_name, spec).await; + let mut receiver = build_libp2p_instance(rt, vec![], receiver_log, fork_name, spec).await; // let the two nodes set up listeners let sender_fut = async { loop { - if let NetworkEvent::NewListenAddr(_) = sender.next_event().await { - return; + if let NetworkEvent::NewListenAddr(addr) = sender.next_event().await { + // Only end once we've listened on the protocol we care about + match protocol { + Protocol::Tcp => { + if addr.iter().any(|multiaddr_proto| { + matches!(multiaddr_proto, libp2p::multiaddr::Protocol::Tcp(_)) + }) { + return addr; + } + } + Protocol::Quic => { + if addr.iter().any(|multiaddr_proto| { + matches!(multiaddr_proto, libp2p::multiaddr::Protocol::QuicV1) + }) { + return addr; + } + } + } } } }; let receiver_fut = async { loop { - if let NetworkEvent::NewListenAddr(_) = receiver.next_event().await { - return; + if let NetworkEvent::NewListenAddr(addr) = receiver.next_event().await { + match protocol { + Protocol::Tcp => { + if addr.iter().any(|multiaddr_proto| { + matches!(multiaddr_proto, libp2p::multiaddr::Protocol::Tcp(_)) + }) { + return addr; + } + } + Protocol::Quic => { + if addr.iter().any(|multiaddr_proto| { + matches!(multiaddr_proto, libp2p::multiaddr::Protocol::QuicV1) + }) { + return addr; + } + } + } } } }; let joined = futures::future::join(sender_fut, receiver_fut); - // wait for either both nodes to listen or a timeout - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(500)) => {} - _ = joined => {} - } + let receiver_multiaddr = joined.await.1; - // sender.dial_peer(peer_id); match sender.testing_dial(receiver_multiaddr.clone()) { Ok(()) => { debug!(log, "Sender dialed receiver"; "address" => format!("{:?}", receiver_multiaddr)) @@ -180,10 +215,11 @@ pub async fn build_linear( log: slog::Logger, n: usize, fork_name: ForkName, + spec: &ChainSpec, ) -> Vec { let mut nodes = Vec::with_capacity(n); for _ in 0..n { - nodes.push(build_libp2p_instance(rt.clone(), vec![], log.clone(), fork_name).await); + nodes.push(build_libp2p_instance(rt.clone(), vec![], log.clone(), fork_name, spec).await); } let multiaddrs: Vec = nodes diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index ebdbb6742..795afd06b 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -1,4 +1,8 @@ #![cfg(test)] + +mod common; + +use common::Protocol; use lighthouse_network::rpc::methods::*; use lighthouse_network::{rpc::max_rpc_size, NetworkEvent, ReportSource, Request, Response}; use slog::{debug, warn, Level}; @@ -9,46 +13,45 @@ use std::time::Duration; use tokio::runtime::Runtime; use tokio::time::sleep; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockMerge, EmptyBlock, Epoch, EthSpec, - ForkContext, ForkName, Hash256, MinimalEthSpec, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockMerge, ChainSpec, EmptyBlock, + Epoch, EthSpec, ForkContext, ForkName, Hash256, MinimalEthSpec, Signature, SignedBeaconBlock, + Slot, }; -mod common; - type E = MinimalEthSpec; /// Merge block with length < max_rpc_size. -fn merge_block_small(fork_context: &ForkContext) -> BeaconBlock { - let mut block = BeaconBlockMerge::::empty(&E::default_spec()); +fn merge_block_small(fork_context: &ForkContext, spec: &ChainSpec) -> BeaconBlock { + let mut block = BeaconBlockMerge::::empty(spec); let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat(tx).take(5000).collect::>()); block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Merge(block); - assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context)); + assert!(block.ssz_bytes_len() <= max_rpc_size(fork_context, spec.max_chunk_size as usize)); block } /// Merge block with length > MAX_RPC_SIZE. /// The max limit for a merge block is in the order of ~16GiB which wouldn't fit in memory. /// Hence, we generate a merge block just greater than `MAX_RPC_SIZE` to test rejection on the rpc layer. -fn merge_block_large(fork_context: &ForkContext) -> BeaconBlock { - let mut block = BeaconBlockMerge::::empty(&E::default_spec()); +fn merge_block_large(fork_context: &ForkContext, spec: &ChainSpec) -> BeaconBlock { + let mut block = BeaconBlockMerge::::empty(spec); let tx = VariableList::from(vec![0; 1024]); let txs = VariableList::from(std::iter::repeat(tx).take(100000).collect::>()); block.body.execution_payload.execution_payload.transactions = txs; let block = BeaconBlock::Merge(block); - assert!(block.ssz_bytes_len() > max_rpc_size(fork_context)); + assert!(block.ssz_bytes_len() > max_rpc_size(fork_context, spec.max_chunk_size as usize)); block } // Tests the STATUS RPC message #[test] #[allow(clippy::single_match)] -fn test_status_rpc() { +fn test_tcp_status_rpc() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -57,10 +60,18 @@ fn test_status_rpc() { let log = common::build_log(log_level, enable_logging); + let spec = E::default_spec(); + rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Base, + &spec, + Protocol::Tcp, + ) + .await; // Dummy STATUS RPC message let rpc_request = Request::Status(StatusMessage { @@ -138,7 +149,7 @@ fn test_status_rpc() { // Tests a streamed BlocksByRange RPC Message #[test] #[allow(clippy::single_match)] -fn test_blocks_by_range_chunked_rpc() { +fn test_tcp_blocks_by_range_chunked_rpc() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -149,16 +160,21 @@ fn test_blocks_by_range_chunked_rpc() { let rt = Arc::new(Runtime::new().unwrap()); + let spec = E::default_spec(); + rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Merge, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); let spec = E::default_spec(); @@ -171,7 +187,7 @@ fn test_blocks_by_range_chunked_rpc() { let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_altair = Response::BlocksByRange(Some(Arc::new(signed_full_block))); - let full_block = merge_block_small(&common::fork_context(ForkName::Merge)); + let full_block = merge_block_small(&common::fork_context(ForkName::Merge), &spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_merge_small = Response::BlocksByRange(Some(Arc::new(signed_full_block))); @@ -265,7 +281,7 @@ fn test_blocks_by_range_chunked_rpc() { // Tests rejection of blocks over `MAX_RPC_SIZE`. #[test] #[allow(clippy::single_match)] -fn test_blocks_by_range_over_limit() { +fn test_tcp_blocks_by_range_over_limit() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -276,19 +292,24 @@ fn test_blocks_by_range_over_limit() { let rt = Arc::new(Runtime::new().unwrap()); + let spec = E::default_spec(); + rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Merge).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Merge, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response - let full_block = merge_block_large(&common::fork_context(ForkName::Merge)); + let full_block = merge_block_large(&common::fork_context(ForkName::Merge), &spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_merge_large = Response::BlocksByRange(Some(Arc::new(signed_full_block))); @@ -349,7 +370,7 @@ fn test_blocks_by_range_over_limit() { // Tests that a streamed BlocksByRange RPC Message terminates when all expected chunks were received #[test] -fn test_blocks_by_range_chunked_rpc_terminates_correctly() { +fn test_tcp_blocks_by_range_chunked_rpc_terminates_correctly() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -361,16 +382,21 @@ fn test_blocks_by_range_chunked_rpc_terminates_correctly() { let rt = Arc::new(Runtime::new().unwrap()); + let spec = E::default_spec(); + rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Base, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: messages_to_send, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, messages_to_send)); // BlocksByRange Response let spec = E::default_spec(); @@ -476,7 +502,7 @@ fn test_blocks_by_range_chunked_rpc_terminates_correctly() { // Tests an empty response to a BlocksByRange RPC Message #[test] #[allow(clippy::single_match)] -fn test_blocks_by_range_single_empty_rpc() { +fn test_tcp_blocks_by_range_single_empty_rpc() { // set up the logging. The level and enabled logging or not let log_level = Level::Trace; let enable_logging = false; @@ -484,16 +510,21 @@ fn test_blocks_by_range_single_empty_rpc() { let log = common::build_log(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); + let spec = E::default_spec(); + rt.block_on(async { // get sender/receiver - let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Base, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRange Request - let rpc_request = Request::BlocksByRange(BlocksByRangeRequest { - start_slot: 0, - count: 10, - }); + let rpc_request = Request::BlocksByRange(BlocksByRangeRequest::new(0, 10)); // BlocksByRange Response let spec = E::default_spec(); @@ -577,7 +608,7 @@ fn test_blocks_by_range_single_empty_rpc() { // serves to test the snappy framing format as well. #[test] #[allow(clippy::single_match)] -fn test_blocks_by_root_chunked_rpc() { +fn test_tcp_blocks_by_root_chunked_rpc() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -590,20 +621,25 @@ fn test_blocks_by_root_chunked_rpc() { 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), &log, ForkName::Merge).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Merge, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); @@ -614,7 +650,7 @@ fn test_blocks_by_root_chunked_rpc() { let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_altair = Response::BlocksByRoot(Some(Arc::new(signed_full_block))); - let full_block = merge_block_small(&common::fork_context(ForkName::Merge)); + let full_block = merge_block_small(&common::fork_context(ForkName::Merge), &spec); let signed_full_block = SignedBeaconBlock::from_block(full_block, Signature::empty()); let rpc_response_merge_small = Response::BlocksByRoot(Some(Arc::new(signed_full_block))); @@ -704,7 +740,7 @@ fn test_blocks_by_root_chunked_rpc() { // Tests a streamed, chunked BlocksByRoot RPC Message terminates when all expected reponses have been received #[test] -fn test_blocks_by_root_chunked_rpc_terminates_correctly() { +fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { // set up the logging. The level and enabled logging or not let log_level = Level::Debug; let enable_logging = false; @@ -718,12 +754,18 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { 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), &log, ForkName::Base).await; + let (mut sender, mut receiver) = common::build_node_pair( + Arc::downgrade(&rt), + &log, + ForkName::Base, + &spec, + Protocol::Tcp, + ) + .await; // BlocksByRoot Request - let rpc_request = Request::BlocksByRoot(BlocksByRootRequest { - block_roots: VariableList::from(vec![ + let rpc_request = + Request::BlocksByRoot(BlocksByRootRequest::new(VariableList::from(vec![ Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), @@ -734,8 +776,7 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), Hash256::from_low_u64_be(0), - ]), - }); + ]))); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); @@ -836,21 +877,20 @@ fn test_blocks_by_root_chunked_rpc_terminates_correctly() { }) } -// Tests a Goodbye RPC message -#[test] -#[allow(clippy::single_match)] -fn test_goodbye_rpc() { - // set up the logging. The level and enabled logging or not - let log_level = Level::Trace; - let enable_logging = false; - +/// Establishes a pair of nodes and disconnects the pair based on the selected protocol via an RPC +/// Goodbye message. +fn goodbye_test(log_level: Level, enable_logging: bool, protocol: Protocol) { let log = common::build_log(log_level, enable_logging); let rt = Arc::new(Runtime::new().unwrap()); + + let spec = E::default_spec(); + // get sender/receiver rt.block_on(async { let (mut sender, mut receiver) = - common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base).await; + common::build_node_pair(Arc::downgrade(&rt), &log, ForkName::Base, &spec, protocol) + .await; // build the sender future let sender_future = async { @@ -876,12 +916,9 @@ fn test_goodbye_rpc() { // build the receiver future let receiver_future = async { loop { - match receiver.next_event().await { - NetworkEvent::PeerDisconnected(_) => { - // Should receive sent RPC request - return; - } - _ => {} // Ignore other events + if let NetworkEvent::PeerDisconnected(_) = receiver.next_event().await { + // Should receive sent RPC request + return; } } }; @@ -896,3 +933,23 @@ fn test_goodbye_rpc() { } }) } + +// Tests a Goodbye RPC message +#[test] +#[allow(clippy::single_match)] +fn tcp_test_goodbye_rpc() { + // set up the logging. The level and enabled logging or not + let log_level = Level::Debug; + let enable_logging = true; + goodbye_test(log_level, enable_logging, Protocol::Tcp); +} + +// Tests a Goodbye RPC message +#[test] +#[allow(clippy::single_match)] +fn quic_test_goodbye_rpc() { + // set up the logging. The level and enabled logging or not + let log_level = Level::Debug; + let enable_logging = true; + goodbye_test(log_level, enable_logging, Protocol::Quic); +} diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index a234165d1..0204fc245 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -2,52 +2,54 @@ name = "network" version = "0.2.0" authors = ["Sigma Prime "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -sloggers = { version = "2.1.1", features = ["json"] } -genesis = { path = "../genesis" } +sloggers = { workspace = true } +genesis = { workspace = true } matches = "0.1.8" -exit-future = "0.2.0" -slog-term = "2.6.0" -slog-async = "2.5.0" -environment = { path = "../../lighthouse/environment" } +exit-future = { workspace = true } +slog-term = { workspace = true } +slog-async = { workspace = true } [dependencies] -beacon_chain = { path = "../beacon_chain" } -store = { path = "../store" } -lighthouse_network = { path = "../lighthouse_network" } -types = { path = "../../consensus/types" } -slot_clock = { path = "../../common/slot_clock" } -slog = { version = "2.5.2", features = ["max_level_trace"] } -hex = "0.4.2" -ethereum_ssz = "0.5.0" -ssz_types = "0.5.0" -futures = "0.3.7" -error-chain = "0.12.4" -tokio = { version = "1.14.0", features = ["full"] } -tokio-stream = "0.1.3" -smallvec = "1.6.1" -rand = "0.8.5" -fnv = "1.0.7" +beacon_chain = { workspace = true } +store = { workspace = true } +lighthouse_network = { workspace = true } +types = { workspace = true } +slot_clock = { workspace = true } +slog = { workspace = true } +hex = { workspace = true } +ethereum_ssz = { workspace = true } +ssz_types = { workspace = true } +futures = { workspace = true } +error-chain = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +smallvec = { workspace = true } +rand = { workspace = true } +fnv = { workspace = true } rlp = "0.5.0" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -logging = { path = "../../common/logging" } -task_executor = { path = "../../common/task_executor" } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +logging = { workspace = true } +task_executor = { workspace = true } igd = "0.12.1" -itertools = "0.10.0" -num_cpus = "1.13.0" -lru_cache = { path = "../../common/lru_cache" } +itertools = { workspace = true } +num_cpus = { workspace = true } +lru_cache = { workspace = true } if-addrs = "0.6.4" -strum = "0.24.0" -tokio-util = { version = "0.6.3", features = ["time"] } -derivative = "2.2.0" -delay_map = "0.3.0" -ethereum-types = { version = "0.14.1", optional = true } -operation_pool = { path = "../operation_pool" } -execution_layer = { path = "../execution_layer" } +strum = { workspace = true } +tokio-util = { workspace = true } +derivative = { workspace = true } +delay_map = { workspace = true } +ethereum-types = { workspace = true } +operation_pool = { workspace = true } +execution_layer = { workspace = true } +beacon_processor = { workspace = true } +parking_lot = { workspace = true } +environment = { workspace = true } [features] -deterministic_long_lived_attnets = [ "ethereum-types" ] -# default = ["deterministic_long_lived_attnets"] +# NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill +disable-backfill = [] diff --git a/beacon_node/network/src/beacon_processor/worker/mod.rs b/beacon_node/network/src/beacon_processor/worker/mod.rs deleted file mode 100644 index 1cbc64b63..000000000 --- a/beacon_node/network/src/beacon_processor/worker/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::work_reprocessing_queue::ReprocessQueueMessage; -use crate::{service::NetworkMessage, sync::SyncMessage}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use slog::{debug, Logger}; -use std::sync::Arc; -use tokio::sync::mpsc; - -mod gossip_methods; -mod rpc_methods; -mod sync_methods; - -pub use gossip_methods::{GossipAggregatePackage, GossipAttestationPackage}; -pub use sync_methods::ChainSegmentProcessId; - -pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; - -/// Contains the context necessary to import blocks, attestations, etc to the beacon chain. -pub struct Worker { - pub chain: Arc>, - pub network_tx: mpsc::UnboundedSender>, - pub sync_tx: mpsc::UnboundedSender>, - pub log: Logger, -} - -impl Worker { - /// Send a message to `sync_tx`. - /// - /// Creates a log if there is an internal error. - fn send_sync_message(&self, message: SyncMessage) { - self.sync_tx.send(message).unwrap_or_else(|e| { - debug!(self.log, "Could not send message to the sync service"; - "error" => %e) - }); - } - - /// Send a message to `network_tx`. - /// - /// Creates a log if there is an internal error. - fn send_network_message(&self, message: NetworkMessage) { - self.network_tx.send(message).unwrap_or_else(|e| { - debug!(self.log, "Could not send message to the network service. Likely shutdown"; - "error" => %e) - }); - } -} - -/// Contains the necessary items for a worker to do their job. -pub struct Toolbox { - pub idle_tx: mpsc::Sender<()>, - pub work_reprocessing_tx: mpsc::Sender>, -} diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 648c636ac..da64368b1 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -6,10 +6,10 @@ pub mod error; #[allow(clippy::mutable_key_type)] // PeerId in hashmaps are no longer permitted by clippy pub mod service; -mod beacon_processor; #[allow(clippy::mutable_key_type)] // PeerId in hashmaps are no longer permitted by clippy mod metrics; mod nat; +mod network_beacon_processor; mod persisted_dht; mod router; mod status; diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 09caaaa11..014482486 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -49,47 +49,8 @@ lazy_static! { /* * Gossip processor */ - pub static ref BEACON_PROCESSOR_WORK_EVENTS_RX_COUNT: Result = try_create_int_counter_vec( - "beacon_processor_work_events_rx_count", - "Count of work events received (but not necessarily processed)", - &["type"] - ); - pub static ref BEACON_PROCESSOR_WORK_EVENTS_IGNORED_COUNT: Result = try_create_int_counter_vec( - "beacon_processor_work_events_ignored_count", - "Count of work events purposefully ignored", - &["type"] - ); - pub static ref BEACON_PROCESSOR_WORK_EVENTS_STARTED_COUNT: Result = try_create_int_counter_vec( - "beacon_processor_work_events_started_count", - "Count of work events which have been started by a worker", - &["type"] - ); - pub static ref BEACON_PROCESSOR_WORKER_TIME: Result = try_create_histogram_vec( - "beacon_processor_worker_time", - "Time taken for a worker to fully process some parcel of work.", - &["type"] - ); - pub static ref BEACON_PROCESSOR_WORKERS_SPAWNED_TOTAL: Result = try_create_int_counter( - "beacon_processor_workers_spawned_total", - "The number of workers ever spawned by the gossip processing pool." - ); - pub static ref BEACON_PROCESSOR_WORKERS_ACTIVE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_workers_active_total", - "Count of active workers in the gossip processing pool." - ); - pub static ref BEACON_PROCESSOR_IDLE_EVENTS_TOTAL: Result = try_create_int_counter( - "beacon_processor_idle_events_total", - "Count of idle events processed by the gossip processor manager." - ); - pub static ref BEACON_PROCESSOR_EVENT_HANDLING_SECONDS: Result = try_create_histogram( - "beacon_processor_event_handling_seconds", - "Time spent handling a new message and allocating it to a queue or worker." - ); + // Gossip blocks. - pub static ref BEACON_PROCESSOR_GOSSIP_BLOCK_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_gossip_block_queue_total", - "Count of blocks from gossip waiting to be verified." - ); pub static ref BEACON_PROCESSOR_GOSSIP_BLOCK_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_gossip_block_verified_total", "Total number of gossip blocks verified for propagation." @@ -107,10 +68,6 @@ lazy_static! { "Whenever a gossip block is received early this metrics is set to how early that block was." ); // Gossip Exits. - pub static ref BEACON_PROCESSOR_EXIT_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_exit_queue_total", - "Count of exits from gossip waiting to be verified." - ); pub static ref BEACON_PROCESSOR_EXIT_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_exit_verified_total", "Total number of voluntary exits verified for propagation." @@ -120,10 +77,6 @@ lazy_static! { "Total number of voluntary exits imported to the op pool." ); // Gossip proposer slashings. - pub static ref BEACON_PROCESSOR_PROPOSER_SLASHING_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_proposer_slashing_queue_total", - "Count of proposer slashings from gossip waiting to be verified." - ); pub static ref BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_proposer_slashing_verified_total", "Total number of proposer slashings verified for propagation." @@ -133,10 +86,6 @@ lazy_static! { "Total number of proposer slashings imported to the op pool." ); // Gossip attester slashings. - pub static ref BEACON_PROCESSOR_ATTESTER_SLASHING_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_attester_slashing_queue_total", - "Count of attester slashings from gossip waiting to be verified." - ); pub static ref BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_attester_slashing_verified_total", "Total number of attester slashings verified for propagation." @@ -146,10 +95,6 @@ lazy_static! { "Total number of attester slashings imported to the op pool." ); // Gossip BLS to execution changes. - pub static ref BEACON_PROCESSOR_BLS_TO_EXECUTION_CHANGE_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_bls_to_execution_change_queue_total", - "Count of address changes from gossip waiting to be verified." - ); pub static ref BEACON_PROCESSOR_BLS_TO_EXECUTION_CHANGE_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_bls_to_execution_change_verified_total", "Total number of address changes verified for propagation." @@ -159,23 +104,11 @@ lazy_static! { "Total number of address changes imported to the op pool." ); // Rpc blocks. - pub static ref BEACON_PROCESSOR_RPC_BLOCK_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_rpc_block_queue_total", - "Count of blocks from the rpc waiting to be verified." - ); pub static ref BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL: Result = try_create_int_counter( "beacon_processor_rpc_block_imported_total", "Total number of gossip blocks imported to fork choice, etc." ); // Chain segments. - pub static ref BEACON_PROCESSOR_CHAIN_SEGMENT_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_chain_segment_queue_total", - "Count of chain segments from the rpc waiting to be verified." - ); - pub static ref BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_backfill_chain_segment_queue_total", - "Count of backfill chain segments from the rpc waiting to be verified." - ); pub static ref BEACON_PROCESSOR_CHAIN_SEGMENT_SUCCESS_TOTAL: Result = try_create_int_counter( "beacon_processor_chain_segment_success_total", "Total number of chain segments successfully processed." @@ -193,10 +126,6 @@ lazy_static! { "Total number of backfill chain segments that failed processing." ); // Unaggregated attestations. - pub static ref BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_unaggregated_attestation_queue_total", - "Count of unagg. attestations waiting to be processed." - ); pub static ref BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_unaggregated_attestation_verified_total", "Total number of unaggregated attestations verified for gossip." @@ -210,10 +139,6 @@ lazy_static! { "Total number of unaggregated attestations that referenced an unknown block and were re-queued." ); // Aggregated attestations. - pub static ref BEACON_PROCESSOR_AGGREGATED_ATTESTATION_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_aggregated_attestation_queue_total", - "Count of agg. attestations waiting to be processed." - ); pub static ref BEACON_PROCESSOR_AGGREGATED_ATTESTATION_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_aggregated_attestation_verified_total", "Total number of aggregated attestations verified for gossip." @@ -227,10 +152,6 @@ lazy_static! { "Total number of aggregated attestations that referenced an unknown block and were re-queued." ); // Sync committee messages. - pub static ref BEACON_PROCESSOR_SYNC_MESSAGE_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_sync_message_queue_total", - "Count of sync committee messages waiting to be processed." - ); pub static ref BEACON_PROCESSOR_SYNC_MESSAGE_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_sync_message_verified_total", "Total number of sync committee messages verified for gossip." @@ -240,10 +161,6 @@ lazy_static! { "Total number of sync committee messages imported to fork choice, etc." ); // Sync contribution. - pub static ref BEACON_PROCESSOR_SYNC_CONTRIBUTION_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_sync_contribution_queue_total", - "Count of sync committee contributions waiting to be processed." - ); pub static ref BEACON_PROCESSOR_SYNC_CONTRIBUTION_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_sync_contribution_verified_total", "Total number of sync committee contributions verified for gossip." @@ -365,35 +282,9 @@ lazy_static! { "Count of times when a gossip block arrived from the network later than the attestation deadline.", ); - /* - * Attestation reprocessing queue metrics. - */ - pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_TOTAL: Result = - try_create_int_gauge_vec( - "beacon_processor_reprocessing_queue_total", - "Count of items in a reprocessing queue.", - &["type"] - ); - pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_EXPIRED_ATTESTATIONS: Result = try_create_int_counter( - "beacon_processor_reprocessing_queue_expired_attestations", - "Number of queued attestations which have expired before a matching block has been found." - ); - pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_ATTESTATIONS: Result = try_create_int_counter( - "beacon_processor_reprocessing_queue_matched_attestations", - "Number of queued attestations where as matching block has been imported." - ); - /* * Light client update reprocessing queue metrics. */ - pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_EXPIRED_OPTIMISTIC_UPDATES: Result = 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." - ); - pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: Result = try_create_int_counter( - "beacon_processor_reprocessing_queue_matched_optimistic_updates", - "Number of queued light client optimistic updates where as matching block has been imported." - ); pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_SENT_OPTIMISTIC_UPDATES: Result = 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." diff --git a/beacon_node/network/src/nat.rs b/beacon_node/network/src/nat.rs index 9bf123e8d..d011ac42e 100644 --- a/beacon_node/network/src/nat.rs +++ b/beacon_node/network/src/nat.rs @@ -12,20 +12,49 @@ use types::EthSpec; /// Configuration required to construct the UPnP port mappings. pub struct UPnPConfig { - /// The local tcp port. + /// The local TCP port. tcp_port: u16, - /// The local udp port. - udp_port: u16, + /// The local UDP discovery port. + disc_port: u16, + /// The local UDP quic port. + quic_port: u16, /// Whether discovery is enabled or not. disable_discovery: bool, + /// Whether quic is enabled or not. + disable_quic_support: bool, +} + +/// Contains mappings that managed to be established. +#[derive(Default, Debug)] +pub struct EstablishedUPnPMappings { + /// A TCP port mapping for libp2p. + pub tcp_port: Option, + /// A UDP port for the QUIC libp2p transport. + pub udp_quic_port: Option, + /// A UDP port for discv5. + pub udp_disc_port: Option, +} + +impl EstablishedUPnPMappings { + /// Returns true if at least one value is set. + pub fn is_some(&self) -> bool { + self.tcp_port.is_some() || self.udp_quic_port.is_some() || self.udp_disc_port.is_some() + } + + // Iterator over the UDP ports + pub fn udp_ports(&self) -> impl Iterator { + self.udp_quic_port.iter().chain(self.udp_disc_port.iter()) + } } impl UPnPConfig { pub fn from_config(config: &NetworkConfig) -> Option { config.listen_addrs().v4().map(|v4_addr| UPnPConfig { tcp_port: v4_addr.tcp_port, - udp_port: v4_addr.udp_port, + disc_port: v4_addr.disc_port, + quic_port: v4_addr.quic_port, disable_discovery: config.disable_discovery, + disable_quic_support: config.disable_quic_support, }) } } @@ -68,6 +97,8 @@ pub fn construct_upnp_mappings( debug!(log, "UPnP Local IP Discovered"; "ip" => ?local_ip); + let mut mappings = EstablishedUPnPMappings::default(); + match local_ip { IpAddr::V4(address) => { let libp2p_socket = SocketAddrV4::new(address, config.tcp_port); @@ -76,39 +107,46 @@ pub fn construct_upnp_mappings( // one. // I've found this to be more reliable. If multiple users are behind a single // router, they should ideally try to set different port numbers. - let tcp_socket = add_port_mapping( + mappings.tcp_port = add_port_mapping( &gateway, igd::PortMappingProtocol::TCP, libp2p_socket, "tcp", &log, - ).and_then(|_| { + ).map(|_| { let external_socket = external_ip.as_ref().map(|ip| SocketAddr::new((*ip).into(), config.tcp_port)).map_err(|_| ()); info!(log, "UPnP TCP route established"; "external_socket" => format!("{}:{}", external_socket.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.tcp_port)); - external_socket + config.tcp_port }).ok(); - let udp_socket = if !config.disable_discovery { - let discovery_socket = SocketAddrV4::new(address, config.udp_port); + let set_udp_mapping = |udp_port| { + let udp_socket = SocketAddrV4::new(address, udp_port); add_port_mapping( &gateway, igd::PortMappingProtocol::UDP, - discovery_socket, + udp_socket, "udp", &log, - ).and_then(|_| { - let external_socket = external_ip - .map(|ip| SocketAddr::new(ip.into(), config.udp_port)).map_err(|_| ()); - info!(log, "UPnP UDP route established"; "external_socket" => format!("{}:{}", external_socket.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), config.udp_port)); - external_socket - }).ok() - } else { - None + ).map(|_| { + info!(log, "UPnP UDP route established"; "external_socket" => format!("{}:{}", external_ip.as_ref().map(|ip| ip.to_string()).unwrap_or_else(|_| "".into()), udp_port)); + }) }; + // Set the discovery UDP port mapping + if !config.disable_discovery && set_udp_mapping(config.disc_port).is_ok() { + mappings.udp_disc_port = Some(config.disc_port); + } + + // Set the quic UDP port mapping + if !config.disable_quic_support && set_udp_mapping(config.quic_port).is_ok() { + mappings.udp_quic_port = Some(config.quic_port); + } + // report any updates to the network service. - network_send.send(NetworkMessage::UPnPMappingEstablished{ tcp_socket, udp_socket }) - .unwrap_or_else(|e| debug!(log, "Could not send message to the network service"; "error" => %e)); + if mappings.is_some() { + network_send.send(NetworkMessage::UPnPMappingEstablished{ mappings }) + .unwrap_or_else(|e| debug!(log, "Could not send message to the network service"; "error" => %e)); + } } _ => debug!(log, "UPnP no routes constructed. IPv6 not supported"), } @@ -161,12 +199,12 @@ fn add_port_mapping( } /// Removes the specified TCP and UDP port mappings. -pub fn remove_mappings(tcp_port: Option, udp_port: Option, log: &slog::Logger) { - if tcp_port.is_some() || udp_port.is_some() { +pub fn remove_mappings(mappings: &EstablishedUPnPMappings, log: &slog::Logger) { + if mappings.is_some() { debug!(log, "Removing UPnP port mappings"); match igd::search_gateway(Default::default()) { Ok(gateway) => { - if let Some(tcp_port) = tcp_port { + if let Some(tcp_port) = mappings.tcp_port { match gateway.remove_port(igd::PortMappingProtocol::TCP, tcp_port) { Ok(()) => debug!(log, "UPnP Removed TCP port mapping"; "port" => tcp_port), Err(e) => { @@ -174,8 +212,8 @@ pub fn remove_mappings(tcp_port: Option, udp_port: Option, log: &slog: } } } - if let Some(udp_port) = udp_port { - match gateway.remove_port(igd::PortMappingProtocol::UDP, udp_port) { + for udp_port in mappings.udp_ports() { + match gateway.remove_port(igd::PortMappingProtocol::UDP, *udp_port) { Ok(()) => debug!(log, "UPnP Removed UDP port mapping"; "port" => udp_port), Err(e) => { debug!(log, "UPnP Failed to remove UDP port mapping"; "port" => udp_port, "error" => %e) diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs similarity index 94% rename from beacon_node/network/src/beacon_processor/worker/gossip_methods.rs rename to beacon_node/network/src/network_beacon_processor/gossip_methods.rs index cb4533f5a..ac7479db0 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1,4 +1,9 @@ -use crate::{metrics, service::NetworkMessage, sync::SyncMessage}; +use crate::{ + metrics, + network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}, + service::NetworkMessage, + sync::SyncMessage, +}; use beacon_chain::store::Error; use beacon_chain::{ @@ -8,8 +13,8 @@ use beacon_chain::{ observed_operations::ObservationOutcome, sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::get_block_delay_ms, - BeaconChainError, BeaconChainTypes, BlockError, CountUnrealized, ForkChoiceError, - GossipVerifiedBlock, NotifyExecutionLayer, + BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, GossipVerifiedBlock, + NotifyExecutionLayer, }; use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; use operation_pool::ReceivedPreCapella; @@ -30,14 +35,13 @@ use types::{ SyncCommitteeMessage, SyncSubnetId, }; -use super::{ - super::work_reprocessing_queue::{ +use beacon_processor::{ + work_reprocessing_queue::{ QueuedAggregate, QueuedGossipBlock, QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, - Worker, + DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, }; -use crate::beacon_processor::{DuplicateCache, InvalidBlockStorage}; /// Set to `true` to introduce stricter penalties for peers who send some types of late consensus /// messages. @@ -144,65 +148,7 @@ impl FailedAtt { } } -/// Items required to verify a batch of unaggregated gossip attestations. -#[derive(Debug)] -pub struct GossipAttestationPackage { - message_id: MessageId, - peer_id: PeerId, - attestation: Box>, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, -} - -impl GossipAttestationPackage { - pub fn new( - message_id: MessageId, - peer_id: PeerId, - attestation: Box>, - subnet_id: SubnetId, - should_import: bool, - seen_timestamp: Duration, - ) -> Self { - Self { - message_id, - peer_id, - attestation, - subnet_id, - should_import, - seen_timestamp, - } - } -} - -/// Items required to verify a batch of aggregated gossip attestations. -#[derive(Debug)] -pub struct GossipAggregatePackage { - message_id: MessageId, - peer_id: PeerId, - aggregate: Box>, - beacon_block_root: Hash256, - seen_timestamp: Duration, -} - -impl GossipAggregatePackage { - pub fn new( - message_id: MessageId, - peer_id: PeerId, - aggregate: Box>, - seen_timestamp: Duration, - ) -> Self { - Self { - message_id, - peer_id, - beacon_block_root: aggregate.message.aggregate.data.beacon_block_root, - aggregate, - seen_timestamp, - } - } -} - -impl Worker { +impl NetworkBeaconProcessor { /* Auxiliary functions */ /// Penalizes a peer for misbehaviour. @@ -245,13 +191,13 @@ impl Worker { /// Raises a log if there are errors. #[allow(clippy::too_many_arguments)] pub fn process_gossip_attestation( - self, + self: Arc, message_id: MessageId, peer_id: PeerId, attestation: Box>, subnet_id: SubnetId, should_import: bool, - reprocess_tx: Option>>, + reprocess_tx: Option>, seen_timestamp: Duration, ) { let result = match self @@ -277,9 +223,9 @@ impl Worker { } pub fn process_gossip_attestation_batch( - self, + self: Arc, packages: Vec>, - reprocess_tx: Option>>, + reprocess_tx: Option>, ) { let attestations_and_subnets = packages .iter() @@ -348,12 +294,12 @@ impl Worker { // cant' be mixed-up) and creating a struct would result in more complexity. #[allow(clippy::too_many_arguments)] fn process_gossip_attestation_result( - &self, + self: &Arc, result: Result, RejectedUnaggregate>, message_id: MessageId, peer_id: PeerId, subnet_id: SubnetId, - reprocess_tx: Option>>, + reprocess_tx: Option>, should_import: bool, seen_timestamp: Duration, ) { @@ -456,11 +402,11 @@ impl Worker { /// /// Raises a log if there are errors. pub fn process_gossip_aggregate( - self, + self: Arc, message_id: MessageId, peer_id: PeerId, aggregate: Box>, - reprocess_tx: Option>>, + reprocess_tx: Option>, seen_timestamp: Duration, ) { let beacon_block_root = aggregate.message.aggregate.data.beacon_block_root; @@ -490,9 +436,9 @@ impl Worker { } pub fn process_gossip_aggregate_batch( - self, + self: Arc, packages: Vec>, - reprocess_tx: Option>>, + reprocess_tx: Option>, ) { let aggregates = packages.iter().map(|package| package.aggregate.as_ref()); @@ -555,12 +501,12 @@ impl Worker { } fn process_gossip_aggregate_result( - &self, + self: &Arc, result: Result, RejectedAggregate>, beacon_block_root: Hash256, message_id: MessageId, peer_id: PeerId, - reprocess_tx: Option>>, + reprocess_tx: Option>, seen_timestamp: Duration, ) { match result { @@ -659,12 +605,12 @@ impl Worker { /// Raises a log if there are errors. #[allow(clippy::too_many_arguments)] pub async fn process_gossip_block( - self, + self: Arc, message_id: MessageId, peer_id: PeerId, peer_client: Client, block: Arc>, - reprocess_tx: mpsc::Sender>, + reprocess_tx: mpsc::Sender, duplicate_cache: DuplicateCache, invalid_block_storage: InvalidBlockStorage, seen_duration: Duration, @@ -708,12 +654,12 @@ impl Worker { /// /// Returns the `GossipVerifiedBlock` if verification passes and raises a log if there are errors. pub async fn process_gossip_unverified_block( - &self, + self: &Arc, message_id: MessageId, peer_id: PeerId, peer_client: Client, block: Arc>, - reprocess_tx: mpsc::Sender>, + reprocess_tx: mpsc::Sender, seen_duration: Duration, ) -> Option> { let block_delay = @@ -785,6 +731,20 @@ impl Worker { verified_block } + Err(e @ BlockError::Slashable) => { + warn!( + self.log, + "Received equivocating block from peer"; + "error" => ?e + ); + /* punish peer for submitting an equivocation, but not too harshly as honest peers may conceivably forward equivocating blocks to us from time to time */ + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "gossip_block_mid", + ); + return None; + } Err(BlockError::ParentUnknown(block)) => { debug!( self.log, @@ -806,7 +766,6 @@ impl Worker { Err(e @ BlockError::FutureSlot { .. }) | Err(e @ BlockError::WouldRevertFinalizedSlot { .. }) | Err(e @ BlockError::BlockIsAlreadyKnown) - | Err(e @ BlockError::RepeatProposal { .. }) | Err(e @ BlockError::NotFinalizedDescendant { .. }) => { debug!(self.log, "Could not verify block for gossip. Ignoring the block"; "error" => %e); @@ -835,7 +794,6 @@ impl Worker { | Err(e @ BlockError::NonLinearParentRoots) | Err(e @ BlockError::BlockIsNotLaterThanParent { .. }) | Err(e @ BlockError::InvalidSignature) - | Err(e @ BlockError::TooManySkippedSlots { .. }) | Err(e @ BlockError::WeakSubjectivityConflict) | Err(e @ BlockError::InconsistentFork(_)) | Err(e @ BlockError::ExecutionPayloadError(_)) @@ -899,11 +857,25 @@ impl Worker { metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_REQUEUED_TOTAL); + 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 { - peer_id, - block: Box::new(verified_block), - seen_timestamp: seen_duration, + beacon_block_slot: block_slot, + beacon_block_root: block_root, + process_fn, })) .is_err() { @@ -936,10 +908,10 @@ impl Worker { /// /// Raises a log if there are errors. pub async fn process_gossip_verified_block( - self, + self: Arc, peer_id: PeerId, verified_block: GossipVerifiedBlock, - reprocess_tx: mpsc::Sender>, + reprocess_tx: mpsc::Sender, invalid_block_storage: InvalidBlockStorage, // This value is not used presently, but it might come in handy for debugging. _seen_duration: Duration, @@ -952,8 +924,8 @@ impl Worker { .process_block( block_root, verified_block, - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ) .await; @@ -1039,7 +1011,7 @@ impl Worker { } pub fn process_gossip_voluntary_exit( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, voluntary_exit: SignedVoluntaryExit, @@ -1097,7 +1069,7 @@ impl Worker { } pub fn process_gossip_proposer_slashing( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, proposer_slashing: ProposerSlashing, @@ -1159,7 +1131,7 @@ impl Worker { } pub fn process_gossip_attester_slashing( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, attester_slashing: AttesterSlashing, @@ -1213,7 +1185,7 @@ impl Worker { } pub fn process_gossip_bls_to_execution_change( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, bls_to_execution_change: SignedBlsToExecutionChange, @@ -1296,7 +1268,7 @@ impl Worker { /// /// Raises a log if there are errors. pub fn process_gossip_sync_committee_signature( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, sync_signature: SyncCommitteeMessage, @@ -1359,7 +1331,7 @@ impl Worker { /// /// Raises a log if there are errors. pub fn process_sync_committee_contribution( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, sync_contribution: SignedContributionAndProof, @@ -1414,7 +1386,7 @@ impl Worker { } pub fn process_gossip_finality_update( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, light_client_finality_update: LightClientFinalityUpdate, @@ -1480,11 +1452,11 @@ impl Worker { } pub fn process_gossip_optimistic_update( - self, + self: &Arc, message_id: MessageId, peer_id: PeerId, light_client_optimistic_update: LightClientOptimisticUpdate, - reprocess_tx: Option>>, + reprocess_tx: Option>, seen_timestamp: Duration, ) { match self.chain.verify_optimistic_update_for_gossip( @@ -1515,15 +1487,19 @@ impl Worker { ); if let Some(sender) = reprocess_tx { + let processor = self.clone(); let msg = ReprocessQueueMessage::UnknownLightClientOptimisticUpdate( QueuedLightClientUpdate { - peer_id, - message_id, - light_client_optimistic_update: Box::new( - light_client_optimistic_update, - ), parent_root, - seen_timestamp, + process_fn: Box::new(move || { + processor.process_gossip_optimistic_update( + message_id, + peer_id, + light_client_optimistic_update, + None, // Do not reprocess this message again. + seen_timestamp, + ) + }), }, ); @@ -1612,11 +1588,11 @@ impl Worker { /// Handle an error whilst verifying an `Attestation` or `SignedAggregateAndProof` from the /// network. fn handle_attestation_verification_failure( - &self, + self: &Arc, peer_id: PeerId, message_id: MessageId, failed_att: FailedAtt, - reprocess_tx: Option>>, + reprocess_tx: Option>, error: AttnError, seen_timestamp: Duration, ) { @@ -1658,6 +1634,7 @@ impl Worker { attestation_verification::verify_propagation_slot_range( seen_clock, failed_att.attestation(), + &self.chain.spec, ); // Only penalize the peer if it would have been invalid at the moment we received @@ -1741,7 +1718,7 @@ impl Worker { "attn_agg_not_in_committee", ); } - AttnError::AttestationAlreadyKnown { .. } => { + AttnError::AttestationSupersetKnown { .. } => { /* * The aggregate attestation has already been observed on the network or in * a block. @@ -1848,11 +1825,18 @@ impl Worker { metrics::inc_counter( &metrics::BEACON_PROCESSOR_AGGREGATED_ATTESTATION_REQUEUED_TOTAL, ); + let processor = self.clone(); ReprocessQueueMessage::UnknownBlockAggregate(QueuedAggregate { - peer_id, - message_id, - attestation, - seen_timestamp, + beacon_block_root: *beacon_block_root, + process_fn: Box::new(move || { + processor.process_gossip_aggregate( + message_id, + peer_id, + attestation, + None, // Do not allow this attestation to be re-processed beyond this point. + seen_timestamp, + ) + }), }) } FailedAtt::Unaggregate { @@ -1864,13 +1848,20 @@ impl Worker { metrics::inc_counter( &metrics::BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_REQUEUED_TOTAL, ); + let processor = self.clone(); ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate { - peer_id, - message_id, - attestation, - subnet_id, - should_import, - seen_timestamp, + beacon_block_root: *beacon_block_root, + process_fn: Box::new(move || { + processor.process_gossip_attestation( + message_id, + peer_id, + attestation, + subnet_id, + should_import, + None, // Do not allow this attestation to be re-processed beyond this point. + seen_timestamp, + ) + }), }) } }; @@ -2071,7 +2062,7 @@ impl Worker { ); } AttnError::BeaconChainError(BeaconChainError::DBError(Error::HotColdDBError( - HotColdDBError::AttestationStateIsFinalized { .. }, + HotColdDBError::FinalizedStateNotInHotDatabase { .. }, ))) => { debug!(self.log, "Attestation for finalized state"; "peer_id" => % peer_id); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2192,6 +2183,7 @@ impl Worker { sync_committee_verification::verify_propagation_slot_range( seen_clock, &sync_committee_message_slot, + &self.chain.spec, ); hindsight_verification.is_err() }; @@ -2250,7 +2242,7 @@ impl Worker { "sync_bad_aggregator", ); } - SyncCommitteeError::SyncContributionAlreadyKnown(_) + SyncCommitteeError::SyncContributionSupersetKnown(_) | SyncCommitteeError::AggregatorAlreadyKnown(_) => { /* * The sync committee message already been observed on the network or in @@ -2504,6 +2496,7 @@ impl Worker { let is_timely = attestation_verification::verify_propagation_slot_range( &self.chain.slot_clock, attestation, + &self.chain.spec, ) .is_ok(); diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs new file mode 100644 index 000000000..f8c4e37ff --- /dev/null +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -0,0 +1,583 @@ +use crate::{ + service::NetworkMessage, + sync::{manager::BlockProcessType, SyncMessage}, +}; +use beacon_chain::{ + builder::Witness, eth1_chain::CachingEth1Backend, test_utils::BeaconChainHarness, BeaconChain, +}; +use beacon_chain::{BeaconChainTypes, NotifyExecutionLayer}; +use beacon_processor::{ + work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorChannels, BeaconProcessorSend, + DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, + WorkEvent as BeaconWorkEvent, +}; +use environment::null_logger; +use lighthouse_network::{ + rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, + Client, MessageId, NetworkGlobals, PeerId, PeerRequestId, +}; +use slog::{debug, Logger}; +use slot_clock::ManualSlotClock; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use store::MemoryStore; +use task_executor::test_utils::TestRuntime; +use task_executor::TaskExecutor; +use tokio::sync::mpsc::{self, error::TrySendError}; +use types::*; + +pub use sync_methods::ChainSegmentProcessId; + +pub type Error = TrySendError>; + +mod gossip_methods; +mod rpc_methods; +mod sync_methods; +mod tests; + +pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; + +/// Defines if and where we will store the SSZ files of invalid blocks. +#[derive(Clone)] +pub enum InvalidBlockStorage { + Enabled(PathBuf), + Disabled, +} + +/// Provides an interface to a `BeaconProcessor` running in some other thread. +/// The wider `networking` crate should use this struct to interface with the +/// beacon processor. +pub struct NetworkBeaconProcessor { + pub beacon_processor_send: BeaconProcessorSend, + pub duplicate_cache: DuplicateCache, + 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, + pub log: Logger, +} + +impl NetworkBeaconProcessor { + fn try_send(&self, event: BeaconWorkEvent) -> Result<(), Error> { + self.beacon_processor_send + .try_send(event) + .map_err(Into::into) + } + + /// Create a new `Work` event for some unaggregated attestation. + pub fn send_unaggregated_attestation( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + attestation: Attestation, + 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, + ) + }; + + // 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)) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipAttestation { + attestation: GossipAttestationPackage { + message_id, + peer_id, + attestation: Box::new(attestation), + subnet_id, + should_import, + seen_timestamp, + }, + process_individual: Box::new(process_individual), + process_batch: Box::new(process_batch), + }, + }) + } + + /// Create a new `Work` event for some aggregated attestation. + pub fn send_aggregated_attestation( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + aggregate: SignedAggregateAndProof, + seen_timestamp: Duration, + ) -> Result<(), Error> { + // 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), + 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 beacon_block_root = aggregate.message.aggregate.data.beacon_block_root; + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipAggregate { + aggregate: GossipAggregatePackage { + message_id, + peer_id, + aggregate: Box::new(aggregate), + beacon_block_root, + seen_timestamp, + }, + process_individual: Box::new(process_individual), + process_batch: Box::new(process_batch), + }, + }) + } + + /// Create a new `Work` event for some block. + pub fn send_gossip_beacon_block( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + peer_client: Client, + block: Arc>, + seen_timestamp: Duration, + ) -> 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 + .process_gossip_block( + message_id, + peer_id, + peer_client, + block, + reprocess_tx, + duplicate_cache, + invalid_block_storage, + seen_timestamp, + ) + .await + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipBlock(Box::pin(process_fn)), + }) + } + + /// Create a new `Work` event for some sync committee signature. + pub fn send_gossip_sync_signature( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + sync_signature: SyncCommitteeMessage, + subnet_id: SyncSubnetId, + seen_timestamp: Duration, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_gossip_sync_committee_signature( + message_id, + peer_id, + sync_signature, + subnet_id, + seen_timestamp, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipSyncSignature(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some sync committee contribution. + pub fn send_gossip_sync_contribution( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + sync_contribution: SignedContributionAndProof, + seen_timestamp: Duration, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_sync_committee_contribution( + message_id, + peer_id, + sync_contribution, + seen_timestamp, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipSyncContribution(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some exit. + pub fn send_gossip_voluntary_exit( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + voluntary_exit: Box, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.process_gossip_voluntary_exit(message_id, peer_id, *voluntary_exit); + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipVoluntaryExit(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some proposer slashing. + pub fn send_gossip_proposer_slashing( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + proposer_slashing: Box, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipProposerSlashing(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some light client finality update. + pub fn send_gossip_light_client_finality_update( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + light_client_finality_update: LightClientFinalityUpdate, + seen_timestamp: Duration, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_gossip_finality_update( + message_id, + peer_id, + light_client_finality_update, + seen_timestamp, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipLightClientFinalityUpdate(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some light client optimistic update. + pub fn send_gossip_light_client_optimistic_update( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + light_client_optimistic_update: LightClientOptimisticUpdate, + seen_timestamp: Duration, + ) -> 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), + seen_timestamp, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::GossipLightClientOptimisticUpdate(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some attester slashing. + pub fn send_gossip_attester_slashing( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + attester_slashing: Box>, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipAttesterSlashing(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some BLS to execution change. + pub fn send_gossip_bls_to_execution_change( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + bls_to_execution_change: Box, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || { + processor.process_gossip_bls_to_execution_change( + message_id, + peer_id, + *bls_to_execution_change, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipBlsToExecutionChange(Box::new(process_fn)), + }) + } + + /// Create a new `Work` event for some block, where the result from computation (if any) is + /// sent to the other side of `result_tx`. + pub fn send_rpc_beacon_block( + self: &Arc, + block_root: Hash256, + block: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Result<(), Error> { + let process_fn = self.clone().generate_rpc_beacon_block_process_fn( + block_root, + block, + seen_timestamp, + process_type, + ); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::RpcBlock { process_fn }, + }) + } + + /// Create a new work event to import `blocks` as a beacon chain segment. + pub fn send_chain_segment( + self: &Arc, + process_id: ChainSegmentProcessId, + blocks: Vec>>, + ) -> Result<(), Error> { + let is_backfill = matches!(&process_id, ChainSegmentProcessId::BackSyncBatchId { .. }); + 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) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work, + }) + } + + /// Create a new work event to process `StatusMessage`s from the RPC network. + pub fn send_status_message( + self: &Arc, + peer_id: PeerId, + message: StatusMessage, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move || processor.process_status(peer_id, message); + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::Status(Box::new(process_fn)), + }) + } + + /// Create a new work event to process `BlocksByRangeRequest`s from the RPC network. + pub fn send_blocks_by_range_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: BlocksByRangeRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move |send_idle_on_drop| { + let executor = processor.executor.clone(); + processor.handle_blocks_by_range_request( + executor, + send_idle_on_drop, + peer_id, + request_id, + request, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::BlocksByRangeRequest(Box::new(process_fn)), + }) + } + + /// Create a new work event to process `BlocksByRootRequest`s from the RPC network. + pub fn send_blocks_by_roots_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: BlocksByRootRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = move |send_idle_on_drop| { + let executor = processor.executor.clone(); + processor.handle_blocks_by_root_request( + executor, + send_idle_on_drop, + peer_id, + request_id, + request, + ) + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::BlocksByRootsRequest(Box::new(process_fn)), + }) + } + + /// Create a new work event to process `LightClientBootstrap`s from the RPC network. + pub fn send_lightclient_bootstrap_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: LightClientBootstrapRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.handle_light_client_bootstrap(peer_id, request_id, request); + + self.try_send(BeaconWorkEvent { + drop_during_sync: true, + work: Work::LightClientBootstrapRequest(Box::new(process_fn)), + }) + } + + /// Send a message to `sync_tx`. + /// + /// Creates a log if there is an internal error. + fn send_sync_message(&self, message: SyncMessage) { + self.sync_tx.send(message).unwrap_or_else(|e| { + debug!(self.log, "Could not send message to the sync service"; + "error" => %e) + }); + } + + /// Send a message to `network_tx`. + /// + /// Creates a log if there is an internal error. + fn send_network_message(&self, message: NetworkMessage) { + self.network_tx.send(message).unwrap_or_else(|e| { + debug!(self.log, "Could not send message to the network service. Likely shutdown"; + "error" => %e) + }); + } +} + +type TestBeaconChainType = + Witness, E, MemoryStore, MemoryStore>; + +impl NetworkBeaconProcessor> { + // Instantiates a mostly non-functional version of `Self` and returns the + // event receiver that would normally go to the beacon processor. This is + // useful for testing that messages are actually being sent to the beacon + // processor (but not much else). + pub fn null_for_testing( + network_globals: Arc>, + ) -> (Self, mpsc::Receiver>) { + 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(); + let (sync_tx, _sync_rx) = mpsc::unbounded_channel(); + let log = null_logger().unwrap(); + let harness: BeaconChainHarness> = + BeaconChainHarness::builder(E::default()) + .spec(E::default_spec()) + .deterministic_keypairs(8) + .logger(log.clone()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let runtime = TestRuntime::default(); + + let network_beacon_processor = Self { + beacon_processor_send: beacon_processor_tx, + duplicate_cache: DuplicateCache::default(), + chain: harness.chain, + network_tx, + sync_tx, + reprocess_tx: work_reprocessing_tx, + network_globals, + invalid_block_storage: InvalidBlockStorage::Disabled, + executor: runtime.task_executor.clone(), + log, + }; + + (network_beacon_processor, beacon_processor_rx) + } +} diff --git a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs similarity index 92% rename from beacon_node/network/src/beacon_processor/worker/rpc_methods.rs rename to beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 81b163bf7..19b0a60a4 100644 --- a/beacon_node/network/src/beacon_processor/worker/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -1,21 +1,21 @@ -use crate::beacon_processor::{worker::FUTURE_SLOT_TOLERANCE, SendOnDrop}; +use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::SyncMessage; use beacon_chain::{BeaconChainError, BeaconChainTypes, HistoricalBlockError, WhenSlotSkipped}; +use beacon_processor::SendOnDrop; use itertools::process_results; use lighthouse_network::rpc::StatusMessage; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; use slog::{debug, error, warn}; use slot_clock::SlotClock; +use std::sync::Arc; use task_executor::TaskExecutor; use tokio_stream::StreamExt; use types::{light_client_bootstrap::LightClientBootstrap, Epoch, EthSpec, Hash256, Slot}; -use super::Worker; - -impl Worker { +impl NetworkBeaconProcessor { /* Auxiliary functions */ /// Disconnects and ban's a peer, sending a Goodbye request with the associated reason. @@ -124,17 +124,17 @@ impl Worker { /// Handle a `BlocksByRoot` request from the peer. pub fn handle_blocks_by_root_request( - self, + self: Arc, executor: TaskExecutor, send_on_drop: SendOnDrop, peer_id: PeerId, request_id: PeerRequestId, request: BlocksByRootRequest, ) { - let requested_blocks = request.block_roots.len(); + let requested_blocks = request.block_roots().len(); let mut block_stream = match self .chain - .get_blocks_checking_early_attester_cache(request.block_roots.into(), &executor) + .get_blocks_checking_early_attester_cache(request.block_roots().to_vec(), &executor) { Ok(block_stream) => block_stream, Err(e) => return error!(self.log, "Error getting block stream"; "error" => ?e), @@ -210,7 +210,7 @@ impl Worker { /// Handle a `BlocksByRoot` request from the peer. pub fn handle_light_client_bootstrap( - self, + self: &Arc, peer_id: PeerId, request_id: PeerRequestId, request: LightClientBootstrapRequest, @@ -283,7 +283,7 @@ impl Worker { /// Handle a `BlocksByRange` request from the peer. pub fn handle_blocks_by_range_request( - self, + self: Arc, executor: TaskExecutor, send_on_drop: SendOnDrop, peer_id: PeerId, @@ -292,18 +292,18 @@ impl Worker { ) { debug!(self.log, "Received BlocksByRange Request"; "peer_id" => %peer_id, - "count" => req.count, - "start_slot" => req.start_slot, + "count" => req.count(), + "start_slot" => req.start_slot(), ); // Should not send more than max request blocks - if req.count > MAX_REQUEST_BLOCKS { - req.count = MAX_REQUEST_BLOCKS; + if *req.count() > MAX_REQUEST_BLOCKS { + *req.count_mut() = MAX_REQUEST_BLOCKS; } let forwards_block_root_iter = match self .chain - .forwards_iter_block_roots(Slot::from(req.start_slot)) + .forwards_iter_block_roots(Slot::from(*req.start_slot())) { Ok(iter) => iter, Err(BeaconChainError::HistoricalBlockError( @@ -326,18 +326,20 @@ impl Worker { // Pick out the required blocks, ignoring skip-slots. let mut last_block_root = None; let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { - iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) - // map skip slots to None - .map(|(root, _)| { - let result = if Some(root) == last_block_root { - None - } else { - Some(root) - }; - last_block_root = Some(root); - result - }) - .collect::>>() + iter.take_while(|(_, slot)| { + slot.as_u64() < req.start_slot().saturating_add(*req.count()) + }) + // map skip slots to None + .map(|(root, _)| { + let result = if Some(root) == last_block_root { + None + } else { + Some(root) + }; + last_block_root = Some(root); + result + }) + .collect::>>() }); let block_roots = match maybe_block_roots { @@ -364,8 +366,8 @@ impl Worker { Ok(Some(block)) => { // Due to skip slots, blocks could be out of the range, we ensure they // are in the range before sending - if block.slot() >= req.start_slot - && block.slot() < req.start_slot + req.count + if block.slot() >= *req.start_slot() + && block.slot() < req.start_slot() + req.count() { blocks_sent += 1; self.send_network_message(NetworkMessage::SendResponse { @@ -440,15 +442,15 @@ impl Worker { .slot() .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); - if blocks_sent < (req.count as usize) { + if blocks_sent < (*req.count() as usize) { debug!( self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, "msg" => "Failed to return all requested blocks", - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } else { @@ -456,9 +458,9 @@ impl Worker { self.log, "BlocksByRange outgoing response processed"; "peer" => %peer_id, - "start_slot" => req.start_slot, + "start_slot" => req.start_slot(), "current_slot" => current_slot, - "requested" => req.count, + "requested" => req.count(), "returned" => blocks_sent ); } diff --git a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs similarity index 88% rename from beacon_node/network/src/beacon_processor/worker/sync_methods.rs rename to beacon_node/network/src/network_beacon_processor/sync_methods.rs index 2dbb5a346..c33e2acf5 100644 --- a/beacon_node/network/src/beacon_processor/worker/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -1,18 +1,21 @@ use std::time::Duration; -use super::{super::work_reprocessing_queue::ReprocessQueueMessage, Worker}; -use crate::beacon_processor::work_reprocessing_queue::QueuedRpcBlock; -use crate::beacon_processor::worker::FUTURE_SLOT_TOLERANCE; -use crate::beacon_processor::DuplicateCache; use crate::metrics; -use crate::sync::manager::{BlockProcessType, SyncMessage}; -use crate::sync::{BatchProcessResult, ChainId}; -use beacon_chain::CountUnrealized; +use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; +use crate::sync::BatchProcessResult; +use crate::sync::{ + manager::{BlockProcessType, SyncMessage}, + ChainId, +}; use beacon_chain::{ observed_block_producers::Error as ObserveError, validator_monitor::get_block_delay_ms, BeaconChainError, BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, }; +use beacon_processor::{ + work_reprocessing_queue::{QueuedRpcBlock, ReprocessQueueMessage}, + AsyncFn, BlockingFn, DuplicateCache, +}; use lighthouse_network::PeerAction; use slog::{debug, error, info, warn}; use slot_clock::SlotClock; @@ -25,7 +28,7 @@ use types::{Epoch, Hash256, SignedBeaconBlock}; #[derive(Clone, Debug, PartialEq)] pub enum ChainSegmentProcessId { /// Processing Id of a range syncing batch. - RangeBatchId(ChainId, Epoch, CountUnrealized), + RangeBatchId(ChainId, Epoch), /// Processing ID for a backfill syncing batch. BackSyncBatchId(Epoch), /// Processing Id of the parent lookup of a block. @@ -40,27 +43,71 @@ struct ChainSegmentFailed { peer_action: Option, } -impl Worker { - /// Attempt to process a block received from a direct RPC request. - #[allow(clippy::too_many_arguments)] - pub async fn process_rpc_block( - self, +impl NetworkBeaconProcessor { + /// Returns an async closure which processes a beacon block recieved via RPC. + /// + /// This separate function was required to prevent a cycle during compiler + /// type checking. + pub fn generate_rpc_beacon_block_process_fn( + self: Arc, block_root: Hash256, block: Arc>, seen_timestamp: Duration, process_type: BlockProcessType, - reprocess_tx: mpsc::Sender>, - duplicate_cache: DuplicateCache, - should_process: bool, - ) { - if !should_process { + ) -> 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; + }; + Box::pin(process_fn) + } + + /// Returns the `process_fn` and `ignore_fn` required when requeuing an RPC block. + pub fn generate_rpc_beacon_block_fns( + self: Arc, + block_root: Hash256, + block: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> (AsyncFn, BlockingFn) { + // An async closure which will import the block. + let process_fn = self.clone().generate_rpc_beacon_block_process_fn( + block_root, + block, + seen_timestamp, + process_type.clone(), + ); + // A closure which will ignore the block. + let ignore_fn = move || { // Sync handles these results self.send_sync_message(SyncMessage::BlockProcessed { process_type, result: crate::sync::manager::BlockProcessResult::Ignored, }); - return; - } + }; + (process_fn, Box::new(ignore_fn)) + } + + /// Attempt to process a block received from a direct RPC request. + #[allow(clippy::too_many_arguments)] + pub async fn process_rpc_block( + self: Arc>, + block_root: Hash256, + block: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + reprocess_tx: mpsc::Sender, + duplicate_cache: DuplicateCache, + ) { // Check if the block is already being imported through another source let handle = match duplicate_cache.check_and_insert(block_root) { Some(handle) => handle, @@ -71,13 +118,18 @@ impl Worker { "action" => "sending rpc block to reprocessing queue", "block_root" => %block_root, ); + // Send message to work reprocess queue to retry the block - let reprocess_msg = ReprocessQueueMessage::RpcBlock(QueuedRpcBlock { + let (process_fn, ignore_fn) = self.clone().generate_rpc_beacon_block_fns( block_root, - block: block.clone(), - process_type, + block, seen_timestamp, - should_process: true, + process_type, + ); + let reprocess_msg = ReprocessQueueMessage::RpcBlock(QueuedRpcBlock { + beacon_block_root: block_root, + process_fn, + ignore_fn, }); if reprocess_tx.try_send(reprocess_msg).is_err() { @@ -99,33 +151,21 @@ impl Worker { }); // Checks if a block from this proposer is already known. - let proposal_already_known = || { + let block_equivocates = || { match self .chain .observed_block_producers .read() - .proposer_has_been_observed(block.message()) + .proposer_has_been_observed(block.message(), block.canonical_root()) { - Ok(is_observed) => is_observed, - // Both of these blocks will be rejected, so reject them now rather + Ok(seen_status) => seen_status.is_slashable(), + //Both of these blocks will be rejected, so reject them now rather // than re-queuing them. Err(ObserveError::FinalizedBlock { .. }) | Err(ObserveError::ValidatorIndexTooHigh { .. }) => false, } }; - // Returns `true` if the block is already known to fork choice. Notably, - // this will return `false` for blocks that we've already imported but - // ancestors of the finalized checkpoint. That should not be an issue - // for our use here since finalized blocks will always be late and won't - // be requeued anyway. - let block_is_already_known = || { - self.chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - }; - // If we've already seen a block from this proposer *and* the block // arrived before the attestation deadline, requeue it to ensure it is // imported late enough that it won't receive a proposer boost. @@ -133,7 +173,7 @@ impl Worker { // Don't requeue blocks if they're already known to fork choice, just // push them through to block processing so they can be handled through // the normal channels. - if !block_is_late && proposal_already_known() && !block_is_already_known() { + if !block_is_late && block_equivocates() { debug!( self.log, "Delaying processing of duplicate RPC block"; @@ -143,12 +183,16 @@ impl Worker { ); // Send message to work reprocess queue to retry the block - let reprocess_msg = ReprocessQueueMessage::RpcBlock(QueuedRpcBlock { + let (process_fn, ignore_fn) = self.clone().generate_rpc_beacon_block_fns( block_root, - block: block.clone(), - process_type, + block, seen_timestamp, - should_process: true, + process_type, + ); + let reprocess_msg = ReprocessQueueMessage::RpcBlock(QueuedRpcBlock { + beacon_block_root: block_root, + process_fn, + ignore_fn, }); if reprocess_tx.try_send(reprocess_msg).is_err() { @@ -166,12 +210,7 @@ impl Worker { let parent_root = block.message().parent_root(); let result = self .chain - .process_block( - block_root, - block, - CountUnrealized::True, - NotifyExecutionLayer::Yes, - ) + .process_block(block_root, block, NotifyExecutionLayer::Yes, || Ok(())) .await; metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); @@ -220,17 +259,13 @@ impl Worker { ) { let result = match sync_type { // this a request from the range sync - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, count_unrealized) => { + 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(); match self - .process_blocks( - downloaded_blocks.iter(), - count_unrealized, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter(), notify_execution_layer) .await { (_, Ok(_)) => { @@ -309,11 +344,7 @@ impl Worker { // parent blocks are ordered from highest slot to lowest, so we need to process in // reverse match self - .process_blocks( - downloaded_blocks.iter().rev(), - CountUnrealized::True, - notify_execution_layer, - ) + .process_blocks(downloaded_blocks.iter().rev(), notify_execution_layer) .await { (imported_blocks, Err(e)) => { @@ -343,13 +374,12 @@ impl Worker { async fn process_blocks<'a>( &self, downloaded_blocks: impl Iterator>>, - count_unrealized: CountUnrealized, notify_execution_layer: NotifyExecutionLayer, ) -> (usize, Result<(), ChainSegmentFailed>) { let blocks: Vec> = downloaded_blocks.cloned().collect(); match self .chain - .process_chain_segment(blocks, count_unrealized, notify_execution_layer) + .process_chain_segment(blocks, notify_execution_layer) .await { ChainSegmentResult::Successful { imported_blocks } => { diff --git a/beacon_node/network/src/beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs similarity index 85% rename from beacon_node/network/src/beacon_processor/tests.rs rename to beacon_node/network/src/network_beacon_processor/tests.rs index b93e83ad7..ac5722a56 100644 --- a/beacon_node/network/src/beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -1,30 +1,32 @@ #![cfg(not(debug_assertions))] // Tests are too slow in debug. #![cfg(test)] -use crate::beacon_processor::work_reprocessing_queue::{ - QUEUED_ATTESTATION_DELAY, QUEUED_RPC_BLOCK_DELAY, +use crate::{ + network_beacon_processor::{ + ChainSegmentProcessId, DuplicateCache, InvalidBlockStorage, NetworkBeaconProcessor, + }, + service::NetworkMessage, + sync::{manager::BlockProcessType, SyncMessage}, }; -use crate::beacon_processor::*; -use crate::{service::NetworkMessage, sync::SyncMessage}; use beacon_chain::test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; -use beacon_chain::{BeaconChain, ChainConfig, MAXIMUM_GOSSIP_CLOCK_DISPARITY}; +use beacon_chain::BeaconChain; +use beacon_processor::{work_reprocessing_queue::*, *}; use lighthouse_network::{ discv5::enr::{CombinedKey, EnrBuilder}, rpc::methods::{MetaData, MetaDataV2}, types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}, - MessageId, NetworkGlobals, PeerId, + Client, MessageId, NetworkGlobals, PeerId, }; use slot_clock::SlotClock; -use std::cmp; use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use types::{ - Attestation, AttesterSlashing, Epoch, EthSpec, MainnetEthSpec, ProposerSlashing, - SignedBeaconBlock, SignedVoluntaryExit, SubnetId, + Attestation, AttesterSlashing, Epoch, EthSpec, Hash256, MainnetEthSpec, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedVoluntaryExit, SubnetId, }; type E = MainnetEthSpec; @@ -35,7 +37,6 @@ const VALIDATOR_COUNT: usize = SLOTS_PER_EPOCH as usize; const SMALL_CHAIN: u64 = 2; const LONG_CHAIN: u64 = SLOTS_PER_EPOCH * 2; -const TCP_PORT: u16 = 42; const SEQ_NUMBER: u64 = 0; /// The default time to wait for `BeaconProcessor` events. @@ -51,11 +52,12 @@ struct TestRig { attester_slashing: AttesterSlashing, proposer_slashing: ProposerSlashing, voluntary_exit: SignedVoluntaryExit, - beacon_processor_tx: mpsc::Sender>, + beacon_processor_tx: BeaconProcessorSend, work_journal_rx: mpsc::Receiver<&'static str>, _network_rx: mpsc::UnboundedReceiver>, _sync_rx: mpsc::UnboundedReceiver>, duplicate_cache: DuplicateCache, + network_beacon_processor: Arc>, _harness: BeaconChainHarness, } @@ -64,16 +66,21 @@ struct TestRig { impl Drop for TestRig { fn drop(&mut self) { // Causes the beacon processor to shutdown. - self.beacon_processor_tx = mpsc::channel(MAX_WORK_EVENT_QUEUE_LEN).0; + let len = BeaconProcessorConfig::default().max_work_event_queue_len; + self.beacon_processor_tx = BeaconProcessorSend(mpsc::channel(len).0); } } impl TestRig { pub async fn new(chain_length: u64) -> Self { - Self::new_with_chain_config(chain_length, ChainConfig::default()).await + Self::new_parametric( + chain_length, + BeaconProcessorConfig::default().enable_backfill_rate_limiting, + ) + .await } - pub async fn new_with_chain_config(chain_length: u64, chain_config: ChainConfig) -> Self { + pub async fn new_parametric(chain_length: u64, enable_backfill_rate_limiting: bool) -> Self { // This allows for testing voluntary exits without building out a massive chain. let mut spec = E::default_spec(); spec.shard_committee_period = 2; @@ -82,7 +89,7 @@ impl TestRig { .spec(spec) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() - .chain_config(chain_config) + .chain_config(<_>::default()) .build(); harness.advance_slot(); @@ -168,7 +175,15 @@ impl TestRig { let log = harness.logger().clone(); - let (beacon_processor_tx, beacon_processor_rx) = mpsc::channel(MAX_WORK_EVENT_QUEUE_LEN); + let mut beacon_processor_config = BeaconProcessorConfig::default(); + beacon_processor_config.enable_backfill_rate_limiting = enable_backfill_rate_limiting; + 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(); // Default metadata @@ -179,34 +194,44 @@ impl TestRig { }); let enr_key = CombinedKey::generate_secp256k1(); let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); - let network_globals = Arc::new(NetworkGlobals::new( - enr, - Some(TCP_PORT), - None, - meta_data, - vec![], - false, - &log, - )); + let network_globals = Arc::new(NetworkGlobals::new(enr, meta_data, vec![], false, &log)); let executor = harness.runtime.task_executor.clone(); let (work_journal_tx, work_journal_rx) = mpsc::channel(16_364); let duplicate_cache = DuplicateCache::default(); - BeaconProcessor { - beacon_chain: Arc::downgrade(&chain), + let network_beacon_processor = NetworkBeaconProcessor { + beacon_processor_send: beacon_processor_tx.clone(), + duplicate_cache: duplicate_cache.clone(), + 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(), + log: log.clone(), + }; + let network_beacon_processor = Arc::new(network_beacon_processor); + + let beacon_processor = BeaconProcessor { network_globals, executor, - max_workers: cmp::max(1, num_cpus::get()), current_workers: 0, - importing_blocks: duplicate_cache.clone(), - invalid_block_storage: InvalidBlockStorage::Disabled, + config: beacon_processor_config, log: log.clone(), } - .spawn_manager(beacon_processor_rx, Some(work_journal_tx)); + .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(), + ); + + assert!(!beacon_processor.is_err()); Self { chain, @@ -222,6 +247,7 @@ impl TestRig { _network_rx, _sync_rx, duplicate_cache, + network_beacon_processor, _harness: harness, } } @@ -235,102 +261,105 @@ impl TestRig { } pub fn enqueue_gossip_block(&self) { - self.beacon_processor_tx - .try_send(WorkEvent::gossip_beacon_block( + self.network_beacon_processor + .send_gossip_beacon_block( junk_message_id(), junk_peer_id(), Client::default(), self.next_block.clone(), Duration::from_secs(0), - )) + ) .unwrap(); } pub fn enqueue_rpc_block(&self) { - let event = WorkEvent::rpc_beacon_block( - self.next_block.canonical_root(), - self.next_block.clone(), - std::time::Duration::default(), - BlockProcessType::ParentLookup { - chain_hash: Hash256::random(), - }, - ); - self.beacon_processor_tx.try_send(event).unwrap(); + self.network_beacon_processor + .send_rpc_beacon_block( + self.next_block.canonical_root(), + self.next_block.clone(), + std::time::Duration::default(), + BlockProcessType::ParentLookup { + chain_hash: Hash256::random(), + }, + ) + .unwrap(); } pub fn enqueue_single_lookup_rpc_block(&self) { - let event = WorkEvent::rpc_beacon_block( - self.next_block.canonical_root(), - self.next_block.clone(), - std::time::Duration::default(), - BlockProcessType::SingleBlock { id: 1 }, - ); - self.beacon_processor_tx.try_send(event).unwrap(); + self.network_beacon_processor + .send_rpc_beacon_block( + self.next_block.canonical_root(), + self.next_block.clone(), + std::time::Duration::default(), + BlockProcessType::SingleBlock { id: 1 }, + ) + .unwrap(); } pub fn enqueue_backfill_batch(&self) { - let event = WorkEvent::chain_segment( - ChainSegmentProcessId::BackSyncBatchId(Epoch::default()), - Vec::default(), - ); - self.beacon_processor_tx.try_send(event).unwrap(); + self.network_beacon_processor + .send_chain_segment( + ChainSegmentProcessId::BackSyncBatchId(Epoch::default()), + Vec::default(), + ) + .unwrap(); } pub fn enqueue_unaggregated_attestation(&self) { let (attestation, subnet_id) = self.attestations.first().unwrap().clone(); - self.beacon_processor_tx - .try_send(WorkEvent::unaggregated_attestation( + self.network_beacon_processor + .send_unaggregated_attestation( junk_message_id(), junk_peer_id(), attestation, subnet_id, true, Duration::from_secs(0), - )) + ) .unwrap(); } pub fn enqueue_gossip_attester_slashing(&self) { - self.beacon_processor_tx - .try_send(WorkEvent::gossip_attester_slashing( + self.network_beacon_processor + .send_gossip_attester_slashing( junk_message_id(), junk_peer_id(), Box::new(self.attester_slashing.clone()), - )) + ) .unwrap(); } pub fn enqueue_gossip_proposer_slashing(&self) { - self.beacon_processor_tx - .try_send(WorkEvent::gossip_proposer_slashing( + self.network_beacon_processor + .send_gossip_proposer_slashing( junk_message_id(), junk_peer_id(), Box::new(self.proposer_slashing.clone()), - )) + ) .unwrap(); } pub fn enqueue_gossip_voluntary_exit(&self) { - self.beacon_processor_tx - .try_send(WorkEvent::gossip_voluntary_exit( + self.network_beacon_processor + .send_gossip_voluntary_exit( junk_message_id(), junk_peer_id(), Box::new(self.voluntary_exit.clone()), - )) + ) .unwrap(); } pub fn enqueue_next_block_unaggregated_attestation(&self) { let (attestation, subnet_id) = self.next_block_attestations.first().unwrap().clone(); - self.beacon_processor_tx - .try_send(WorkEvent::unaggregated_attestation( + self.network_beacon_processor + .send_unaggregated_attestation( junk_message_id(), junk_peer_id(), attestation, subnet_id, true, Duration::from_secs(0), - )) + ) .unwrap(); } @@ -340,13 +369,13 @@ impl TestRig { .first() .unwrap() .clone(); - self.beacon_processor_tx - .try_send(WorkEvent::aggregated_attestation( + self.network_beacon_processor + .send_aggregated_attestation( junk_message_id(), junk_peer_id(), aggregate, Duration::from_secs(0), - )) + ) .unwrap(); } @@ -478,7 +507,7 @@ async fn import_gossip_block_acceptably_early() { rig.chain .slot_clock - .set_current_time(slot_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY); + .set_current_time(slot_start - rig.chain.spec.maximum_gossip_clock_disparity()); assert_eq!( rig.chain.slot().unwrap(), @@ -525,9 +554,9 @@ async fn import_gossip_block_unacceptably_early() { .start_of(rig.next_block.slot()) .unwrap(); - rig.chain - .slot_clock - .set_current_time(slot_start - MAXIMUM_GOSSIP_CLOCK_DISPARITY - Duration::from_millis(1)); + rig.chain.slot_clock.set_current_time( + slot_start - rig.chain.spec.maximum_gossip_clock_disparity() - Duration::from_millis(1), + ); assert_eq!( rig.chain.slot().unwrap(), @@ -913,11 +942,8 @@ 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 chain_config = ChainConfig { - enable_backfill_rate_limiting: false, - ..Default::default() - }; - let mut rig = TestRig::new_with_chain_config(SMALL_CHAIN, chain_config).await; + let enable_backfill_rate_limiting = false; + let mut rig = TestRig::new_parametric(SMALL_CHAIN, enable_backfill_rate_limiting).await; for _ in 0..3 { rig.enqueue_backfill_batch(); diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 1b0f1fb41..c8332705c 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -5,23 +5,24 @@ //! syncing-related responses to the Sync manager. #![allow(clippy::unit_arg)] -use crate::beacon_processor::{ - BeaconProcessor, InvalidBlockStorage, WorkEvent as BeaconWorkEvent, MAX_WORK_EVENT_QUEUE_LEN, -}; use crate::error; +use crate::network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}; use crate::service::{NetworkMessage, RequestId}; use crate::status::status_message; use crate::sync::manager::RequestId as SyncId; use crate::sync::SyncMessage; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_processor::{ + work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend, DuplicateCache, +}; use futures::prelude::*; use lighthouse_network::rpc::*; use lighthouse_network::{ MessageId, NetworkGlobals, PeerId, PeerRequestId, PubsubMessage, Request, Response, }; +use logging::TimeLatch; use slog::{debug, o, trace}; use slog::{error, warn}; -use std::cmp; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; @@ -39,9 +40,11 @@ pub struct Router { /// A network context to return and handle RPC requests. network: HandlerNetworkContext, /// A multi-threaded, non-blocking processor for applying messages to the beacon chain. - beacon_processor_send: mpsc::Sender>, + network_beacon_processor: Arc>, /// The `Router` logger. log: slog::Logger, + /// Provides de-bounce functionality for logging. + logger_debounce: TimeLatch, } /// Types of messages the router can receive. @@ -76,12 +79,15 @@ pub enum RouterMessage { impl Router { /// Initializes and runs the Router. + #[allow(clippy::too_many_arguments)] pub fn spawn( beacon_chain: Arc>, network_globals: Arc>, network_send: mpsc::UnboundedSender>, executor: task_executor::TaskExecutor, invalid_block_storage: InvalidBlockStorage, + beacon_processor_send: BeaconProcessorSend, + beacon_processor_reprocess_tx: mpsc::Sender, log: slog::Logger, ) -> error::Result>> { let message_handler_log = log.new(o!("service"=> "router")); @@ -89,34 +95,33 @@ impl Router { let (handler_send, handler_recv) = mpsc::unbounded_channel(); - let (beacon_processor_send, beacon_processor_receive) = - mpsc::channel(MAX_WORK_EVENT_QUEUE_LEN); - let sync_logger = log.new(o!("service"=> "sync")); + // generate the message channel + let (sync_send, sync_recv) = mpsc::unbounded_channel::>(); - // spawn the sync thread - let sync_send = crate::sync::manager::spawn( - executor.clone(), - beacon_chain.clone(), - network_globals.clone(), - network_send.clone(), - beacon_processor_send.clone(), - sync_logger, - ); - - BeaconProcessor { - beacon_chain: Arc::downgrade(&beacon_chain), + let network_beacon_processor = NetworkBeaconProcessor { + beacon_processor_send, + duplicate_cache: DuplicateCache::default(), + 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(), - executor: executor.clone(), - max_workers: cmp::max(1, num_cpus::get()), - current_workers: 0, - importing_blocks: Default::default(), invalid_block_storage, + executor: executor.clone(), log: log.clone(), - } - .spawn_manager(beacon_processor_receive, None); + }; + let network_beacon_processor = Arc::new(network_beacon_processor); + + // spawn the sync thread + crate::sync::manager::spawn( + executor.clone(), + beacon_chain.clone(), + network_send.clone(), + network_beacon_processor.clone(), + sync_recv, + sync_logger, + ); // generate the Message handler let mut handler = Router { @@ -124,8 +129,9 @@ impl Router { chain: beacon_chain, sync_send, network: HandlerNetworkContext::new(network_send, log.clone()), - beacon_processor_send, + network_beacon_processor, log: message_handler_log, + logger_debounce: TimeLatch::default(), }; // spawn handler task and move the message handler instance into the spawned thread @@ -192,14 +198,17 @@ impl Router { Request::Status(status_message) => { self.on_status_request(peer_id, request_id, status_message) } - Request::BlocksByRange(request) => self.send_beacon_processor_work( - BeaconWorkEvent::blocks_by_range_request(peer_id, request_id, request), + Request::BlocksByRange(request) => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_blocks_by_range_request(peer_id, request_id, request), ), - Request::BlocksByRoot(request) => self.send_beacon_processor_work( - BeaconWorkEvent::blocks_by_roots_request(peer_id, request_id, request), + Request::BlocksByRoot(request) => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_blocks_by_roots_request(peer_id, request_id, request), ), - Request::LightClientBootstrap(request) => self.send_beacon_processor_work( - BeaconWorkEvent::lightclient_bootstrap_request(peer_id, request_id, request), + Request::LightClientBootstrap(request) => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_lightclient_bootstrap_request(peer_id, request_id, request), ), } } @@ -214,10 +223,10 @@ impl Router { match response { Response::Status(status_message) => { debug!(self.log, "Received Status Response"; "peer_id" => %peer_id, &status_message); - self.send_beacon_processor_work(BeaconWorkEvent::status_message( - peer_id, - status_message, - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_status_message(peer_id, status_message), + ) } Response::BlocksByRange(beacon_block) => { self.on_blocks_by_range_response(peer_id, request_id, beacon_block); @@ -242,36 +251,40 @@ impl Router { ) { match gossip_message { PubsubMessage::AggregateAndProofAttestation(aggregate_and_proof) => self - .send_beacon_processor_work(BeaconWorkEvent::aggregated_attestation( - message_id, - peer_id, - *aggregate_and_proof, - timestamp_now(), - )), - PubsubMessage::Attestation(subnet_attestation) => { - self.send_beacon_processor_work(BeaconWorkEvent::unaggregated_attestation( - message_id, - peer_id, - subnet_attestation.1, - subnet_attestation.0, - should_process, - timestamp_now(), - )) - } - PubsubMessage::BeaconBlock(block) => { - self.send_beacon_processor_work(BeaconWorkEvent::gossip_beacon_block( + .handle_beacon_processor_send_result( + self.network_beacon_processor.send_aggregated_attestation( + message_id, + peer_id, + *aggregate_and_proof, + timestamp_now(), + ), + ), + PubsubMessage::Attestation(subnet_attestation) => self + .handle_beacon_processor_send_result( + self.network_beacon_processor.send_unaggregated_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, peer_id, self.network_globals.client(&peer_id), block, timestamp_now(), - )) - } + ), + ), PubsubMessage::VoluntaryExit(exit) => { debug!(self.log, "Received a voluntary exit"; "peer_id" => %peer_id); - self.send_beacon_processor_work(BeaconWorkEvent::gossip_voluntary_exit( - message_id, peer_id, exit, - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_gossip_voluntary_exit(message_id, peer_id, exit), + ) } PubsubMessage::ProposerSlashing(proposer_slashing) => { debug!( @@ -279,11 +292,13 @@ impl Router { "Received a proposer slashing"; "peer_id" => %peer_id ); - self.send_beacon_processor_work(BeaconWorkEvent::gossip_proposer_slashing( - message_id, - peer_id, - proposer_slashing, - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_proposer_slashing( + message_id, + peer_id, + proposer_slashing, + ), + ) } PubsubMessage::AttesterSlashing(attester_slashing) => { debug!( @@ -291,11 +306,13 @@ impl Router { "Received a attester slashing"; "peer_id" => %peer_id ); - self.send_beacon_processor_work(BeaconWorkEvent::gossip_attester_slashing( - message_id, - peer_id, - attester_slashing, - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_attester_slashing( + message_id, + peer_id, + attester_slashing, + ), + ) } PubsubMessage::SignedContributionAndProof(contribution_and_proof) => { trace!( @@ -303,12 +320,14 @@ impl Router { "Received sync committee aggregate"; "peer_id" => %peer_id ); - self.send_beacon_processor_work(BeaconWorkEvent::gossip_sync_contribution( - message_id, - peer_id, - *contribution_and_proof, - timestamp_now(), - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_sync_contribution( + message_id, + peer_id, + *contribution_and_proof, + timestamp_now(), + ), + ) } PubsubMessage::SyncCommitteeMessage(sync_committtee_msg) => { trace!( @@ -316,13 +335,15 @@ impl Router { "Received sync committee signature"; "peer_id" => %peer_id ); - self.send_beacon_processor_work(BeaconWorkEvent::gossip_sync_signature( - message_id, - peer_id, - sync_committtee_msg.1, - sync_committtee_msg.0, - timestamp_now(), - )) + self.handle_beacon_processor_send_result( + self.network_beacon_processor.send_gossip_sync_signature( + message_id, + peer_id, + sync_committtee_msg.1, + sync_committtee_msg.0, + timestamp_now(), + ), + ) } PubsubMessage::LightClientFinalityUpdate(light_client_finality_update) => { trace!( @@ -330,13 +351,14 @@ impl Router { "Received light client finality update"; "peer_id" => %peer_id ); - self.send_beacon_processor_work( - BeaconWorkEvent::gossip_light_client_finality_update( - message_id, - peer_id, - light_client_finality_update, - timestamp_now(), - ), + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_gossip_light_client_finality_update( + message_id, + peer_id, + *light_client_finality_update, + timestamp_now(), + ), ) } PubsubMessage::LightClientOptimisticUpdate(light_client_optimistic_update) => { @@ -345,21 +367,25 @@ impl Router { "Received light client optimistic update"; "peer_id" => %peer_id ); - self.send_beacon_processor_work( - BeaconWorkEvent::gossip_light_client_optimistic_update( - message_id, - peer_id, - light_client_optimistic_update, - timestamp_now(), - ), + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_gossip_light_client_optimistic_update( + message_id, + peer_id, + *light_client_optimistic_update, + timestamp_now(), + ), ) } PubsubMessage::BlsToExecutionChange(bls_to_execution_change) => self - .send_beacon_processor_work(BeaconWorkEvent::gossip_bls_to_execution_change( - message_id, - peer_id, - bls_to_execution_change, - )), + .handle_beacon_processor_send_result( + self.network_beacon_processor + .send_gossip_bls_to_execution_change( + message_id, + peer_id, + bls_to_execution_change, + ), + ), } } @@ -410,7 +436,10 @@ impl Router { request_id, ); - self.send_beacon_processor_work(BeaconWorkEvent::status_message(peer_id, status)) + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_status_message(peer_id, status), + ) } /// Handle a `BlocksByRange` response from the peer. @@ -475,17 +504,22 @@ impl Router { }); } - fn send_beacon_processor_work(&mut self, work: BeaconWorkEvent) { - self.beacon_processor_send - .try_send(work) - .unwrap_or_else(|e| { - let work_type = match &e { - mpsc::error::TrySendError::Closed(work) - | mpsc::error::TrySendError::Full(work) => work.work_type(), - }; + fn handle_beacon_processor_send_result( + &mut self, + result: Result<(), crate::network_beacon_processor::Error>, + ) { + if let Err(e) = result { + let work_type = match &e { + mpsc::error::TrySendError::Closed(work) | mpsc::error::TrySendError::Full(work) => { + work.work_type() + } + }; + + if self.logger_debounce.elapsed() { error!(&self.log, "Unable to send message to the beacon processor"; "error" => %e, "type" => work_type) - }) + } + } } } diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index edc1d5c2e..174a0ec14 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,5 +1,6 @@ use super::sync::manager::RequestId as SyncId; -use crate::beacon_processor::InvalidBlockStorage; +use crate::nat::EstablishedUPnPMappings; +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::SyncCommitteeService; @@ -9,6 +10,7 @@ use crate::{ NetworkConfig, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; use futures::channel::mpsc::Sender; use futures::future::OptionFuture; use futures::prelude::*; @@ -25,7 +27,7 @@ use lighthouse_network::{ MessageId, NetworkEvent, NetworkGlobals, PeerId, }; use slog::{crit, debug, error, info, o, trace, warn}; -use std::{collections::HashSet, net::SocketAddr, pin::Pin, sync::Arc, time::Duration}; +use std::{collections::HashSet, pin::Pin, sync::Arc, time::Duration}; use store::HotColdDB; use strum::IntoStaticStr; use task_executor::ShutdownReason; @@ -92,12 +94,10 @@ pub enum NetworkMessage { /// The result of the validation validation_result: MessageAcceptance, }, - /// Called if a known external TCP socket address has been updated. + /// Called if UPnP managed to establish an external port mapping. UPnPMappingEstablished { - /// The external TCP address has been updated. - tcp_socket: Option, - /// The external UDP address has been updated. - udp_socket: Option, + /// The mappings that were established. + mappings: EstablishedUPnPMappings, }, /// Reports a peer to the peer manager for performing an action. ReportPeer { @@ -189,11 +189,8 @@ pub struct NetworkService { /// A collection of global variables, accessible outside of the network service. network_globals: Arc>, /// Stores potentially created UPnP mappings to be removed on shutdown. (TCP port and UDP - /// port). - upnp_mappings: (Option, Option), - /// Keeps track of if discovery is auto-updating or not. This is used to inform us if we should - /// update the UDP socket of discovery if the UPnP mappings get established. - discovery_auto_update: bool, + /// ports). + upnp_mappings: EstablishedUPnPMappings, /// 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. @@ -224,11 +221,19 @@ impl NetworkService { config: &NetworkConfig, executor: task_executor::TaskExecutor, gossipsub_registry: Option<&'_ mut Registry>, + beacon_processor_send: BeaconProcessorSend, + beacon_processor_reprocess_tx: mpsc::Sender, ) -> error::Result<(Arc>, NetworkSenders)> { let network_log = executor.log().clone(); // build the channels for external comms let (network_senders, network_recievers) = NetworkSenders::new(); + #[cfg(feature = "disable-backfill")] + warn!( + network_log, + "Backfill is disabled. DO NOT RUN IN PRODUCTION" + ); + // try and construct UPnP port mappings if required. if let Some(upnp_config) = crate::nat::UPnPConfig::from_config(config) { let upnp_log = network_log.new(o!("service" => "UPnP")); @@ -311,14 +316,15 @@ impl NetworkService { network_senders.network_send(), executor.clone(), invalid_block_storage, + beacon_processor_send, + beacon_processor_reprocess_tx, network_log.clone(), )?; // attestation subnet service let attestation_service = AttestationService::new( beacon_chain.clone(), - #[cfg(feature = "deterministic_long_lived_attnets")] - network_globals.local_enr().node_id().raw().into(), + network_globals.local_enr().node_id(), config, &network_log, ); @@ -349,8 +355,7 @@ impl NetworkService { router_send, store, network_globals: network_globals.clone(), - upnp_mappings: (None, None), - discovery_auto_update: config.discv5_config.enr_update, + upnp_mappings: EstablishedUPnPMappings::default(), next_fork_update, next_fork_subscriptions, next_unsubscribe, @@ -483,10 +488,8 @@ impl NetworkService { NetworkEvent::PeerConnectedOutgoing(peer_id) => { self.send_to_router(RouterMessage::StatusPeer(peer_id)); } - NetworkEvent::PeerConnectedIncoming(_) - | NetworkEvent::PeerBanned(_) - | NetworkEvent::PeerUnbanned(_) => { - // No action required for these events. + NetworkEvent::PeerConnectedIncoming(_) => { + // No action required for this event. } NetworkEvent::PeerDisconnected(peer_id) => { self.send_to_router(RouterMessage::PeerDisconnected(peer_id)); @@ -608,32 +611,18 @@ impl NetworkService { } => { self.libp2p.send_error_reponse(peer_id, id, error, reason); } - NetworkMessage::UPnPMappingEstablished { - tcp_socket, - udp_socket, - } => { - self.upnp_mappings = (tcp_socket.map(|s| s.port()), udp_socket.map(|s| s.port())); + NetworkMessage::UPnPMappingEstablished { mappings } => { + self.upnp_mappings = mappings; // If there is an external TCP port update, modify our local ENR. - if let Some(tcp_socket) = tcp_socket { - if let Err(e) = self - .libp2p - .discovery_mut() - .update_enr_tcp_port(tcp_socket.port()) - { + if let Some(tcp_port) = self.upnp_mappings.tcp_port { + if let Err(e) = self.libp2p.discovery_mut().update_enr_tcp_port(tcp_port) { warn!(self.log, "Failed to update ENR"; "error" => e); } } - // if the discovery service is not auto-updating, update it with the - // UPnP mappings - if !self.discovery_auto_update { - if let Some(udp_socket) = udp_socket { - if let Err(e) = self - .libp2p - .discovery_mut() - .update_enr_udp_socket(udp_socket) - { - warn!(self.log, "Failed to update ENR"; "error" => e); - } + // If there is an external QUIC port update, modify our local ENR. + if let Some(quic_port) = self.upnp_mappings.udp_quic_port { + if let Err(e) = self.libp2p.discovery_mut().update_enr_quic_port(quic_port) { + warn!(self.log, "Failed to update ENR"; "error" => e); } } } @@ -986,7 +975,7 @@ impl Drop for NetworkService { } // attempt to remove port mappings - crate::nat::remove_mappings(self.upnp_mappings.0, self.upnp_mappings.1, &self.log); + crate::nat::remove_mappings(&self.upnp_mappings, &self.log); info!(self.log, "Network service shutdown"); } diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index 83fcc8c9a..23bcf456d 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -4,6 +4,7 @@ mod tests { use crate::persisted_dht::load_dht; use crate::{NetworkConfig, NetworkService}; use beacon_chain::test_utils::BeaconChainHarness; + use beacon_processor::BeaconProcessorChannels; use lighthouse_network::Enr; use slog::{o, Drain, Level, Logger}; use sloggers::{null::NullLoggerBuilder, Build}; @@ -59,7 +60,7 @@ mod tests { ); let mut config = NetworkConfig::default(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212); + config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); config.discv5_config.table_filter = |_| true; // Do not ignore local IPs config.upnp_enabled = false; config.boot_nodes_enr = enrs.clone(); @@ -67,10 +68,23 @@ mod tests { // Create a new network service which implicitly gets dropped at the // end of the block. - let _network_service = - NetworkService::start(beacon_chain.clone(), &config, executor, None) - .await - .unwrap(); + 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( + beacon_chain.clone(), + &config, + executor, + None, + beacon_processor_tx, + work_reprocessing_tx, + ) + .await + .unwrap(); drop(signal); }); diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs index e46a52cfb..1cae6299e 100644 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ b/beacon_node/network/src/subnet_service/attestation_subnets.rs @@ -3,7 +3,6 @@ //! determines whether attestations should be aggregated and/or passed to the beacon node. use super::SubnetServiceMessage; -#[cfg(any(test, feature = "deterministic_long_lived_attnets"))] use std::collections::HashSet; use std::collections::{HashMap, VecDeque}; use std::pin::Pin; @@ -14,10 +13,8 @@ use std::time::Duration; use beacon_chain::{BeaconChain, BeaconChainTypes}; use delay_map::{HashMapDelay, HashSetDelay}; use futures::prelude::*; -use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery}; -#[cfg(not(feature = "deterministic_long_lived_attnets"))] -use rand::seq::SliceRandom; -use slog::{debug, error, o, trace, warn}; +use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; +use slog::{debug, error, info, o, trace, warn}; use slot_clock::SlotClock; use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; @@ -27,10 +24,6 @@ use crate::metrics; /// 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 time (in slots) before a last seen validator is considered absent and we unsubscribe from -/// the random gossip topics that we subscribed to due to the validator connection. -#[cfg(not(feature = "deterministic_long_lived_attnets"))] -const LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS: u32 = 150; /// The fraction of a slot that we subscribe to a subnet before the required slot. /// /// Currently a whole slot ahead. @@ -67,30 +60,23 @@ pub struct AttestationService { /// 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. - #[cfg(feature = "deterministic_long_lived_attnets")] + /// These are required of all beacon nodes. The exact number is determined by the chain + /// specification. long_lived_subscriptions: HashSet, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - long_lived_subscriptions: HashMapDelay, - /// Short lived subscriptions that need to be done in the future. + /// 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>, - /// A collection of seen validators. These dictate how many random subnets we should be - /// subscribed to. As these time out, we unsubscribe for the required random subnets and update - /// our ENR. - /// This is a set of validator indices. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - known_validators: HashSetDelay, - /// The waker for the current thread. waker: Option, @@ -100,16 +86,10 @@ pub struct AttestationService { /// We are always subscribed to all subnets. subscribe_all_subnets: bool, - /// For how many slots we subscribe to long lived subnets. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - long_lived_subnet_subscription_slots: u64, - /// Our Discv5 node_id. - #[cfg(feature = "deterministic_long_lived_attnets")] - node_id: ethereum_types::U256, + node_id: NodeId, /// Future used to manage subscribing and unsubscribing from long lived subnets. - #[cfg(feature = "deterministic_long_lived_attnets")] next_long_lived_subscription_event: Pin>, /// Whether this node is a block proposer-only node. @@ -122,62 +102,22 @@ pub struct AttestationService { impl AttestationService { /* Public functions */ - #[cfg(not(feature = "deterministic_long_lived_attnets"))] + /// Establish the service based on the passed configuration. pub fn new( beacon_chain: Arc>, + node_id: NodeId, config: &NetworkConfig, log: &slog::Logger, ) -> Self { let log = log.new(o!("service" => "attestation_service")); - // Calculate the random subnet duration from the spec constants. - let spec = &beacon_chain.spec; let slot_duration = beacon_chain.slot_clock.slot_duration(); - let long_lived_subnet_subscription_slots = spec - .epochs_per_random_subnet_subscription - .saturating_mul(T::EthSpec::slots_per_epoch()); - let long_lived_subscription_duration = Duration::from_millis( - slot_duration.as_millis() as u64 * long_lived_subnet_subscription_slots, - ); - // Panics on overflow. Ensure LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS is not too large. - let last_seen_val_timeout = slot_duration - .checked_mul(LAST_SEEN_VALIDATOR_TIMEOUT_SLOTS) - .expect("LAST_SEEN_VALIDATOR_TIMEOUT must not be ridiculously large"); - - let track_validators = !config.import_all_attestations; - let aggregate_validators_on_subnet = - track_validators.then(|| HashSetDelay::new(slot_duration)); - AttestationService { - events: VecDeque::with_capacity(10), - beacon_chain, - short_lived_subscriptions: HashMapDelay::new(slot_duration), - long_lived_subscriptions: HashMapDelay::new(long_lived_subscription_duration), - scheduled_short_lived_subscriptions: HashSetDelay::default(), - aggregate_validators_on_subnet, - known_validators: HashSetDelay::new(last_seen_val_timeout), - waker: None, - discovery_disabled: config.disable_discovery, - proposer_only: config.proposer_only, - subscribe_all_subnets: config.subscribe_all_subnets, - long_lived_subnet_subscription_slots, - log, + if config.subscribe_all_subnets { + slog::info!(log, "Subscribing to all subnets"); + } else { + slog::info!(log, "Deterministic long lived subnets enabled"; "subnets_per_node" => beacon_chain.spec.subnets_per_node, "subscription_duration_in_epochs" => beacon_chain.spec.epochs_per_subnet_subscription); } - } - - #[cfg(feature = "deterministic_long_lived_attnets")] - pub fn new( - beacon_chain: Arc>, - node_id: ethereum_types::U256, - config: &NetworkConfig, - log: &slog::Logger, - ) -> Self { - let log = log.new(o!("service" => "attestation_service")); - - // Calculate the random subnet duration from the spec constants. - let slot_duration = beacon_chain.slot_clock.slot_duration(); - - slog::info!(log, "Deterministic long lived subnets enabled"; "subnets_per_node" => beacon_chain.spec.subnets_per_node); let track_validators = !config.import_all_attestations; let aggregate_validators_on_subnet = @@ -198,9 +138,15 @@ impl AttestationService { // value with a smarter timing Box::pin(tokio::time::sleep(Duration::from_secs(1))) }, + proposer_only: config.proposer_only, log, }; - service.recompute_long_lived_subnets(); + + // 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 } @@ -210,20 +156,12 @@ impl AttestationService { if self.subscribe_all_subnets { self.beacon_chain.spec.attestation_subnet_count as usize } else { - #[cfg(feature = "deterministic_long_lived_attnets")] let count = self .short_lived_subscriptions .keys() .chain(self.long_lived_subscriptions.iter()) .collect::>() .len(); - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - let count = self - .short_lived_subscriptions - .keys() - .chain(self.long_lived_subscriptions.keys()) - .collect::>() - .len(); count } } @@ -236,20 +174,20 @@ impl AttestationService { subscription_kind: SubscriptionKind, ) -> bool { match subscription_kind { - #[cfg(feature = "deterministic_long_lived_attnets")] SubscriptionKind::LongLived => self.long_lived_subscriptions.contains(subnet_id), - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::LongLived => self.long_lived_subscriptions.contains_key(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. - /// - Subscribe to the required number of random subnets. - /// - Update the local ENR for new random subnets due to seeing new validators. /// - Search for peers for required subnets. /// - Request subscriptions for subnets on specific slots when required. /// - Build the timeouts for each of these events. @@ -267,18 +205,17 @@ impl AttestationService { // 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); - // Registers the validator with the attestation service. - // This will subscribe to long-lived random subnets if required. trace!(self.log, "Validator subscription"; "subscription" => ?subscription, ); - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - self.add_known_validator(subscription.validator_index); + // Compute the subnet that is associated with this subscription let subnet_id = match SubnetId::compute_subnet::( subscription.slot, subscription.attestation_committee_index, @@ -316,7 +253,7 @@ impl AttestationService { if subscription.is_aggregator { metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); - if let Err(e) = self.subscribe_to_subnet(exact_subnet) { + if let Err(e) = self.subscribe_to_short_lived_subnet(exact_subnet) { warn!(self.log, "Subscription to subnet error"; "error" => e, @@ -347,14 +284,13 @@ impl AttestationService { Ok(()) } - #[cfg(feature = "deterministic_long_lived_attnets")] 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!(self.log, "Recomputing deterministic long lived attnets"); + debug!(self.log, "Recomputing deterministic long lived subnets"); self.next_long_lived_subscription_event = Box::pin(tokio::time::sleep(next_subscription_event)); @@ -365,14 +301,20 @@ impl AttestationService { /// Gets the long lived subnets the node should be subscribed to during the current epoch and /// the remaining duration for which they remain valid. - #[cfg(feature = "deterministic_long_lived_attnets")] fn recompute_long_lived_subnets_inner(&mut self) -> Result { - let current_epoch = self.beacon_chain.epoch().map_err( - |e| error!(self.log, "Failed to get the current epoch from clock"; "err" => ?e), - )?; + let current_epoch = self.beacon_chain.epoch().map_err(|e| { + if !self + .beacon_chain + .slot_clock + .is_prior_to_genesis() + .unwrap_or(false) + { + error!(self.log, "Failed to get the current epoch from clock"; "err" => ?e) + } + })?; let (subnets, next_subscription_epoch) = SubnetId::compute_subnets_for_epoch::( - self.node_id, + self.node_id.raw().into(), current_epoch, &self.beacon_chain.spec, ) @@ -396,17 +338,12 @@ impl AttestationService { Ok(next_subscription_event) } - #[cfg(all(test, feature = "deterministic_long_lived_attnets"))] - pub fn update_long_lived_subnets_testing(&mut self, subnets: HashSet) { - self.update_long_lived_subnets(subnets) - } - /// Updates the long lived subnets. /// /// New subnets are registered as subscribed, removed subnets as unsubscribed and the Enr /// updated accordingly. - #[cfg(feature = "deterministic_long_lived_attnets")] fn update_long_lived_subnets(&mut self, mut subnets: HashSet) { + info!(self.log, "Subscribing to long-lived subnets"; "subnets" => ?subnets.iter().collect::>()); for subnet in &subnets { // Add the events for those subnets that are new as long lived subscriptions. if !self.long_lived_subscriptions.contains(subnet) { @@ -430,28 +367,15 @@ impl AttestationService { } } - // Check for subnets that are being removed + // 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) { - if !self.short_lived_subscriptions.contains_key(&subnet) { - debug!(self.log, "Unsubscribing from subnet"; "subnet" => ?subnet, "subscription_kind" => ?SubscriptionKind::LongLived); - self.queue_event(SubnetServiceMessage::Unsubscribe(Subnet::Attestation( - subnet, - ))); - } - - self.queue_event(SubnetServiceMessage::EnrRemove(Subnet::Attestation(subnet))); + self.handle_removed_subnet(subnet, SubscriptionKind::LongLived); } } } - /// Overwrites the long lived subscriptions for testing. - #[cfg(all(test, feature = "deterministic_long_lived_attnets"))] - pub fn set_long_lived_subscriptions(&mut self, subnets: HashSet) { - self.long_lived_subscriptions = subnets - } - /// 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( @@ -535,7 +459,7 @@ impl AttestationService { } // Subscribes to the subnet if it should be done immediately, or schedules it if required. - fn subscribe_to_subnet( + fn subscribe_to_short_lived_subnet( &mut self, ExactSubnet { subnet_id, slot }: ExactSubnet, ) -> Result<(), &'static str> { @@ -564,12 +488,7 @@ impl AttestationService { // immediately. if time_to_subscription_start.is_zero() { // This is a current or past slot, we subscribe immediately. - self.subscribe_to_subnet_immediately( - subnet_id, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived, - slot + 1, - )?; + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1)?; } else { // This is a future slot, schedule subscribing. trace!(self.log, "Scheduling subnet subscription"; "subnet" => ?subnet_id, "time_to_subscription_start" => ?time_to_subscription_start); @@ -580,79 +499,6 @@ impl AttestationService { Ok(()) } - /// Updates the `known_validators` mapping and subscribes to long lived subnets if required. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn add_known_validator(&mut self, validator_index: u64) { - let previously_known = self.known_validators.contains_key(&validator_index); - // Add the new validator or update the current timeout for a known validator. - self.known_validators.insert(validator_index); - if !previously_known { - // New validator has subscribed. - // Subscribe to random topics and update the ENR if needed. - self.subscribe_to_random_subnets(); - } - } - - /// Subscribe to long-lived random subnets and update the local ENR bitfield. - /// The number of subnets to subscribe depends on the number of active validators and number of - /// current subscriptions. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn subscribe_to_random_subnets(&mut self) { - if self.subscribe_all_subnets { - // This case is not handled by this service. - return; - } - - let max_subnets = self.beacon_chain.spec.attestation_subnet_count; - // Calculate how many subnets we need, - let required_long_lived_subnets = { - let subnets_for_validators = self - .known_validators - .len() - .saturating_mul(self.beacon_chain.spec.random_subnets_per_validator as usize); - subnets_for_validators // How many subnets we need - .min(max_subnets as usize) // Capped by the max - .saturating_sub(self.long_lived_subscriptions.len()) // Minus those we have - }; - - if required_long_lived_subnets == 0 { - // Nothing to do. - return; - } - - // Build a list of the subnets that we are not currently advertising. - let available_subnets = (0..max_subnets) - .map(SubnetId::new) - .filter(|subnet_id| !self.long_lived_subscriptions.contains_key(subnet_id)) - .collect::>(); - - let subnets_to_subscribe: Vec<_> = available_subnets - .choose_multiple(&mut rand::thread_rng(), required_long_lived_subnets) - .cloned() - .collect(); - - // Calculate in which slot does this subscription end. - let end_slot = match self.beacon_chain.slot_clock.now() { - Some(slot) => slot + self.long_lived_subnet_subscription_slots, - None => { - return debug!( - self.log, - "Failed to calculate end slot of long lived subnet subscriptions." - ) - } - }; - - for subnet_id in &subnets_to_subscribe { - if let Err(e) = self.subscribe_to_subnet_immediately( - *subnet_id, - SubscriptionKind::LongLived, - end_slot, - ) { - debug!(self.log, "Failed to subscribe to long lived subnet"; "subnet" => ?subnet_id, "err" => e); - } - } - } - /* A collection of functions that handle the various timeouts */ /// Registers a subnet as subscribed. @@ -662,11 +508,9 @@ impl AttestationService { /// out the appropriate events. /// /// On determinist long lived subnets, this is only used for short lived subscriptions. - fn subscribe_to_subnet_immediately( + fn subscribe_to_short_lived_subnet_immediately( &mut self, subnet_id: SubnetId, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - subscription_kind: SubscriptionKind, end_slot: Slot, ) -> Result<(), &'static str> { if self.subscribe_all_subnets { @@ -685,25 +529,12 @@ impl AttestationService { return Err("Time when subscription would end has already passed."); } - #[cfg(feature = "deterministic_long_lived_attnets")] 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. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - let (subscriptions, already_subscribed_as_other_kind) = match subscription_kind { - SubscriptionKind::ShortLived => ( - &mut self.short_lived_subscriptions, - self.long_lived_subscriptions.contains_key(&subnet_id), - ), - SubscriptionKind::LongLived => ( - &mut self.long_lived_subscriptions, - self.short_lived_subscriptions.contains_key(&subnet_id), - ), - }; - #[cfg(feature = "deterministic_long_lived_attnets")] let (subscriptions, already_subscribed_as_other_kind) = ( &mut self.short_lived_subscriptions, self.long_lived_subscriptions.contains(&subnet_id), @@ -738,57 +569,19 @@ impl AttestationService { subnet_id, ))); } - - // If this is a new long lived subscription, send out the appropriate events. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - if SubscriptionKind::LongLived == subscription_kind { - let subnet = Subnet::Attestation(subnet_id); - // Advertise this subnet in our ENR. - self.long_lived_subscriptions.insert_at( - subnet_id, - end_slot, - time_to_subscription_end, - ); - self.queue_event(SubnetServiceMessage::EnrAdd(subnet)); - - if !self.discovery_disabled { - self.queue_event(SubnetServiceMessage::DiscoverPeers(vec![ - SubnetDiscovery { - subnet, - min_ttl: None, - }, - ])) - } - } } } Ok(()) } - /// A random subnet has expired. - /// - /// This function selects a new subnet to join, or extends the expiry if there are no more - /// available subnets to choose from. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn handle_random_subnet_expiry(&mut self, subnet_id: SubnetId) { - self.handle_removed_subnet(subnet_id, SubscriptionKind::LongLived); - - // Remove the ENR bitfield bit and choose a new random on from the available subnets - // Subscribe to a new random subnet. - self.subscribe_to_random_subnets(); - } - // 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), - #[cfg(feature = "deterministic_long_lived_attnets")] SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains(&subnet_id), - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains_key(&subnet_id), }; if !exists_in_other_subscriptions { @@ -806,48 +599,6 @@ impl AttestationService { ))); } } - - /// A known validator has not sent a subscription in a while. They are considered offline and the - /// beacon node no longer needs to be subscribed to the allocated random subnets. - /// - /// We don't keep track of a specific validator to random subnet, rather the ratio of active - /// validators to random subnets. So when a validator goes offline, we can simply remove the - /// allocated amount of random subnets. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - fn handle_known_validator_expiry(&mut self) { - // Calculate how many subnets should we remove. - let extra_subnet_count = { - let max_subnets = self.beacon_chain.spec.attestation_subnet_count; - let subnets_for_validators = self - .known_validators - .len() - .saturating_mul(self.beacon_chain.spec.random_subnets_per_validator as usize) - .min(max_subnets as usize); - - self.long_lived_subscriptions - .len() - .saturating_sub(subnets_for_validators) - }; - - if extra_subnet_count == 0 { - // Nothing to do - return; - } - - let advertised_subnets = self - .long_lived_subscriptions - .keys() - .cloned() - .collect::>(); - let to_remove_subnets = advertised_subnets - .choose_multiple(&mut rand::thread_rng(), extra_subnet_count) - .cloned(); - - for subnet_id in to_remove_subnets { - self.long_lived_subscriptions.remove(&subnet_id); - self.handle_removed_subnet(subnet_id, SubscriptionKind::LongLived); - } - } } impl Stream for AttestationService { @@ -868,37 +619,34 @@ impl Stream for AttestationService { return Poll::Ready(Some(event)); } - // Process first any known validator expiries, since these affect how many long lived - // subnets we need. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - match self.known_validators.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(_validator_index))) => { - self.handle_known_validator_expiry(); + // 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 => {} } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for random subnet cycles"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - - #[cfg(feature = "deterministic_long_lived_attnets")] - match self.next_long_lived_subscription_event.as_mut().poll(cx) { - Poll::Ready(_) => self.recompute_long_lived_subnets(), - 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_subnet_immediately( - subnet_id, - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - SubscriptionKind::ShortLived, - slot + 1, - ) { + if let Err(e) = + self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1) + { debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet_id, "err" => e); } + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); } Poll::Ready(Some(Err(e))) => { error!(self.log, "Failed to check for scheduled subnet subscriptions"; "error"=> e); @@ -910,6 +658,11 @@ impl Stream for AttestationService { 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!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); @@ -917,18 +670,6 @@ impl Stream for AttestationService { Poll::Ready(None) | Poll::Pending => {} } - // Process any random subnet expiries. - #[cfg(not(feature = "deterministic_long_lived_attnets"))] - match self.long_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { - self.handle_random_subnet_expiry(subnet_id) - } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for random subnet cycles"; "error"=> e); - } - 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) { diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index a407fe1bc..3b8c89a44 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -126,10 +126,7 @@ fn get_attestation_service( AttestationService::new( beacon_chain, - #[cfg(feature = "deterministic_long_lived_attnets")] - lighthouse_network::discv5::enr::NodeId::random() - .raw() - .into(), + lighthouse_network::discv5::enr::NodeId::random(), &config, &log, ) @@ -179,9 +176,6 @@ async fn get_events + Unpin>( mod attestation_service { - #[cfg(feature = "deterministic_long_lived_attnets")] - use std::collections::HashSet; - #[cfg(not(windows))] use crate::subnet_service::attestation_subnets::MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD; @@ -192,8 +186,8 @@ mod attestation_service { attestation_committee_index: CommitteeIndex, slot: Slot, committee_count_at_slot: u64, + is_aggregator: bool, ) -> ValidatorSubscription { - let is_aggregator = true; ValidatorSubscription { validator_index, attestation_committee_index, @@ -203,11 +197,11 @@ mod attestation_service { } } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] fn get_subscriptions( validator_count: u64, slot: Slot, committee_count_at_slot: u64, + is_aggregator: bool, ) -> Vec { (0..validator_count) .map(|validator_index| { @@ -216,6 +210,7 @@ mod attestation_service { validator_index, slot, committee_count_at_slot, + is_aggregator, ) }) .collect() @@ -229,6 +224,7 @@ mod attestation_service { // Keep a low subscription slot so that there are no additional subnet discovery events. let subscription_slot = 0; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions let mut attestation_service = get_attestation_service(None); @@ -243,6 +239,7 @@ mod attestation_service { committee_index, current_slot + Slot::new(subscription_slot), committee_count, + true, )]; // submit the subscriptions @@ -266,16 +263,19 @@ mod attestation_service { // Wait for 1 slot duration to get the unsubscription event let events = get_events( &mut attestation_service, - Some(5), + Some(subnets_per_node * 3 + 2), (MainnetEthSpec::slots_per_epoch() * 3) as u32, ) .await; matches::assert_matches!( - events[..3], + events[..6], [ SubnetServiceMessage::Subscribe(_any1), SubnetServiceMessage::EnrAdd(_any3), SubnetServiceMessage::DiscoverPeers(_), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), ] ); @@ -284,10 +284,10 @@ mod attestation_service { if !attestation_service .is_subscribed(&subnet_id, attestation_subnets::SubscriptionKind::LongLived) { - assert_eq!(expected[..], events[3..]); + assert_eq!(expected[..], events[subnets_per_node * 3..]); } - // Should be subscribed to only 1 long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), 1); + // Should be subscribed to only subnets_per_node long lived subnet after unsubscription. + assert_eq!(attestation_service.subscription_count(), subnets_per_node); } /// Test to verify that we are not unsubscribing to a subnet before a required subscription. @@ -297,6 +297,7 @@ mod attestation_service { // subscription config let validator_index = 1; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -318,6 +319,7 @@ mod attestation_service { com1, current_slot + Slot::new(subscription_slot1), committee_count, + true, ); let sub2 = get_subscription( @@ -325,6 +327,7 @@ mod attestation_service { com2, current_slot + Slot::new(subscription_slot2), committee_count, + true, ); let subnet_id1 = SubnetId::compute_subnet::( @@ -366,16 +369,22 @@ mod attestation_service { let expected = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); - // Should be still subscribed to 1 long lived and 1 short lived subnet if both are + // Should be still subscribed to 2 long lived and up to 1 short lived subnet if both are // different. if !attestation_service.is_subscribed( &subnet_id1, attestation_subnets::SubscriptionKind::LongLived, ) { - assert_eq!(expected, events[3]); - assert_eq!(attestation_service.subscription_count(), 2); + // The index is 3*subnets_per_node (because we subscribe + discover + enr per long lived + // subnet) + 1 + let index = 3 * subnets_per_node; + assert_eq!(expected, events[index]); + assert_eq!( + attestation_service.subscription_count(), + subnets_per_node + 1 + ); } else { - assert_eq!(attestation_service.subscription_count(), 1); + assert!(attestation_service.subscription_count() == subnets_per_node); } // Get event for 1 more slot duration, we should get the unsubscribe event now. @@ -395,17 +404,17 @@ mod attestation_service { ); } - // Should be subscribed to only 1 long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), 1); + // Should be subscribed 2 long lived subnet after unsubscription. + assert_eq!(attestation_service.subscription_count(), subnets_per_node); } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] #[tokio::test] - async fn subscribe_all_random_subnets() { + async fn subscribe_all_subnets() { let attestation_subnet_count = MainnetEthSpec::default_spec().attestation_subnet_count; - let subscription_slot = 10; + let subscription_slot = 3; let subscription_count = attestation_subnet_count; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions let mut attestation_service = get_attestation_service(None); @@ -419,6 +428,7 @@ mod attestation_service { subscription_count, current_slot + subscription_slot, committee_count, + true, ); // submit the subscriptions @@ -426,42 +436,52 @@ mod attestation_service { .validator_subscriptions(subscriptions) .unwrap(); - let events = get_events(&mut attestation_service, None, 3).await; + let events = get_events(&mut attestation_service, Some(131), 10).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; let mut unexpected_msg_count = 0; + let mut unsubscribe_event_count = 0; for event in &events { match event { SubnetServiceMessage::DiscoverPeers(_) => discover_peer_count += 1, SubnetServiceMessage::Subscribe(_any_subnet) => {} SubnetServiceMessage::EnrAdd(_any_subnet) => enr_add_count += 1, + SubnetServiceMessage::Unsubscribe(_) => unsubscribe_event_count += 1, _ => unexpected_msg_count += 1, } } + // There should be a Subscribe Event, and Enr Add event and a DiscoverPeers event for each + // long-lived subnet initially. The next event should be a bulk discovery event. + let bulk_discovery_index = 3 * subnets_per_node; // The bulk discovery request length should be equal to validator_count - let bulk_discovery_event = events.last().unwrap(); + let bulk_discovery_event = &events[bulk_discovery_index]; if let SubnetServiceMessage::DiscoverPeers(d) = bulk_discovery_event { assert_eq!(d.len(), attestation_subnet_count as usize); } else { panic!("Unexpected event {:?}", bulk_discovery_event); } - // 64 `DiscoverPeer` requests of length 1 corresponding to random subnets + // 64 `DiscoverPeer` requests of length 1 corresponding to deterministic subnets // and 1 `DiscoverPeer` request corresponding to bulk subnet discovery. - assert_eq!(discover_peer_count, subscription_count + 1); - assert_eq!(attestation_service.subscription_count(), 64); - assert_eq!(enr_add_count, 64); + assert_eq!(discover_peer_count, subnets_per_node + 1); + assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(enr_add_count, subnets_per_node); + assert_eq!( + unsubscribe_event_count, + attestation_subnet_count - subnets_per_node as u64 + ); assert_eq!(unexpected_msg_count, 0); // test completed successfully } - #[cfg(not(feature = "deterministic_long_lived_attnets"))] #[tokio::test] - async fn subscribe_all_random_subnets_plus_one() { + async fn subscribe_correct_number_of_subnets() { let attestation_subnet_count = MainnetEthSpec::default_spec().attestation_subnet_count; let subscription_slot = 10; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; + // the 65th subscription should result in no more messages than the previous scenario let subscription_count = attestation_subnet_count + 1; let committee_count = 1; @@ -478,6 +498,7 @@ mod attestation_service { subscription_count, current_slot + subscription_slot, committee_count, + true, ); // submit the subscriptions @@ -506,12 +527,12 @@ mod attestation_service { } else { panic!("Unexpected event {:?}", bulk_discovery_event); } - // 64 `DiscoverPeer` requests of length 1 corresponding to random subnets + // subnets_per_node `DiscoverPeer` requests of length 1 corresponding to long-lived subnets // and 1 `DiscoverPeer` request corresponding to the bulk subnet discovery. - // For the 65th subscription, the call to `subscribe_to_random_subnets` is not made because we are at capacity. - assert_eq!(discover_peer_count, 64 + 1); - assert_eq!(attestation_service.subscription_count(), 64); - assert_eq!(enr_add_count, 64); + + assert_eq!(discover_peer_count, subnets_per_node + 1); + assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(enr_add_count, subnets_per_node); assert_eq!(unexpected_msg_count, 0); } @@ -521,6 +542,7 @@ mod attestation_service { // subscription config let validator_index = 1; let committee_count = 1; + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -542,6 +564,7 @@ mod attestation_service { com1, current_slot + Slot::new(subscription_slot1), committee_count, + true, ); let sub2 = get_subscription( @@ -549,6 +572,7 @@ mod attestation_service { com2, current_slot + Slot::new(subscription_slot2), committee_count, + true, ); let subnet_id1 = SubnetId::compute_subnet::( @@ -596,11 +620,10 @@ mod attestation_service { &subnet_id1, attestation_subnets::SubscriptionKind::LongLived, ) { - assert_eq!(expected_subscription, events[3]); - // fourth is a discovery event - assert_eq!(expected_unsubscription, events[5]); + assert_eq!(expected_subscription, events[subnets_per_node * 3]); + assert_eq!(expected_unsubscription, events[subnets_per_node * 3 + 2]); } - assert_eq!(attestation_service.subscription_count(), 1); + assert_eq!(attestation_service.subscription_count(), 2); println!("{events:?}"); let subscription_slot = current_slot + subscription_slot2 - 1; // one less do to the @@ -633,40 +656,44 @@ mod attestation_service { } #[tokio::test] - #[cfg(feature = "deterministic_long_lived_attnets")] async fn test_update_deterministic_long_lived_subnets() { let mut attestation_service = get_attestation_service(None); - let new_subnet = SubnetId::new(1); - let maintained_subnet = SubnetId::new(2); - let removed_subnet = SubnetId::new(3); + let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; + let current_slot = attestation_service + .beacon_chain + .slot_clock + .now() + .expect("Could not get current slot"); + + let subscriptions = get_subscriptions(20, current_slot, 30, false); + + // submit the subscriptions attestation_service - .set_long_lived_subscriptions(HashSet::from([removed_subnet, maintained_subnet])); - // clear initial events - let _events = get_events(&mut attestation_service, None, 1).await; + .validator_subscriptions(subscriptions) + .unwrap(); - attestation_service - .update_long_lived_subnets_testing(HashSet::from([maintained_subnet, new_subnet])); - - let events = get_events(&mut attestation_service, None, 1).await; - let new_subnet = Subnet::Attestation(new_subnet); - let removed_subnet = Subnet::Attestation(removed_subnet); + // There should only be the same subscriptions as there are in the specification, + // regardless of subscriptions assert_eq!( - events, + attestation_service.long_lived_subscriptions().len(), + subnets_per_node + ); + + let events = get_events(&mut attestation_service, None, 4).await; + + // Check that we attempt to subscribe and register ENRs + matches::assert_matches!( + events[..6], [ - // events for the new subnet - SubnetServiceMessage::Subscribe(new_subnet), - SubnetServiceMessage::EnrAdd(new_subnet), - SubnetServiceMessage::DiscoverPeers(vec![SubnetDiscovery { - subnet: new_subnet, - min_ttl: None - }]), - // events for the removed subnet - SubnetServiceMessage::Unsubscribe(removed_subnet), - SubnetServiceMessage::EnrRemove(removed_subnet), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), + SubnetServiceMessage::Subscribe(_), + SubnetServiceMessage::EnrAdd(_), + SubnetServiceMessage::DiscoverPeers(_), ] ); - println!("{events:?}") } } diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 460c8b1ee..a1c2404e5 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -8,7 +8,7 @@ //! If a batch fails, the backfill sync cannot progress. In this scenario, we mark the backfill //! sync as failed, log an error and attempt to retry once a new peer joins the node. -use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; +use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::manager::{BatchProcessResult, Id}; use crate::sync::network_context::SyncNetworkContext; use crate::sync::range_sync::{ @@ -537,8 +537,8 @@ impl BackFillSync { self.current_processing_batch = Some(batch_id); if let Err(e) = network - .processor_channel() - .try_send(BeaconWorkEvent::chain_segment(process_id, blocks)) + .beacon_processor() + .send_chain_segment(process_id, blocks) { crit!(self.log, "Failed to send backfill segment to processor."; "msg" => "process_batch", "error" => %e, "batch" => self.processing_target); @@ -1098,12 +1098,7 @@ impl BackFillSync { match self.batches.entry(batch_id) { Entry::Occupied(_) => { // this batch doesn't need downloading, let this same function decide the next batch - if batch_id - == self - .beacon_chain - .genesis_backfill_slot - .epoch(T::EthSpec::slots_per_epoch()) - { + if self.would_complete(batch_id) { self.last_batch_downloaded = true; } @@ -1114,12 +1109,7 @@ impl BackFillSync { } Entry::Vacant(entry) => { entry.insert(BatchInfo::new(&batch_id, BACKFILL_EPOCHS_PER_BATCH)); - if batch_id - == self - .beacon_chain - .genesis_backfill_slot - .epoch(T::EthSpec::slots_per_epoch()) - { + if self.would_complete(batch_id) { self.last_batch_downloaded = true; } self.to_be_downloaded = self @@ -1151,14 +1141,8 @@ impl BackFillSync { /// Checks with the beacon chain if backfill sync has completed. fn check_completed(&mut self) -> bool { - if self.current_start - == self - .beacon_chain - .genesis_backfill_slot - .epoch(T::EthSpec::slots_per_epoch()) - { + if self.would_complete(self.current_start) { // Check that the beacon chain agrees - if let Some(anchor_info) = self.beacon_chain.store.get_anchor_info() { // Conditions that we have completed a backfill sync if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { @@ -1171,6 +1155,15 @@ impl BackFillSync { false } + /// Checks if backfill would complete by syncing to `start_epoch`. + fn would_complete(&self, start_epoch: Epoch) -> bool { + start_epoch + <= self + .beacon_chain + .genesis_backfill_slot + .epoch(T::EthSpec::slots_per_epoch()) + } + /// Updates the global network state indicating the current state of a backfill sync. fn set_state(&self, state: BackFillState) { *self.network_globals.backfill_state.write() = state; diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index aa2694769..4340aa41d 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -2,6 +2,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::time::Duration; +use crate::network_beacon_processor::ChainSegmentProcessId; use beacon_chain::{BeaconChainTypes, BlockError}; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; @@ -11,7 +12,6 @@ use smallvec::SmallVec; use std::sync::Arc; use store::{Hash256, SignedBeaconBlock}; -use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent}; use crate::metrics; use self::parent_lookup::PARENT_FAIL_TOLERANCE; @@ -542,8 +542,8 @@ impl BlockLookups { BlockProcessResult::Ok | BlockProcessResult::Err(BlockError::BlockIsAlreadyKnown { .. }) => { // Check if the beacon processor is available - let beacon_processor_send = match cx.processor_channel_if_enabled() { - Some(channel) => channel, + let beacon_processor = match cx.beacon_processor_if_enabled() { + Some(beacon_processor) => beacon_processor, None => { return trace!( self.log, @@ -555,7 +555,7 @@ impl BlockLookups { let (chain_hash, blocks, hashes, request) = parent_lookup.parts_for_processing(); let process_id = ChainSegmentProcessId::ParentLookup(chain_hash); - match beacon_processor_send.try_send(WorkEvent::chain_segment(process_id, blocks)) { + match beacon_processor.send_chain_segment(process_id, blocks) { Ok(_) => { self.processing_parent_lookups .insert(chain_hash, (hashes, request)); @@ -664,11 +664,15 @@ impl BlockLookups { process_type: BlockProcessType, cx: &mut SyncNetworkContext, ) -> Result<(), ()> { - match cx.processor_channel_if_enabled() { - Some(beacon_processor_send) => { + match cx.beacon_processor_if_enabled() { + Some(beacon_processor) => { trace!(self.log, "Sending block for processing"; "block" => ?block_root, "process" => ?process_type); - let event = WorkEvent::rpc_beacon_block(block_root, block, duration, process_type); - if let Err(e) = beacon_processor_send.try_send(event) { + if let Err(e) = beacon_processor.send_rpc_beacon_block( + block_root, + block, + duration, + process_type, + ) { error!( self.log, "Failed to send sync block to processor"; 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 256a2b429..62ca68e7b 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 @@ -156,9 +156,7 @@ impl SingleBlockRequest { cannot_process: self.failed_processing >= self.failed_downloading, }) } else if let Some(&peer_id) = self.available_peers.iter().choose(&mut rand::thread_rng()) { - let request = BlocksByRootRequest { - block_roots: VariableList::from(vec![self.hash]), - }; + let request = BlocksByRootRequest::new(VariableList::from(vec![self.hash])); self.state = State::Downloading { peer_id }; self.used_peers.insert(peer_id); Ok((peer_id, request)) diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 5a70944f6..c588f867b 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::RequestId; use crate::sync::manager::RequestId as SyncId; use crate::NetworkMessage; @@ -8,18 +9,19 @@ use super::*; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; +use beacon_processor::WorkEvent; use lighthouse_network::{NetworkGlobals, Request}; use slog::{Drain, Level}; -use slot_clock::SystemTimeSlotClock; +use slot_clock::ManualSlotClock; use store::MemoryStore; use tokio::sync::mpsc; use types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; use types::MinimalEthSpec as E; -type T = Witness, E, MemoryStore, MemoryStore>; +type T = Witness, E, MemoryStore, MemoryStore>; struct TestRig { - beacon_processor_rx: mpsc::Receiver>, + beacon_processor_rx: mpsc::Receiver>, network_rx: mpsc::UnboundedReceiver>, rng: XorShiftRng, } @@ -40,8 +42,10 @@ impl TestRig { } }; - let (beacon_processor_tx, beacon_processor_rx) = mpsc::channel(100); let (network_tx, network_rx) = mpsc::unbounded_channel(); + let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); + let (network_beacon_processor, beacon_processor_rx) = + NetworkBeaconProcessor::null_for_testing(globals); let rng = XorShiftRng::from_seed([42; 16]); let rig = TestRig { beacon_processor_rx, @@ -50,11 +54,9 @@ impl TestRig { }; let bl = BlockLookups::new(log.new(slog::o!("component" => "block_lookups"))); let cx = { - let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); SyncNetworkContext::new( network_tx, - globals, - beacon_processor_tx, + Arc::new(network_beacon_processor), log.new(slog::o!("component" => "network_context")), ) }; @@ -101,7 +103,7 @@ impl TestRig { fn expect_block_process(&mut self) { match self.beacon_processor_rx.try_recv() { Ok(work) => { - assert_eq!(work.work_type(), crate::beacon_processor::RPC_BLOCK); + assert_eq!(work.work_type(), beacon_processor::RPC_BLOCK); } other => panic!("Expected block process, found {:?}", other), } @@ -111,7 +113,7 @@ impl TestRig { fn expect_parent_chain_process(&mut self) { match self.beacon_processor_rx.try_recv() { Ok(work) => { - assert_eq!(work.work_type(), crate::beacon_processor::CHAIN_SEGMENT); + assert_eq!(work.work_type(), beacon_processor::CHAIN_SEGMENT); } other => panic!("Expected chain segment process, found {:?}", other), } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 230c883a9..b910f7b33 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -38,7 +38,7 @@ use super::block_lookups::BlockLookups; use super::network_context::SyncNetworkContext; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; -use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; +use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockError, EngineState}; @@ -159,9 +159,6 @@ pub struct SyncManager { /// A reference to the underlying beacon chain. chain: Arc>, - /// A reference to the network globals and peer-db. - network_globals: Arc>, - /// A receiving channel sent by the message processor thread. input_channel: mpsc::UnboundedReceiver>, @@ -186,29 +183,22 @@ pub struct SyncManager { pub fn spawn( executor: task_executor::TaskExecutor, beacon_chain: Arc>, - network_globals: Arc>, network_send: mpsc::UnboundedSender>, - beacon_processor_send: mpsc::Sender>, + beacon_processor: Arc>, + sync_recv: mpsc::UnboundedReceiver>, log: slog::Logger, -) -> mpsc::UnboundedSender> { +) { assert!( MAX_REQUEST_BLOCKS >= 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" ); - // generate the message channel - let (sync_send, sync_recv) = mpsc::unbounded_channel::>(); // create an instance of the SyncManager + let network_globals = beacon_processor.network_globals.clone(); let mut sync_manager = SyncManager { chain: beacon_chain.clone(), - network_globals: network_globals.clone(), input_channel: sync_recv, - network: SyncNetworkContext::new( - network_send, - network_globals.clone(), - beacon_processor_send, - log.clone(), - ), + network: SyncNetworkContext::new(network_send, beacon_processor, log.clone()), range_sync: RangeSync::new(beacon_chain.clone(), log.clone()), backfill_sync: BackFillSync::new(beacon_chain, network_globals, log.clone()), block_lookups: BlockLookups::new(log.clone()), @@ -218,10 +208,13 @@ pub fn spawn( // spawn the sync manager thread debug!(log, "Sync Manager started"); executor.spawn(async move { Box::pin(sync_manager.main()).await }, "sync"); - sync_send } impl SyncManager { + fn network_globals(&self) -> &NetworkGlobals { + self.network.network_globals() + } + /* Input Handling Functions */ /// A peer has connected which has blocks that are unknown to us. @@ -322,12 +315,12 @@ impl SyncManager { let rpr = new_state.as_str(); // Drop the write lock let update_sync_status = self - .network_globals + .network_globals() .peers .write() .update_sync_status(peer_id, new_state.clone()); if let Some(was_updated) = update_sync_status { - let is_connected = self.network_globals.peers.read().is_connected(peer_id); + let is_connected = self.network_globals().peers.read().is_connected(peer_id); if was_updated { debug!( self.log, @@ -383,7 +376,7 @@ impl SyncManager { let head = self.chain.best_slot(); let current_slot = self.chain.slot().unwrap_or_else(|_| Slot::new(0)); - let peers = self.network_globals.peers.read(); + let peers = self.network_globals().peers.read(); if current_slot >= head && current_slot.sub(head) <= (SLOT_IMPORT_TOLERANCE as u64) && head > 0 @@ -402,6 +395,7 @@ impl SyncManager { // If we would otherwise be synced, first check if we need to perform or // complete a backfill sync. + #[cfg(not(feature = "disable-backfill"))] if matches!(sync_state, SyncState::Synced) { // Determine if we need to start/resume/restart a backfill sync. match self.backfill_sync.start(&mut self.network) { @@ -426,6 +420,7 @@ impl SyncManager { } Some((RangeSyncType::Finalized, start_slot, target_slot)) => { // If there is a backfill sync in progress pause it. + #[cfg(not(feature = "disable-backfill"))] self.backfill_sync.pause(); SyncState::SyncingFinalized { @@ -435,6 +430,7 @@ impl SyncManager { } Some((RangeSyncType::Head, start_slot, target_slot)) => { // If there is a backfill sync in progress pause it. + #[cfg(not(feature = "disable-backfill"))] self.backfill_sync.pause(); SyncState::SyncingHead { @@ -445,8 +441,8 @@ impl SyncManager { }, }; - let old_state = self.network_globals.set_sync_state(new_state); - let new_state = self.network_globals.sync_state.read(); + let old_state = self.network_globals().set_sync_state(new_state); + let new_state = self.network_globals().sync_state.read().clone(); if !new_state.eq(&old_state) { info!(self.log, "Sync state updated"; "old_state" => %old_state, "new_state" => %new_state); // If we have become synced - Subscribe to all the core subnet topics @@ -505,7 +501,7 @@ impl SyncManager { } SyncMessage::UnknownBlock(peer_id, block, block_root) => { // If we are not synced or within SLOT_IMPORT_TOLERANCE of the block, ignore - if !self.network_globals.sync_state.read().is_synced() { + if !self.network_globals().sync_state.read().is_synced() { let head_slot = self.chain.canonical_head.cached_head().head_slot(); let unknown_block_slot = block.slot(); @@ -519,7 +515,7 @@ impl SyncManager { return; } } - if self.network_globals.peers.read().is_connected(&peer_id) + if self.network_globals().peers.read().is_connected(&peer_id) && self.network.is_execution_engine_online() { self.block_lookups @@ -528,8 +524,8 @@ impl SyncManager { } SyncMessage::UnknownBlockHash(peer_id, block_hash) => { // If we are not synced, ignore this block. - if self.network_globals.sync_state.read().is_synced() - && self.network_globals.peers.read().is_connected(&peer_id) + if self.network_globals().sync_state.read().is_synced() + && self.network_globals().peers.read().is_connected(&peer_id) && self.network.is_execution_engine_online() { self.block_lookups @@ -556,7 +552,7 @@ impl SyncManager { .parent_block_processed(chain_hash, result, &mut self.network), }, SyncMessage::BatchProcessed { sync_type, result } => match sync_type { - ChainSegmentProcessId::RangeBatchId(chain_id, epoch, _) => { + ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( &mut self.network, chain_id, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c81fed244..adc235130 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -3,7 +3,7 @@ use super::manager::{Id, RequestId as SyncRequestId}; use super::range_sync::{BatchId, ChainId}; -use crate::beacon_processor::WorkEvent; +use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::{NetworkMessage, RequestId}; use crate::status::ToStatusMessage; use beacon_chain::{BeaconChainTypes, EngineState}; @@ -20,9 +20,6 @@ pub struct SyncNetworkContext { /// The network channel to relay messages to the Network service. network_send: mpsc::UnboundedSender>, - /// Access to the network global vars. - network_globals: Arc>, - /// A sequential ID for all RPC requests. request_id: Id, @@ -36,8 +33,8 @@ pub struct SyncNetworkContext { /// `beacon_processor_send`. execution_engine_state: EngineState, - /// Channel to send work to the beacon processor. - beacon_processor_send: mpsc::Sender>, + /// Sends work to the beacon processor via a channel. + network_beacon_processor: Arc>, /// Logger for the `SyncNetworkContext`. log: slog::Logger, @@ -46,25 +43,27 @@ pub struct SyncNetworkContext { impl SyncNetworkContext { pub fn new( network_send: mpsc::UnboundedSender>, - network_globals: Arc>, - beacon_processor_send: mpsc::Sender>, + network_beacon_processor: Arc>, log: slog::Logger, ) -> Self { Self { network_send, execution_engine_state: EngineState::Online, // always assume `Online` at the start - network_globals, request_id: 1, range_requests: FnvHashMap::default(), backfill_requests: FnvHashMap::default(), - beacon_processor_send, + network_beacon_processor, log, } } + pub fn network_globals(&self) -> &NetworkGlobals { + &self.network_beacon_processor.network_globals + } + /// Returns the Client type of the peer if known pub fn client_type(&self, peer_id: &PeerId) -> Client { - self.network_globals + self.network_globals() .peers .read() .peer_info(peer_id) @@ -112,7 +111,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -138,7 +137,7 @@ impl SyncNetworkContext { self.log, "Sending backfill BlocksByRange Request"; "method" => "BlocksByRange", - "count" => request.count, + "count" => request.count(), "peer" => %peer_id, ); let request = Request::BlocksByRange(request); @@ -185,7 +184,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); @@ -209,7 +208,7 @@ impl SyncNetworkContext { self.log, "Sending BlocksByRoot Request"; "method" => "BlocksByRoot", - "count" => request.block_roots.len(), + "count" => request.block_roots().len(), "peer" => %peer_id ); let request = Request::BlocksByRoot(request); @@ -278,13 +277,13 @@ impl SyncNetworkContext { }) } - pub fn processor_channel_if_enabled(&self) -> Option<&mpsc::Sender>> { + pub fn beacon_processor_if_enabled(&self) -> Option<&Arc>> { self.is_execution_engine_online() - .then_some(&self.beacon_processor_send) + .then_some(&self.network_beacon_processor) } - pub fn processor_channel(&self) -> &mpsc::Sender> { - &self.beacon_processor_send + pub fn beacon_processor(&self) -> &Arc> { + &self.network_beacon_processor } fn next_id(&mut self) -> Id { diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 3eee7223d..723ea9b59 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -202,10 +202,10 @@ impl BatchInfo { /// Returns a BlocksByRange request associated with the batch. pub fn to_blocks_by_range_request(&self) -> BlocksByRangeRequest { - BlocksByRangeRequest { - start_slot: self.start_slot.into(), - count: self.end_slot.sub(self.start_slot).into(), - } + BlocksByRangeRequest::new( + self.start_slot.into(), + self.end_slot.sub(self.start_slot).into(), + ) } /// After different operations over a batch, this could be in a state that allows it to diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 4226b600f..af547885d 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,9 +1,9 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; -use crate::beacon_processor::{ChainSegmentProcessId, WorkEvent as BeaconWorkEvent}; +use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::{ manager::Id, network_context::SyncNetworkContext, BatchOperationOutcome, BatchProcessResult, }; -use beacon_chain::{BeaconChainTypes, CountUnrealized}; +use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; use rand::seq::SliceRandom; @@ -101,8 +101,6 @@ pub struct SyncingChain { /// Batches validated by this chain. validated_batches: u64, - is_finalized_segment: bool, - /// The chain's log. log: slog::Logger, } @@ -128,7 +126,6 @@ impl SyncingChain { target_head_slot: Slot, target_head_root: Hash256, peer_id: PeerId, - is_finalized_segment: bool, log: &slog::Logger, ) -> Self { let mut peers = FnvHashMap::default(); @@ -150,7 +147,6 @@ impl SyncingChain { state: ChainSyncingState::Stopped, current_processing_batch: None, validated_batches: 0, - is_finalized_segment, log: log.new(o!("chain" => id)), } } @@ -298,8 +294,8 @@ impl SyncingChain { return Ok(KeepChain); } - let beacon_processor_send = match network.processor_channel_if_enabled() { - Some(channel) => channel, + let beacon_processor = match network.beacon_processor_if_enabled() { + Some(beacon_processor) => beacon_processor, None => return Ok(KeepChain), }; @@ -318,17 +314,10 @@ impl SyncingChain { // for removing chains and checking completion is in the callback. let blocks = batch.start_processing()?; - let count_unrealized = if self.is_finalized_segment { - CountUnrealized::False - } else { - CountUnrealized::True - }; - let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id, count_unrealized); + let process_id = ChainSegmentProcessId::RangeBatchId(self.id, batch_id); self.current_processing_batch = Some(batch_id); - if let Err(e) = - beacon_processor_send.try_send(BeaconWorkEvent::chain_segment(process_id, blocks)) - { + if let Err(e) = beacon_processor.send_chain_segment(process_id, blocks) { crit!(self.log, "Failed to send chain segment to processor."; "msg" => "process_batch", "error" => %e, "batch" => self.processing_target); // This is unlikely to happen but it would stall syncing since the batch now has no 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 37a3f13e7..65ddcefe8 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -465,10 +465,10 @@ impl ChainCollection { network: &mut SyncNetworkContext, ) { let id = SyncingChain::::id(&target_head_root, &target_head_slot); - let (collection, is_finalized) = if let RangeSyncType::Finalized = sync_type { - (&mut self.finalized_chains, true) + let collection = if let RangeSyncType::Finalized = sync_type { + &mut self.finalized_chains } else { - (&mut self.head_chains, false) + &mut self.head_chains }; match collection.entry(id) { Entry::Occupied(mut entry) => { @@ -493,7 +493,6 @@ impl ChainCollection { target_head_slot, target_head_root, peer, - is_finalized, &self.log, ); debug_assert_eq!(new_chain.get_id(), id); diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 0f1c00e50..05ad5204b 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -371,22 +371,23 @@ where #[cfg(test)] mod tests { + use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::RequestId; use crate::NetworkMessage; use super::*; - use crate::beacon_processor::WorkEvent as BeaconWorkEvent; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::parking_lot::RwLock; use beacon_chain::EngineState; + use beacon_processor::WorkEvent as BeaconWorkEvent; use lighthouse_network::rpc::BlocksByRangeRequest; use lighthouse_network::Request; use lighthouse_network::{rpc::StatusMessage, NetworkGlobals}; use slog::{o, Drain}; use tokio::sync::mpsc; - use slot_clock::SystemTimeSlotClock; + use slot_clock::ManualSlotClock; use std::collections::HashSet; use std::sync::Arc; use store::MemoryStore; @@ -437,7 +438,7 @@ mod tests { } type TestBeaconChainType = - Witness, E, MemoryStore, MemoryStore>; + Witness, E, MemoryStore, MemoryStore>; fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { let decorator = slog_term::TermDecorator::new().build(); @@ -455,7 +456,7 @@ mod tests { struct TestRig { log: slog::Logger, /// To check what does sync send to the beacon processor. - beacon_processor_rx: mpsc::Receiver>, + beacon_processor_rx: mpsc::Receiver>, /// To set up different scenarios where sync is told about known/unkown blocks. chain: Arc, /// Needed by range to handle communication with the network. @@ -583,7 +584,7 @@ mod tests { fn expect_chain_segment(&mut self) { match self.beacon_processor_rx.try_recv() { Ok(work) => { - assert_eq!(work.work_type(), crate::beacon_processor::CHAIN_SEGMENT); + assert_eq!(work.work_type(), beacon_processor::CHAIN_SEGMENT); } other => panic!("Expected chain segment process, found {:?}", other), } @@ -593,17 +594,17 @@ mod tests { fn range(log_enabled: bool) -> (TestRig, RangeSync) { let chain = Arc::new(FakeStorage::default()); let log = build_log(slog::Level::Trace, log_enabled); - let (beacon_processor_tx, beacon_processor_rx) = mpsc::channel(10); let range_sync = RangeSync::::new( chain.clone(), log.new(o!("component" => "range")), ); let (network_tx, network_rx) = mpsc::unbounded_channel(); let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); + let (network_beacon_processor, beacon_processor_rx) = + NetworkBeaconProcessor::null_for_testing(globals.clone()); let cx = SyncNetworkContext::new( network_tx, - globals.clone(), - beacon_processor_tx, + Arc::new(network_beacon_processor), log.new(o!("component" => "network_context")), ); let test_rig = TestRig { diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index fdbecb656..afdbd7257 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -2,26 +2,26 @@ name = "operation_pool" version = "0.2.0" authors = ["Michael Sproul "] -edition = "2021" +edition = { workspace = true } [dependencies] -derivative = "2.1.1" -itertools = "0.10.0" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -parking_lot = "0.12.0" -types = { path = "../../consensus/types" } -state_processing = { path = "../../consensus/state_processing" } -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -rayon = "1.5.0" -serde = "1.0.116" +derivative = { workspace = true } +itertools = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +parking_lot = { workspace = true } +types = { workspace = true } +state_processing = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -store = { path = "../store" } -bitvec = "1" -rand = "0.8.5" +store = { workspace = true } +bitvec = { workspace = true } +rand = { workspace = true } [dev-dependencies] -beacon_chain = { path = "../beacon_chain" } -tokio = { version = "1.14.0", features = ["rt-multi-thread"] } -maplit = "1.0.2" +beacon_chain = { workspace = true } +tokio = { workspace = true } +maplit = { workspace = true } diff --git a/beacon_node/operation_pool/src/reward_cache.rs b/beacon_node/operation_pool/src/reward_cache.rs index 5b9d4258e..9e4c424bd 100644 --- a/beacon_node/operation_pool/src/reward_cache.rs +++ b/beacon_node/operation_pool/src/reward_cache.rs @@ -12,7 +12,7 @@ struct Initialization { #[derive(Debug, Clone, Default)] pub struct RewardCache { initialization: Option, - /// `BitVec` of validator indices which don't have default participation flags for the prev. epoch. + /// `BitVec` of validator indices which don't have default participation flags for the prev epoch. /// /// We choose to only track whether validators have *any* participation flag set because /// it's impossible to include a new attestation which is better than the existing participation diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 59a5f4b2e..15f8f5655 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1,5 +1,6 @@ -use clap::{App, Arg}; +use clap::{App, Arg, ArgGroup}; use strum::VariantNames; +use types::ProgressiveBalancesMode; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new("beacon_node") @@ -74,11 +75,11 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("The address lighthouse will listen for UDP and TCP connections. To listen \ over IpV4 and IpV6 set this flag twice with the different values.\n\ Examples:\n\ - - --listen-address '0.0.0.0' will listen over Ipv4.\n\ - - --listen-address '::' will listen over Ipv6.\n\ + - --listen-address '0.0.0.0' will listen over IPv4.\n\ + - --listen-address '::' will listen over IPv6.\n\ - --listen-address '0.0.0.0' --listen-address '::' will listen over both \ - Ipv4 and Ipv6. The order of the given addresses is not relevant. However, \ - multiple Ipv4, or multiple Ipv6 addresses will not be accepted.") + IPv4 and IPv6. The order of the given addresses is not relevant. However, \ + multiple IPv4, or multiple IPv6 addresses will not be accepted.") .multiple(true) .max_values(2) .default_value("0.0.0.0") @@ -88,9 +89,10 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("port") .long("port") .value_name("PORT") - .help("The TCP/UDP port to listen on. The UDP port can be modified by the \ - --discovery-port flag. If listening over both Ipv4 and Ipv6 the --port flag \ - will apply to the Ipv4 address and --port6 to the Ipv6 address.") + .help("The TCP/UDP ports to listen on. There are two UDP ports. \ + The discovery UDP port will be set to this value and the Quic UDP port will be set to this value + 1. The discovery port can be modified by the \ + --discovery-port flag and the quic port can be modified by the --quic-port flag. If listening over both IPv4 and IPv6 the --port flag \ + will apply to the IPv4 address and --port6 to the IPv6 address.") .default_value("9000") .takes_value(true), ) @@ -98,8 +100,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("port6") .long("port6") .value_name("PORT") - .help("The TCP/UDP port to listen on over IpV6 when listening over both Ipv4 and \ - Ipv6. Defaults to 9090 when required.") + .help("The TCP/UDP ports to listen on over IPv6 when listening over both IPv4 and \ + IPv6. Defaults to 9090 when required. The Quic UDP port will be set to this value + 1.") .default_value("9090") .takes_value(true), ) @@ -110,13 +112,27 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("The UDP port that discovery will listen on. Defaults to `port`") .takes_value(true), ) + .arg( + Arg::with_name("quic-port") + .long("quic-port") + .value_name("PORT") + .help("The UDP port that quic will listen on. Defaults to `port` + 1") + .takes_value(true), + ) .arg( Arg::with_name("discovery-port6") .long("discovery-port6") .value_name("PORT") - .help("The UDP port that discovery will listen on over IpV6 if listening over \ - both Ipv4 and IpV6. Defaults to `port6`") - .hidden(true) // TODO: implement dual stack via two sockets in discv5. + .help("The UDP port that discovery will listen on over IPv6 if listening over \ + both IPv4 and IPv6. Defaults to `port6`") + .takes_value(true), + ) + .arg( + Arg::with_name("quic-port6") + .long("quic-port6") + .value_name("PORT") + .help("The UDP port that quic will listen on over IPv6 if listening over \ + both IPv4 and IPv6. Defaults to `port6` + 1") .takes_value(true), ) .arg( @@ -159,7 +175,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("enr-udp-port") .value_name("PORT") .help("The UDP4 port of the local ENR. Set this only if you are sure other nodes \ - can connect to your local node on this port over IpV4.") + can connect to your local node on this port over IPv4.") + .takes_value(true), + ) + .arg( + Arg::with_name("enr-quic-port") + .long("enr-quic-port") + .value_name("PORT") + .help("The quic UDP4 port that will be set on the local ENR. Set this only if you are sure other nodes \ + can connect to your local node on this port over IPv4.") .takes_value(true), ) .arg( @@ -167,7 +191,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("enr-udp6-port") .value_name("PORT") .help("The UDP6 port of the local ENR. Set this only if you are sure other nodes \ - can connect to your local node on this port over IpV6.") + can connect to your local node on this port over IPv6.") + .takes_value(true), + ) + .arg( + Arg::with_name("enr-quic6-port") + .long("enr-quic6-port") + .value_name("PORT") + .help("The quic UDP6 port that will be set on the local ENR. Set this only if you are sure other nodes \ + can connect to your local node on this port over IPv6.") .takes_value(true), ) .arg( @@ -175,7 +207,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("enr-tcp-port") .value_name("PORT") .help("The TCP4 port of the local ENR. Set this only if you are sure other nodes \ - can connect to your local node on this port over IpV4. The --port flag is \ + can connect to your local node on this port over IPv4. The --port flag is \ used if this is not set.") .takes_value(true), ) @@ -184,7 +216,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long("enr-tcp6-port") .value_name("PORT") .help("The TCP6 port of the local ENR. Set this only if you are sure other nodes \ - can connect to your local node on this port over IpV6. The --port6 flag is \ + can connect to your local node on this port over IPv6. The --port6 flag is \ used if this is not set.") .takes_value(true), ) @@ -198,7 +230,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { discovery. Set this only if you are sure other nodes can connect to your \ local node on this address. This will update the `ip4` or `ip6` ENR fields \ accordingly. To update both, set this flag twice with the different values.") - .requires("enr-udp-port") .multiple(true) .max_values(2) .takes_value(true), @@ -226,11 +257,18 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { without an ENR.") .takes_value(true), ) + // NOTE: This is hidden because it is primarily a developer feature for testnets and + // debugging. We remove it from the list to avoid clutter. .arg( Arg::with_name("disable-discovery") .long("disable-discovery") .help("Disables the discv5 discovery protocol. The node will not search for new peers or participate in the discovery protocol.") - .takes_value(false), + .hidden(true) + ) + .arg( + Arg::with_name("disable-quic") + .long("disable-quic") + .help("Disables the quic transport. The node will rely solely on the TCP transport for libp2p connections.") ) .arg( Arg::with_name("disable-peer-scoring") @@ -282,7 +320,23 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { for a beacon node being referenced by validator client using the --proposer-node flag. This configuration is for enabling more secure setups.") .takes_value(false), ) - + .arg( + Arg::with_name("inbound-rate-limiter") + .long("inbound-rate-limiter") + .help( + "Configures the inbound rate limiter (requests received by this node).\ + \ + Rate limit quotas per protocol can be set in the form of \ + :/. To set quotas for multiple protocols, \ + separate them by ';'. If the inbound rate limiter is enabled and a protocol is not \ + present in the configuration, the default quotas will be used. \ + \ + This is enabled by default, using default quotas. To disable rate limiting pass \ + `disabled` to this option instead." + ) + .takes_value(true) + .hidden(true) + ) .arg( Arg::with_name("disable-backfill-rate-limiting") .long("disable-backfill-rate-limiting") @@ -301,22 +355,25 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-address") .long("http-address") + .requires("enable_http") .value_name("ADDRESS") .help("Set the listen address for the RESTful HTTP API server.") - .default_value("127.0.0.1") + .default_value_if("enable_http", None, "127.0.0.1") .takes_value(true), ) .arg( Arg::with_name("http-port") .long("http-port") + .requires("enable_http") .value_name("PORT") .help("Set the listen TCP port for the RESTful HTTP API server.") - .default_value("5052") + .default_value_if("enable_http", None, "5052") .takes_value(true), ) .arg( Arg::with_name("http-allow-origin") .long("http-allow-origin") + .requires("enable_http") .value_name("ORIGIN") .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ Use * to allow any origin (not recommended in production). \ @@ -327,11 +384,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-disable-legacy-spec") .long("http-disable-legacy-spec") + .requires("enable_http") .hidden(true) ) .arg( Arg::with_name("http-spec-fork") .long("http-spec-fork") + .requires("enable_http") .value_name("FORK") .help("Serve the spec for a specific hard fork on /eth/v1/config/spec. It should \ not be necessary to set this flag.") @@ -349,6 +408,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-tls-cert") .long("http-tls-cert") + .requires("enable_http") .help("The path of the certificate to be used when serving the HTTP API server \ over TLS.") .takes_value(true) @@ -356,6 +416,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-tls-key") .long("http-tls-key") + .requires("enable_http") .help("The path of the private key to be used when serving the HTTP API server \ over TLS. Must not be password-protected.") .takes_value(true) @@ -363,10 +424,43 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-allow-sync-stalled") .long("http-allow-sync-stalled") + .requires("enable_http") .help("Forces the HTTP to indicate that the node is synced when sync is actually \ stalled. This is useful for very small testnets. TESTING ONLY. DO NOT USE ON \ MAINNET.") ) + .arg( + Arg::with_name("http-sse-capacity-multiplier") + .long("http-sse-capacity-multiplier") + .requires("enable_http") + .takes_value(true) + .default_value_if("enable_http", None, "1") + .value_name("N") + .help("Multiplier to apply to the length of HTTP server-sent-event (SSE) channels. \ + Increasing this value can prevent messages from being dropped.") + ) + .arg( + Arg::with_name("http-duplicate-block-status") + .long("http-duplicate-block-status") + .requires("enable_http") + .takes_value(true) + .default_value_if("enable_http", None, "202") + .value_name("STATUS_CODE") + .help("Status code to send when a block that is already known is POSTed to the \ + HTTP API.") + ) + .arg( + Arg::with_name("http-enable-beacon-processor") + .long("http-enable-beacon-processor") + .requires("enable_http") + .value_name("BOOLEAN") + .help("The beacon processor is a scheduler which provides quality-of-service and \ + DoS protection. When set to \"true\", HTTP API requests will be queued and scheduled \ + alongside other tasks. When set to \"false\", HTTP API responses will be executed \ + immediately.") + .takes_value(true) + .default_value_if("enable_http", None, "true") + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::with_name("metrics") @@ -378,22 +472,25 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("metrics-address") .long("metrics-address") .value_name("ADDRESS") + .requires("metrics") .help("Set the listen address for the Prometheus metrics HTTP server.") - .default_value("127.0.0.1") + .default_value_if("metrics", None, "127.0.0.1") .takes_value(true), ) .arg( Arg::with_name("metrics-port") .long("metrics-port") + .requires("metrics") .value_name("PORT") .help("Set the listen TCP port for the Prometheus metrics HTTP server.") - .default_value("5054") + .default_value_if("metrics", None, "5054") .takes_value(true), ) .arg( Arg::with_name("metrics-allow-origin") .long("metrics-allow-origin") .value_name("ORIGIN") + .requires("metrics") .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ Use * to allow any origin (not recommended in production). \ If no value is supplied, the CORS allowed origin is set to the listen \ @@ -518,6 +615,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { [default: 8192 (mainnet) or 64 (minimal)]") .takes_value(true) ) + .arg( + Arg::with_name("epochs-per-migration") + .long("epochs-per-migration") + .value_name("N") + .help("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_value("1") + .takes_value(true) + ) .arg( Arg::with_name("block-cache-size") .long("block-cache-size") @@ -671,7 +778,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("max-skip-slots") .long("max-skip-slots") .help( - "Refuse to skip more than this many slots when processing a block or attestation. \ + "Refuse to skip more than this many slots when processing an attestation. \ This prevents nodes on minority forks from wasting our time and disk space, \ but could also cause unnecessary consensus failures, so is disabled by default." ) @@ -776,8 +883,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("slasher-broadcast") .long("slasher-broadcast") .help("Broadcast slashings found by the slasher to the rest of the network \ - [disabled by default].") - .requires("slasher") + [Enabled by default].") + .takes_value(true) + .default_value("true") ) .arg( Arg::with_name("slasher-backend") @@ -832,7 +940,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("Set the timeout for checkpoint sync calls to remote beacon node HTTP endpoint.") .value_name("SECONDS") .takes_value(true) - .default_value("60") + .default_value("180") ) .arg( Arg::with_name("reconstruct-historic-states") @@ -1079,7 +1187,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("gui") .long("gui") - .hidden(true) .help("Enable the graphical user interface and all its requirements. \ This enables --http and --validator-monitor-auto and enables SSE logging.") .takes_value(false) @@ -1102,4 +1209,69 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { developers. This directory is not pruned, users should be careful to avoid \ filling up their disks.") ) + .arg( + Arg::with_name("progressive-balances") + .long("progressive-balances") + .value_name("MODE") + .help("Options to enable or disable the progressive balances cache for \ + unrealized FFG progression calculation. The default `checked` mode compares \ + the progressive balances from the cache against results from the existing \ + method. If there is a mismatch, it falls back to the existing method. The \ + optimized mode (`fast`) is faster but is still experimental, and is \ + not recommended for mainnet usage at this time.") + .takes_value(true) + .possible_values(ProgressiveBalancesMode::VARIANTS) + ) + .arg( + Arg::with_name("beacon-processor-max-workers") + .long("beacon-processor-max-workers") + .value_name("INTEGER") + .help("Specifies the maximum concurrent tasks for the task scheduler. Increasing \ + this value may increase resource consumption. Reducing the value \ + may result in decreased resource usage and diminished performance. The \ + default value is the number of logical CPU cores on the host.") + .takes_value(true) + ) + .arg( + Arg::with_name("beacon-processor-work-queue-len") + .long("beacon-processor-work-queue-len") + .value_name("INTEGER") + .help("Specifies the length of the inbound event queue. \ + Higher values may prevent messages from being dropped while lower values \ + may help protect the node from becoming overwhelmed.") + .default_value("16384") + .takes_value(true) + ) + .arg( + Arg::with_name("beacon-processor-reprocess-queue-len") + .long("beacon-processor-reprocess-queue-len") + .value_name("INTEGER") + .help("Specifies the length of the queue for messages requiring delayed processing. \ + Higher values may prevent messages from being dropped while lower values \ + may help protect the node from becoming overwhelmed.") + .default_value("12288") + .takes_value(true) + ) + .arg( + Arg::with_name("beacon-processor-attestation-batch-size") + .long("beacon-processor-attestation-batch-size") + .value_name("INTEGER") + .help("Specifies the number of gossip attestations in a signature verification batch. \ + Higher values may reduce CPU usage in a healthy network whilst lower values may \ + increase CPU usage in an unhealthy or hostile network.") + .default_value("64") + .takes_value(true) + ) + .arg( + Arg::with_name("beacon-processor-aggregate-batch-size") + .long("beacon-processor-aggregate-batch-size") + .value_name("INTEGER") + .help("Specifies the number of gossip aggregate attestations in a signature \ + verification batch. \ + Higher values may reduce CPU usage in a healthy network while lower values may \ + increase CPU usage in an unhealthy or hostile network.") + .default_value("64") + .takes_value(true) + ) + .group(ArgGroup::with_name("enable_http").args(&["http", "gui", "staking"]).multiple(true)) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 6f626bee8..4ab92a7fd 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -4,6 +4,7 @@ use beacon_chain::chain_config::{ }; use clap::ArgMatches; use clap_utils::flags::DISABLE_MALLOC_TUNING_FLAG; +use clap_utils::parse_required; use client::{ClientConfig, ClientGenesis}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use environment::RuntimeContext; @@ -93,59 +94,68 @@ pub fn get_config( * Http API server */ - if cli_args.is_present("http") { + if cli_args.is_present("enable_http") { client_config.http_api.enabled = true; - } - if let Some(address) = cli_args.value_of("http-address") { - client_config.http_api.listen_addr = address - .parse::() - .map_err(|_| "http-address is not a valid IP address.")?; - } + if let Some(address) = cli_args.value_of("http-address") { + client_config.http_api.listen_addr = address + .parse::() + .map_err(|_| "http-address is not a valid IP address.")?; + } - if let Some(port) = cli_args.value_of("http-port") { - client_config.http_api.listen_port = port - .parse::() - .map_err(|_| "http-port is not a valid u16.")?; - } + if let Some(port) = cli_args.value_of("http-port") { + client_config.http_api.listen_port = port + .parse::() + .map_err(|_| "http-port is not a valid u16.")?; + } - if let Some(allow_origin) = cli_args.value_of("http-allow-origin") { - // Pre-validate the config value to give feedback to the user on node startup, instead of - // as late as when the first API response is produced. - hyper::header::HeaderValue::from_str(allow_origin) - .map_err(|_| "Invalid allow-origin value")?; + if let Some(allow_origin) = cli_args.value_of("http-allow-origin") { + // Pre-validate the config value to give feedback to the user on node startup, instead of + // as late as when the first API response is produced. + hyper::header::HeaderValue::from_str(allow_origin) + .map_err(|_| "Invalid allow-origin value")?; - client_config.http_api.allow_origin = Some(allow_origin.to_string()); - } + client_config.http_api.allow_origin = Some(allow_origin.to_string()); + } - if cli_args.is_present("http-disable-legacy-spec") { - warn!( - log, - "The flag --http-disable-legacy-spec is deprecated and will be removed" - ); - } + if cli_args.is_present("http-disable-legacy-spec") { + warn!( + log, + "The flag --http-disable-legacy-spec is deprecated and will be removed" + ); + } - if let Some(fork_name) = clap_utils::parse_optional(cli_args, "http-spec-fork")? { - client_config.http_api.spec_fork_name = Some(fork_name); - } + if let Some(fork_name) = clap_utils::parse_optional(cli_args, "http-spec-fork")? { + client_config.http_api.spec_fork_name = Some(fork_name); + } - if cli_args.is_present("http-enable-tls") { - client_config.http_api.tls_config = Some(TlsConfig { - cert: cli_args - .value_of("http-tls-cert") - .ok_or("--http-tls-cert was not provided.")? - .parse::() - .map_err(|_| "http-tls-cert is not a valid path name.")?, - key: cli_args - .value_of("http-tls-key") - .ok_or("--http-tls-key was not provided.")? - .parse::() - .map_err(|_| "http-tls-key is not a valid path name.")?, - }); - } + if cli_args.is_present("http-enable-tls") { + client_config.http_api.tls_config = Some(TlsConfig { + cert: cli_args + .value_of("http-tls-cert") + .ok_or("--http-tls-cert was not provided.")? + .parse::() + .map_err(|_| "http-tls-cert is not a valid path name.")?, + key: cli_args + .value_of("http-tls-key") + .ok_or("--http-tls-key was not provided.")? + .parse::() + .map_err(|_| "http-tls-key is not a valid path name.")?, + }); + } - if cli_args.is_present("http-allow-sync-stalled") { - client_config.http_api.allow_sync_stalled = true; + if cli_args.is_present("http-allow-sync-stalled") { + client_config.http_api.allow_sync_stalled = true; + } + + client_config.http_api.sse_capacity_multiplier = + parse_required(cli_args, "http-sse-capacity-multiplier")?; + + client_config.http_api.enable_beacon_processor = + parse_required(cli_args, "http-enable-beacon-processor")?; + + client_config.http_api.duplicate_block_status_code = + parse_required(cli_args, "http-duplicate-block-status")?; } if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? { @@ -344,6 +354,9 @@ pub fn get_config( el_config.default_datadir = client_config.data_dir().clone(); el_config.builder_profit_threshold = clap_utils::parse_required(cli_args, "builder-profit-threshold")?; + el_config.always_prefer_builder_payload = + cli_args.is_present("always-prefer-builder-payload"); + let execution_timeout_multiplier = clap_utils::parse_required(cli_args, "execution-timeout-multiplier")?; el_config.execution_timeout_multiplier = Some(execution_timeout_multiplier); @@ -400,6 +413,12 @@ pub fn get_config( client_config.store.prune_payloads = prune_payloads; } + if let Some(epochs_per_migration) = + clap_utils::parse_optional(cli_args, "epochs-per-migration")? + { + client_config.chain.epochs_per_migration = epochs_per_migration; + } + /* * Zero-ports * @@ -452,9 +471,30 @@ pub fn get_config( client_config.chain.checkpoint_sync_url_timeout = clap_utils::parse_required::(cli_args, "checkpoint-sync-url-timeout")?; - client_config.genesis = if let Some(genesis_state_bytes) = - eth2_network_config.genesis_state_bytes.clone() - { + client_config.genesis_state_url_timeout = + clap_utils::parse_required(cli_args, "genesis-state-url-timeout") + .map(Duration::from_secs)?; + + let genesis_state_url_opt = + clap_utils::parse_optional::(cli_args, "genesis-state-url")?; + let checkpoint_sync_url_opt = + clap_utils::parse_optional::(cli_args, "checkpoint-sync-url")?; + + // If the `--genesis-state-url` is defined, use that to download the + // genesis state bytes. If it's not defined, try `--checkpoint-sync-url`. + client_config.genesis_state_url = if let Some(genesis_state_url) = genesis_state_url_opt { + Some(genesis_state_url) + } else if let Some(checkpoint_sync_url) = checkpoint_sync_url_opt { + // If the checkpoint sync URL is going to be used to download the + // genesis state, adopt the timeout from the checkpoint sync URL too. + client_config.genesis_state_url_timeout = + Duration::from_secs(client_config.chain.checkpoint_sync_url_timeout); + Some(checkpoint_sync_url) + } else { + None + }; + + client_config.genesis = if eth2_network_config.genesis_state_is_known() { // Set up weak subjectivity sync, or start from the hardcoded genesis state. if let (Some(initial_state_path), Some(initial_block_path)) = ( cli_args.value_of("checkpoint-state"), @@ -476,7 +516,6 @@ pub fn get_config( let anchor_block_bytes = read(initial_block_path)?; ClientGenesis::WeakSubjSszBytes { - genesis_state_bytes, anchor_state_bytes, anchor_block_bytes, } @@ -484,17 +523,9 @@ pub fn get_config( let url = SensitiveUrl::parse(remote_bn_url) .map_err(|e| format!("Invalid checkpoint sync URL: {:?}", e))?; - ClientGenesis::CheckpointSyncUrl { - genesis_state_bytes, - url, - } + ClientGenesis::CheckpointSyncUrl { url } } else { - // Note: re-serializing the genesis state is not so efficient, however it avoids adding - // trait bounds to the `ClientGenesis` enum. This would have significant flow-on - // effects. - ClientGenesis::SszBytes { - genesis_state_bytes, - } + ClientGenesis::GenesisState } } else { if cli_args.is_present("checkpoint-state") || cli_args.is_present("checkpoint-sync-url") { @@ -575,8 +606,10 @@ pub fn get_config( }; } - client_config.chain.max_network_size = - lighthouse_network::gossip_max_size(spec.bellatrix_fork_epoch.is_some()); + client_config.chain.max_network_size = lighthouse_network::gossip_max_size( + spec.bellatrix_fork_epoch.is_some(), + spec.gossip_max_size as usize, + ); if cli_args.is_present("slasher") { let slasher_dir = if let Some(slasher_dir) = cli_args.value_of("slasher-dir") { @@ -633,7 +666,9 @@ pub fn get_config( slasher_config.validator_chunk_size = validator_chunk_size; } - slasher_config.broadcast = cli_args.is_present("slasher-broadcast"); + if let Some(broadcast) = clap_utils::parse_optional(cli_args, "slasher-broadcast")? { + slasher_config.broadcast = broadcast; + } if let Some(backend) = clap_utils::parse_optional(cli_args, "slasher-backend")? { slasher_config.backend = backend; @@ -784,13 +819,9 @@ pub fn get_config( if cli_args.is_present("genesis-backfill") { client_config.chain.genesis_backfill = true; } - // Payload selection configs - if cli_args.is_present("always-prefer-builder-payload") { - client_config.always_prefer_builder_payload = true; - } // Backfill sync rate-limiting - client_config.chain.enable_backfill_rate_limiting = + client_config.beacon_processor.enable_backfill_rate_limiting = !cli_args.is_present("disable-backfill-rate-limiting"); if let Some(path) = clap_utils::parse_optional(cli_args, "invalid-gossip-verified-blocks-path")? @@ -798,6 +829,34 @@ pub fn get_config( client_config.network.invalid_block_storage = Some(path); } + if let Some(progressive_balances_mode) = + clap_utils::parse_optional(cli_args, "progressive-balances")? + { + client_config.chain.progressive_balances_mode = progressive_balances_mode; + } + + if let Some(max_workers) = clap_utils::parse_optional(cli_args, "beacon-processor-max-workers")? + { + client_config.beacon_processor.max_workers = max_workers; + } + + if client_config.beacon_processor.max_workers == 0 { + return Err("--beacon-processor-max-workers must be a non-zero value".to_string()); + } + + client_config.beacon_processor.max_work_event_queue_len = + clap_utils::parse_required(cli_args, "beacon-processor-work-queue-len")?; + client_config.beacon_processor.max_scheduled_work_queue_len = + clap_utils::parse_required(cli_args, "beacon-processor-reprocess-queue-len")?; + client_config + .beacon_processor + .max_gossip_attestation_batch_size = + clap_utils::parse_required(cli_args, "beacon-processor-attestation-batch-size")?; + client_config + .beacon_processor + .max_gossip_aggregate_batch_size = + clap_utils::parse_required(cli_args, "beacon-processor-aggregate-batch-size")?; + Ok(client_config) } @@ -855,15 +914,15 @@ pub fn parse_listening_addresses( .map_err(|parse_error| format!("Failed to parse --port6 as an integer: {parse_error}"))? .unwrap_or(9090); - // parse the possible udp ports - let maybe_udp_port = cli_args + // parse the possible discovery ports. + let maybe_disc_port = cli_args .value_of("discovery-port") .map(str::parse::) .transpose() .map_err(|parse_error| { format!("Failed to parse --discovery-port as an integer: {parse_error}") })?; - let maybe_udp6_port = cli_args + let maybe_disc6_port = cli_args .value_of("discovery-port6") .map(str::parse::) .transpose() @@ -871,6 +930,24 @@ pub fn parse_listening_addresses( format!("Failed to parse --discovery-port6 as an integer: {parse_error}") })?; + // parse the possible quic port. + let maybe_quic_port = cli_args + .value_of("quic-port") + .map(str::parse::) + .transpose() + .map_err(|parse_error| { + format!("Failed to parse --quic-port as an integer: {parse_error}") + })?; + + // parse the possible quic port. + let maybe_quic6_port = cli_args + .value_of("quic-port6") + .map(str::parse::) + .transpose() + .map_err(|parse_error| { + format!("Failed to parse --quic6-port as an integer: {parse_error}") + })?; + // Now put everything together let listening_addresses = match (maybe_ipv4, maybe_ipv6) { (None, None) => { @@ -881,7 +958,7 @@ pub fn parse_listening_addresses( // A single ipv6 address was provided. Set the ports if cli_args.is_present("port6") { - warn!(log, "When listening only over IpV6, use the --port flag. The value of --port6 will be ignored.") + warn!(log, "When listening only over IPv6, use the --port flag. The value of --port6 will be ignored.") } // use zero ports if required. If not, use the given port. let tcp_port = use_zero_ports @@ -889,20 +966,32 @@ pub fn parse_listening_addresses( .transpose()? .unwrap_or(port); - if maybe_udp6_port.is_some() { - warn!(log, "When listening only over IpV6, use the --discovery-port flag. The value of --discovery-port6 will be ignored.") + if maybe_disc6_port.is_some() { + warn!(log, "When listening only over IPv6, use the --discovery-port flag. The value of --discovery-port6 will be ignored.") } + + if maybe_quic6_port.is_some() { + warn!(log, "When listening only over IPv6, use the --quic-port flag. The value of --quic-port6 will be ignored.") + } + // use zero ports if required. If not, use the specific udp port. If none given, use // the tcp port. - let udp_port = use_zero_ports + let disc_port = use_zero_ports .then(unused_port::unused_udp6_port) .transpose()? - .or(maybe_udp_port) + .or(maybe_disc_port) .unwrap_or(port); + let quic_port = use_zero_ports + .then(unused_port::unused_udp6_port) + .transpose()? + .or(maybe_quic_port) + .unwrap_or(port + 1); + ListenAddress::V6(lighthouse_network::ListenAddr { addr: ipv6, - udp_port, + quic_port, + disc_port, tcp_port, }) } @@ -914,16 +1003,25 @@ pub fn parse_listening_addresses( .then(unused_port::unused_tcp4_port) .transpose()? .unwrap_or(port); - // use zero ports if required. If not, use the specific udp port. If none given, use + // use zero ports if required. If not, use the specific discovery port. If none given, use // the tcp port. - let udp_port = use_zero_ports + let disc_port = use_zero_ports .then(unused_port::unused_udp4_port) .transpose()? - .or(maybe_udp_port) + .or(maybe_disc_port) .unwrap_or(port); + // use zero ports if required. If not, use the specific quic port. If none given, use + // the tcp port + 1. + let quic_port = use_zero_ports + .then(unused_port::unused_udp4_port) + .transpose()? + .or(maybe_quic_port) + .unwrap_or(port + 1); + ListenAddress::V4(lighthouse_network::ListenAddr { addr: ipv4, - udp_port, + disc_port, + quic_port, tcp_port, }) } @@ -932,31 +1030,44 @@ pub fn parse_listening_addresses( .then(unused_port::unused_tcp4_port) .transpose()? .unwrap_or(port); - let ipv4_udp_port = use_zero_ports + let ipv4_disc_port = use_zero_ports .then(unused_port::unused_udp4_port) .transpose()? - .or(maybe_udp_port) + .or(maybe_disc_port) .unwrap_or(ipv4_tcp_port); + let ipv4_quic_port = use_zero_ports + .then(unused_port::unused_udp4_port) + .transpose()? + .or(maybe_quic_port) + .unwrap_or(port + 1); // Defaults to 9090 when required let ipv6_tcp_port = use_zero_ports .then(unused_port::unused_tcp6_port) .transpose()? .unwrap_or(port6); - let ipv6_udp_port = use_zero_ports + let ipv6_disc_port = use_zero_ports .then(unused_port::unused_udp6_port) .transpose()? - .or(maybe_udp6_port) + .or(maybe_disc6_port) .unwrap_or(ipv6_tcp_port); + let ipv6_quic_port = use_zero_ports + .then(unused_port::unused_udp6_port) + .transpose()? + .or(maybe_quic6_port) + .unwrap_or(ipv6_tcp_port + 1); + ListenAddress::DualStack( lighthouse_network::ListenAddr { addr: ipv4, - udp_port: ipv4_udp_port, + disc_port: ipv4_disc_port, + quic_port: ipv4_quic_port, tcp_port: ipv4_tcp_port, }, lighthouse_network::ListenAddr { addr: ipv6, - udp_port: ipv6_udp_port, + disc_port: ipv6_disc_port, + quic_port: ipv6_quic_port, tcp_port: ipv6_tcp_port, }, ) @@ -1072,6 +1183,14 @@ pub fn set_network_config( ); } + if let Some(enr_quic_port_str) = cli_args.value_of("enr-quic-port") { + config.enr_quic4_port = Some( + enr_quic_port_str + .parse::() + .map_err(|_| format!("Invalid quic port: {}", enr_quic_port_str))?, + ); + } + if let Some(enr_tcp_port_str) = cli_args.value_of("enr-tcp-port") { config.enr_tcp4_port = Some( enr_tcp_port_str @@ -1088,6 +1207,14 @@ pub fn set_network_config( ); } + if let Some(enr_quic_port_str) = cli_args.value_of("enr-quic6-port") { + config.enr_quic6_port = Some( + enr_quic_port_str + .parse::() + .map_err(|_| format!("Invalid quic port: {}", enr_quic_port_str))?, + ); + } + if let Some(enr_tcp_port_str) = cli_args.value_of("enr-tcp6-port") { config.enr_tcp6_port = Some( enr_tcp_port_str @@ -1097,9 +1224,9 @@ pub fn set_network_config( } if cli_args.is_present("enr-match") { - // Match the Ip and UDP port in the enr. + // Match the IP and UDP port in the ENR. - // set the enr address to localhost if the address is unspecified + // Set the ENR address to localhost if the address is unspecified. if let Some(ipv4_addr) = config.listen_addrs().v4().cloned() { let ipv4_enr_addr = if ipv4_addr.addr == Ipv4Addr::UNSPECIFIED { Ipv4Addr::LOCALHOST @@ -1107,7 +1234,7 @@ pub fn set_network_config( ipv4_addr.addr }; config.enr_address.0 = Some(ipv4_enr_addr); - config.enr_udp4_port = Some(ipv4_addr.udp_port); + config.enr_udp4_port = Some(ipv4_addr.disc_port); } if let Some(ipv6_addr) = config.listen_addrs().v6().cloned() { @@ -1117,7 +1244,7 @@ pub fn set_network_config( ipv6_addr.addr }; config.enr_address.1 = Some(ipv6_enr_addr); - config.enr_udp6_port = Some(ipv6_addr.udp_port); + config.enr_udp6_port = Some(ipv6_addr.disc_port); } } @@ -1150,11 +1277,11 @@ pub fn set_network_config( // actually matters. Just use the udp port. let port = match config.listen_addrs() { - ListenAddress::V4(v4_addr) => v4_addr.udp_port, - ListenAddress::V6(v6_addr) => v6_addr.udp_port, + ListenAddress::V4(v4_addr) => v4_addr.disc_port, + ListenAddress::V6(v6_addr) => v6_addr.disc_port, ListenAddress::DualStack(v4_addr, _v6_addr) => { // NOTE: slight preference for ipv4 that I don't think is of importance. - v4_addr.udp_port + v4_addr.disc_port } }; @@ -1213,6 +1340,10 @@ pub fn set_network_config( warn!(log, "Discovery is disabled. New peers will not be found"); } + if cli_args.is_present("disable-quic") { + config.disable_quic_support = true; + } + if cli_args.is_present("disable-upnp") { config.upnp_enabled = false; } @@ -1232,6 +1363,7 @@ pub fn set_network_config( // Light client server config. config.enable_light_client_server = cli_args.is_present("light-client-server"); + // The self limiter is disabled by default. // This flag can be used both with or without a value. Try to parse it first with a value, if // no value is defined but the flag is present, use the default params. config.outbound_rate_limiter_config = clap_utils::parse_optional(cli_args, "self-limiter")?; @@ -1252,7 +1384,22 @@ pub fn set_network_config( config.proposer_only = true; warn!(log, "Proposer-only mode enabled"; "info"=> "Do not connect a validator client to this node unless via the --proposer-nodes flag"); } - + // The inbound rate limiter is enabled by default unless `disabled` is passed to the + // `inbound-rate-limiter` flag. Any other value should be parsed as a configuration string. + config.inbound_rate_limiter_config = match cli_args.value_of("inbound-rate-limiter") { + None => { + // Enabled by default, with default values + Some(Default::default()) + } + Some("disabled") => { + // Explicitly disabled + None + } + Some(config_str) => { + // Enabled with a custom configuration + Some(config_str.parse()?) + } + }; Ok(()) } diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 650763dca..3bef69ce8 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -16,7 +16,7 @@ pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; pub use config::{get_config, get_data_dir, get_slots_per_restore_point, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; -use slasher::Slasher; +use slasher::{DatabaseBackendOverride, Slasher}; use slog::{info, warn}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; @@ -83,10 +83,31 @@ impl ProductionBeaconNode { let builder = ClientBuilder::new(context.eth_spec_instance.clone()) .runtime_context(context) .chain_spec(spec) + .beacon_processor(client_config.beacon_processor.clone()) .http_api_config(client_config.http_api.clone()) .disk_store(&db_path, &freezer_db_path, store_config, log.clone())?; - let builder = if let Some(slasher_config) = client_config.slasher.clone() { + let builder = if let Some(mut slasher_config) = client_config.slasher.clone() { + match slasher_config.override_backend() { + DatabaseBackendOverride::Success(old_backend) => { + info!( + log, + "Slasher backend overriden"; + "reason" => "database exists", + "configured_backend" => %old_backend, + "override_backend" => %slasher_config.backend, + ); + } + DatabaseBackendOverride::Failure(path) => { + warn!( + log, + "Slasher backend override failed"; + "advice" => "delete old MDBX database or enable MDBX backend", + "path" => path.display() + ); + } + _ => {} + } let slasher = Arc::new( Slasher::open(slasher_config, log.new(slog::o!("service" => "slasher"))) .map_err(|e| format!("Slasher open error: {:?}", e))?, diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index a1c65bd26..32c386829 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -2,27 +2,27 @@ name = "store" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" -beacon_chain = {path = "../beacon_chain"} +tempfile = { workspace = true } +beacon_chain = { workspace = true } [dependencies] db-key = "0.0.5" -leveldb = { version = "0.8.6", default-features = false } -parking_lot = "0.12.0" -itertools = "0.10.0" -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -types = { path = "../../consensus/types" } -state_processing = { path = "../../consensus/state_processing" } -slog = "2.5.2" -serde = "1.0.116" +leveldb = { version = "0.8" } +parking_lot = { workspace = true } +itertools = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +types = { workspace = true } +state_processing = { workspace = true } +slog = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../../common/lighthouse_metrics" } -lru = "0.7.1" -sloggers = { version = "2.1.1", features = ["json"] } -directory = { path = "../../common/directory" } -strum = { version = "0.24.0", features = ["derive"] } \ No newline at end of file +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +lru = { workspace = true } +sloggers = { workspace = true } +directory = { workspace = true } +strum = { workspace = true } diff --git a/beacon_node/store/src/chunked_iter.rs b/beacon_node/store/src/chunked_iter.rs index 8ef0b6d20..b3322b522 100644 --- a/beacon_node/store/src/chunked_iter.rs +++ b/beacon_node/store/src/chunked_iter.rs @@ -30,16 +30,16 @@ where /// 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 `last_restore_point` slot should be the slot of a recent restore point as obtained from - /// `HotColdDB::get_latest_restore_point_slot`. We pass it as a parameter so that the caller can + /// 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, - last_restore_point_slot: Slot, + freezer_upper_limit: Slot, spec: &ChainSpec, ) -> Self { - let (_, end_vindex) = F::start_and_end_vindex(last_restore_point_slot, spec); + 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(); diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs index 73edfbb07..537614f28 100644 --- a/beacon_node/store/src/chunked_vector.rs +++ b/beacon_node/store/src/chunked_vector.rs @@ -299,7 +299,8 @@ macro_rules! field { } fn update_pattern(spec: &ChainSpec) -> UpdatePattern { - $update_pattern(spec) + let update_pattern = $update_pattern; + update_pattern(spec) } fn get_value( @@ -307,7 +308,8 @@ macro_rules! field { vindex: u64, spec: &ChainSpec, ) -> Result { - $get_value(state, vindex, spec) + let get_value = $get_value; + get_value(state, vindex, spec) } fn is_fixed_length() -> bool { diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 353be6bf0..125b73a45 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -19,6 +19,14 @@ pub trait Root: Field { end_state: BeaconState, end_root: Hash256, ) -> Result; + + /// The first slot for which this field is *no longer* stored in the freezer database. + /// + /// If `None`, then this field is not stored in the freezer database at all due to pruning + /// configuration. + fn freezer_upper_limit, Cold: ItemStore>( + store: &HotColdDB, + ) -> Option; } impl Root for BlockRoots { @@ -39,6 +47,13 @@ impl Root for BlockRoots { )?; Ok(SimpleForwardsIterator { values }) } + + fn freezer_upper_limit, Cold: ItemStore>( + store: &HotColdDB, + ) -> Option { + // Block roots are stored for all slots up to the split slot (exclusive). + Some(store.get_split_slot()) + } } impl Root for StateRoots { @@ -59,6 +74,15 @@ impl Root for StateRoots { )?; Ok(SimpleForwardsIterator { values }) } + + fn freezer_upper_limit, Cold: ItemStore>( + store: &HotColdDB, + ) -> Option { + // State roots are stored for all slots up to the latest restore point (exclusive). + // There may not be a latest restore point if state pruning is enabled, in which + // case this function will return `None`. + store.get_latest_restore_point_slot() + } } /// Forwards root iterator that makes use of a flat field table in the freezer DB. @@ -118,6 +142,7 @@ impl Iterator for SimpleForwardsIterator { pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> { PreFinalization { iter: Box>, + end_slot: Option, /// Data required by the `PostFinalization` iterator when we get to it. continuation_data: Option, Hash256)>>, }, @@ -129,6 +154,7 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, C PostFinalization { iter: SimpleForwardsIterator, }, + Finished, } impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> @@ -138,8 +164,8 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> /// /// The `get_state` closure should return a beacon state and final block/state root to backtrack /// from in the case where the iterated range does not lie entirely within the frozen portion of - /// the database. If an `end_slot` is provided and it is before the database's latest restore - /// point slot then the `get_state` closure will not be called at all. + /// the database. If an `end_slot` is provided and it is before the database's freezer upper + /// limit for the field then the `get_state` closure will not be called at all. /// /// It is OK for `get_state` to hold a lock while this function is evaluated, as the returned /// iterator is as lazy as possible and won't do any work apart from calling `get_state`. @@ -155,13 +181,15 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> ) -> Result { use HybridForwardsIterator::*; - let latest_restore_point_slot = store.get_latest_restore_point_slot(); + // First slot at which this field is *not* available in the freezer. i.e. all slots less + // than this slot have their data available in the freezer. + let freezer_upper_limit = F::freezer_upper_limit(store).unwrap_or(Slot::new(0)); - let result = if start_slot < latest_restore_point_slot { + let result = if start_slot < freezer_upper_limit { let iter = Box::new(FrozenForwardsIterator::new( store, start_slot, - latest_restore_point_slot, + freezer_upper_limit, spec, )); @@ -169,13 +197,14 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> // `end_slot`. If it tries to continue further a `NoContinuationData` error will be // returned. let continuation_data = - if end_slot.map_or(false, |end_slot| end_slot < latest_restore_point_slot) { + if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_limit) { None } else { Some(Box::new(get_state())) }; PreFinalization { iter, + end_slot, continuation_data, } } else { @@ -195,6 +224,7 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> match self { PreFinalization { iter, + end_slot, continuation_data, } => { match iter.next() { @@ -203,10 +233,17 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> // to a post-finalization iterator beginning from the last slot // of the pre iterator. None => { + // If the iterator has an end slot (inclusive) which has already been + // covered by the (exclusive) frozen forwards iterator, then we're done! + let iter_end_slot = Slot::from(iter.inner.end_vindex); + if end_slot.map_or(false, |end_slot| iter_end_slot == end_slot + 1) { + *self = Finished; + return Ok(None); + } + let continuation_data = continuation_data.take(); let store = iter.inner.store; - let start_slot = Slot::from(iter.inner.end_vindex); - + let start_slot = iter_end_slot; *self = PostFinalizationLazy { continuation_data, store, @@ -230,6 +267,7 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> self.do_next() } PostFinalization { iter } => iter.next().transpose(), + Finished => Ok(None), } } } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 7695ea520..87f8e0ffc 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -14,11 +14,11 @@ use crate::memory_store::MemoryStore; use crate::metadata::{ AnchorInfo, CompactionTimestamp, PruningCheckpoint, SchemaVersion, ANCHOR_INFO_KEY, COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, PRUNING_CHECKPOINT_KEY, - SCHEMA_VERSION_KEY, SPLIT_KEY, + SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, }; use crate::metrics; use crate::{ - get_key_for_col, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, + get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, PartialBeaconState, StoreItem, StoreOp, }; use itertools::process_results; @@ -110,10 +110,10 @@ pub enum HotColdDBError { IterationError { unexpected_key: BytesKey, }, - AttestationStateIsFinalized { + FinalizedStateNotInHotDatabase { split_slot: Slot, - request_slot: Option, - state_root: Hash256, + request_slot: Slot, + block_root: Hash256, }, } @@ -545,7 +545,7 @@ impl, Cold: ItemStore> HotColdDB /// upon that state (e.g., state roots). Additionally, only states from the hot store are /// returned. /// - /// See `Self::get_state` for information about `slot`. + /// See `Self::get_advanced_hot_state` for information about `max_slot`. /// /// ## Warning /// @@ -557,23 +557,78 @@ impl, Cold: ItemStore> HotColdDB /// - `state.block_roots` pub fn get_inconsistent_state_for_attestation_verification_only( &self, - state_root: &Hash256, - slot: Option, - ) -> Result>, Error> { + block_root: &Hash256, + max_slot: Slot, + state_root: Hash256, + ) -> Result)>, Error> { metrics::inc_counter(&metrics::BEACON_STATE_GET_COUNT); + self.get_advanced_hot_state_with_strategy( + *block_root, + max_slot, + state_root, + StateProcessingStrategy::Inconsistent, + ) + } - let split_slot = self.get_split_slot(); + /// Get a state with `latest_block_root == block_root` advanced through to at most `max_slot`. + /// + /// The `state_root` argument is used to look up the block's un-advanced state in case an + /// advanced state is not found. + /// + /// Return the `(result_state_root, state)` satisfying: + /// + /// - `result_state_root == state.canonical_root()` + /// - `state.slot() <= max_slot` + /// - `state.get_latest_block_root(result_state_root) == block_root` + /// + /// Presently this is only used to avoid loading the un-advanced split state, but in future will + /// be expanded to return states from an in-memory cache. + pub fn get_advanced_hot_state( + &self, + block_root: Hash256, + max_slot: Slot, + state_root: Hash256, + ) -> Result)>, Error> { + self.get_advanced_hot_state_with_strategy( + block_root, + max_slot, + state_root, + StateProcessingStrategy::Accurate, + ) + } - if slot.map_or(false, |slot| slot < split_slot) { - Err(HotColdDBError::AttestationStateIsFinalized { - split_slot, - request_slot: slot, - state_root: *state_root, + /// Same as `get_advanced_hot_state` but taking a `StateProcessingStrategy`. + pub fn get_advanced_hot_state_with_strategy( + &self, + block_root: Hash256, + max_slot: Slot, + state_root: Hash256, + state_processing_strategy: StateProcessingStrategy, + ) -> Result)>, Error> { + // Hold a read lock on the split point so it can't move while we're trying to load the + // state. + let split = self.split.read_recursive(); + + // Sanity check max-slot against the split slot. + if max_slot < split.slot { + return Err(HotColdDBError::FinalizedStateNotInHotDatabase { + split_slot: split.slot, + request_slot: max_slot, + block_root, } - .into()) - } else { - self.load_hot_state(state_root, StateProcessingStrategy::Inconsistent) + .into()); } + + let state_root = if block_root == split.block_root && split.slot <= max_slot { + split.state_root + } else { + state_root + }; + let state = self + .load_hot_state(&state_root, state_processing_strategy)? + .map(|state| (state_root, state)); + drop(split); + Ok(state) } /// Delete a state, ensuring it is removed from the LRU cache, as well as from on-disk. @@ -908,6 +963,9 @@ impl, Cold: ItemStore> HotColdDB ops.push(op); // 2. Store updated vector entries. + // Block roots need to be written here as well as by the `ChunkWriter` in `migrate_db` + // because states may require older block roots, and the writer only stores block roots + // between the previous split point and the new split point. let db = &self.cold_db; store_updated_vector(BlockRoots, db, state, &self.spec, ops)?; store_updated_vector(StateRoots, db, state, &self.spec, ops)?; @@ -1180,14 +1238,29 @@ impl, Cold: ItemStore> HotColdDB *self.split.read_recursive() } - pub fn set_split(&self, slot: Slot, state_root: Hash256) { - *self.split.write() = Split { slot, state_root }; + pub fn set_split(&self, slot: Slot, state_root: Hash256, block_root: Hash256) { + *self.split.write() = Split { + slot, + state_root, + block_root, + }; } - /// Fetch the slot of the most recently stored restore point. - pub fn get_latest_restore_point_slot(&self) -> Slot { - (self.get_split_slot() - 1) / self.config.slots_per_restore_point - * self.config.slots_per_restore_point + /// Fetch the slot of the most recently stored restore point (if any). + pub fn get_latest_restore_point_slot(&self) -> Option { + let split_slot = self.get_split_slot(); + let anchor = self.get_anchor_info(); + + // There are no restore points stored if the state upper limit lies in the hot database. + // It hasn't been reached yet, and may never be. + if anchor.map_or(false, |a| a.state_upper_limit >= split_slot) { + None + } else { + Some( + (split_slot - 1) / self.config.slots_per_restore_point + * self.config.slots_per_restore_point, + ) + } } /// Load the database schema version from disk. @@ -1216,25 +1289,36 @@ impl, Cold: ItemStore> HotColdDB } /// Initialise the anchor info for checkpoint sync starting from `block`. - pub fn init_anchor_info(&self, block: BeaconBlockRef<'_, E>) -> Result { + pub fn init_anchor_info( + &self, + block: BeaconBlockRef<'_, E>, + retain_historic_states: bool, + ) -> Result { let anchor_slot = block.slot(); let slots_per_restore_point = self.config.slots_per_restore_point; - // Set the `state_upper_limit` to the slot of the *next* restore point. - // See `get_state_upper_limit` for rationale. - let next_restore_point_slot = if anchor_slot % slots_per_restore_point == 0 { + let state_upper_limit = if !retain_historic_states { + STATE_UPPER_LIMIT_NO_RETAIN + } else if anchor_slot % slots_per_restore_point == 0 { anchor_slot } else { + // Set the `state_upper_limit` to the slot of the *next* restore point. + // See `get_state_upper_limit` for rationale. (anchor_slot / slots_per_restore_point + 1) * slots_per_restore_point }; - let anchor_info = AnchorInfo { - anchor_slot, - oldest_block_slot: anchor_slot, - oldest_block_parent: block.parent_root(), - state_upper_limit: next_restore_point_slot, - state_lower_limit: self.spec.genesis_slot, + let anchor_info = if state_upper_limit == 0 && anchor_slot == 0 { + // Genesis archive node: no anchor because we *will* store all states. + None + } else { + Some(AnchorInfo { + anchor_slot, + oldest_block_slot: anchor_slot, + oldest_block_parent: block.parent_root(), + state_upper_limit, + state_lower_limit: self.spec.genesis_slot, + }) }; - self.compare_and_set_anchor_info(None, Some(anchor_info)) + self.compare_and_set_anchor_info(None, anchor_info) } /// Get a clone of the store's anchor info. @@ -1361,11 +1445,26 @@ impl, Cold: ItemStore> HotColdDB self.hot_db.put(&CONFIG_KEY, &self.config.as_disk_config()) } - /// Load the split point from disk. - fn load_split(&self) -> Result, Error> { + /// Load the split point from disk, sans block root. + fn load_split_partial(&self) -> Result, Error> { self.hot_db.get(&SPLIT_KEY) } + /// Load the split point from disk, including block root. + fn load_split(&self) -> Result, Error> { + match self.load_split_partial()? { + Some(mut split) => { + // 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; + Ok(Some(split)) + } + None => Ok(None), + } + } + /// Stage the split for storage to disk. pub fn store_split_in_batch(&self) -> KeyValueStoreOp { self.split.read_recursive().as_kv_store_op(SPLIT_KEY) @@ -1500,6 +1599,25 @@ impl, Cold: ItemStore> HotColdDB ) } + /// Update the linear array of frozen block roots with the block root for several skipped slots. + /// + /// Write the block root at all slots from `start_slot` (inclusive) to `end_slot` (exclusive). + pub fn store_frozen_block_root_at_skip_slots( + &self, + start_slot: Slot, + end_slot: Slot, + block_root: Hash256, + ) -> Result, Error> { + let mut ops = vec![]; + let mut block_root_writer = + ChunkWriter::::new(&self.cold_db, start_slot.as_usize())?; + for slot in start_slot.as_usize()..end_slot.as_usize() { + block_root_writer.set(slot, block_root, &mut ops)?; + } + block_root_writer.write(&mut ops)?; + Ok(ops) + } + /// 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(); @@ -1611,42 +1729,47 @@ impl, Cold: ItemStore> HotColdDB /// Advance the split point of the store, moving new finalized states to the freezer. pub fn migrate_database, Cold: ItemStore>( store: Arc>, - frozen_head_root: Hash256, - frozen_head: &BeaconState, + finalized_state_root: Hash256, + finalized_block_root: Hash256, + finalized_state: &BeaconState, ) -> Result<(), Error> { debug!( store.log, "Freezer migration started"; - "slot" => frozen_head.slot() + "slot" => finalized_state.slot() ); // 0. Check that the migration is sensible. - // The new frozen head must increase the current split slot, and lie on an epoch + // 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 anchor_slot = store - .anchor_info - .read_recursive() - .as_ref() - .map(|a| a.anchor_slot); + let anchor_info = store.anchor_info.read_recursive().clone(); + let anchor_slot = anchor_info.as_ref().map(|a| a.anchor_slot); - if frozen_head.slot() < current_split_slot { + if finalized_state.slot() < current_split_slot { return Err(HotColdDBError::FreezeSlotError { current_split_slot, - proposed_split_slot: frozen_head.slot(), + proposed_split_slot: finalized_state.slot(), } .into()); } - if frozen_head.slot() % E::slots_per_epoch() != 0 { - return Err(HotColdDBError::FreezeSlotUnaligned(frozen_head.slot()).into()); + if finalized_state.slot() % E::slots_per_epoch() != 0 { + return Err(HotColdDBError::FreezeSlotUnaligned(finalized_state.slot()).into()); } - let mut hot_db_ops: Vec> = Vec::new(); + let mut hot_db_ops = vec![]; + let mut cold_db_ops = vec![]; - // 1. Copy all of the states between the head and the split slot, from the hot DB + // Chunk writer for the linear block roots in the freezer DB. + // Start at the new upper limit because we iterate backwards. + let new_frozen_block_root_upper_limit = finalized_state.slot().as_usize().saturating_sub(1); + let mut block_root_writer = + ChunkWriter::::new(&store.cold_db, new_frozen_block_root_upper_limit)?; + + // 1. Copy all of the states between the new finalized state and the split slot, from the hot DB // to the cold DB. Delete the execution payloads of these now-finalized blocks. - let state_root_iter = RootsIterator::new(&store, frozen_head); + let state_root_iter = RootsIterator::new(&store, finalized_state); for maybe_tuple in state_root_iter.take_while(|result| match result { Ok((_, _, slot)) => { slot >= ¤t_split_slot @@ -1656,13 +1779,31 @@ pub fn migrate_database, Cold: ItemStore>( }) { let (block_root, state_root, slot) = maybe_tuple?; - let mut cold_db_ops: Vec = Vec::new(); + // Delete the execution payload if payload pruning is enabled. At a skipped slot we may + // delete the payload for the finalized block itself, but that's OK as we only guarantee + // that payloads are present for slots >= the split slot. The payload fetching code is also + // forgiving of missing payloads. + if store.config.prune_payloads { + hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); + } - if slot % store.config.slots_per_restore_point == 0 { - let state: BeaconState = get_full_state(&store.hot_db, &state_root, &store.spec)? - .ok_or(HotColdDBError::MissingStateToFreeze(state_root))?; + // Delete the old summary, and the full state if we lie on an epoch boundary. + hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); - store.store_cold_state(&state_root, &state, &mut cold_db_ops)?; + // Store the block root for this slot in the linear array of frozen block roots. + block_root_writer.set(slot.as_usize(), block_root, &mut cold_db_ops)?; + + // Do not try to store states if a restore point is yet to be stored, or will never be + // stored (see `STATE_UPPER_LIMIT_NO_RETAIN`). Make an exception for the genesis state + // which always needs to be copied from the hot DB to the freezer and should not be deleted. + if slot != 0 + && anchor_info + .as_ref() + .map_or(false, |anchor| slot < anchor.state_upper_limit) + { + debug!(store.log, "Pruning finalized state"; "slot" => slot); + + continue; } // Store a pointer from this state root to its slot, so we can later reconstruct states @@ -1671,22 +1812,24 @@ pub fn migrate_database, Cold: ItemStore>( let op = cold_state_summary.as_kv_store_op(state_root); cold_db_ops.push(op); - // There are data dependencies between calls to `store_cold_state()` that prevent us from - // doing one big call to `store.cold_db.do_atomically()` at end of the loop. - store.cold_db.do_atomically(cold_db_ops)?; + if slot % store.config.slots_per_restore_point == 0 { + let state: BeaconState = get_full_state(&store.hot_db, &state_root, &store.spec)? + .ok_or(HotColdDBError::MissingStateToFreeze(state_root))?; - // Delete the old summary, and the full state if we lie on an epoch boundary. - hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); + store.store_cold_state(&state_root, &state, &mut cold_db_ops)?; - // Delete the execution payload if payload pruning is enabled. At a skipped slot we may - // delete the payload for the finalized block itself, but that's OK as we only guarantee - // that payloads are present for slots >= the split slot. The payload fetching code is also - // forgiving of missing payloads. - if store.config.prune_payloads { - hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); + // Commit the batch of cold DB ops whenever a full state is written. Each state stored + // may read the linear fields of previous states stored. + store + .cold_db + .do_atomically(std::mem::take(&mut cold_db_ops))?; } } + // Finish writing the block roots and commit the remaining cold DB ops. + block_root_writer.write(&mut cold_db_ops)?; + store.cold_db.do_atomically(cold_db_ops)?; + // Warning: Critical section. We have to take care not to put any of the two databases in an // inconsistent state if the OS process dies at any point during the freezeing // procedure. @@ -1724,8 +1867,9 @@ pub fn migrate_database, Cold: ItemStore>( // 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 { - slot: frozen_head.slot(), - state_root: frozen_head_root, + slot: finalized_state.slot(), + state_root: finalized_state_root, + block_root: finalized_block_root, }; store.hot_db.put_sync(&SPLIT_KEY, &split)?; @@ -1741,7 +1885,7 @@ pub fn migrate_database, Cold: ItemStore>( debug!( store.log, "Freezer migration complete"; - "slot" => frozen_head.slot() + "slot" => finalized_state.slot() ); Ok(()) @@ -1750,8 +1894,16 @@ pub fn migrate_database, Cold: ItemStore>( /// Struct for storing the split slot and state root in the database. #[derive(Debug, Clone, Copy, PartialEq, Default, Encode, Decode, Deserialize, Serialize)] pub struct Split { - pub(crate) slot: Slot, - pub(crate) state_root: Hash256, + pub slot: Slot, + pub state_root: Hash256, + /// The block root of the split state. + /// + /// This is used to provide special handling for the split state in the case where there are + /// skipped slots. The split state will *always* be the advanced state, so callers + /// who only have the finalized block root should use `get_advanced_hot_state` to get this state, + /// rather than fetching `block.state_root()` (the unaligned state) which will have been pruned. + #[ssz(skip_serializing, skip_deserializing)] + pub block_root: Hash256, } impl StoreItem for Split { diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs index 86bd4ffac..7aac9f72d 100644 --- a/beacon_node/store/src/leveldb_store.rs +++ b/beacon_node/store/src/leveldb_store.rs @@ -167,7 +167,7 @@ impl KeyValueStore for LevelDB { ) }; - for (start_key, end_key) in vec![ + for (start_key, end_key) in [ endpoints(DBColumn::BeaconStateTemporary), endpoints(DBColumn::BeaconState), ] { diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 6f50d7038..ccfddcf8f 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -16,6 +16,9 @@ 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); +/// 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); + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SchemaVersion(pub u64); diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index cd923da40..9f2532d0a 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -373,6 +373,7 @@ macro_rules! impl_try_into_beacon_state { // Caching total_active_balance: <_>::default(), + progressive_balances_cache: <_>::default(), committee_caches: <_>::default(), pubkey_cache: <_>::default(), exit_cache: <_>::default(), diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index cd50babdb..bac5d3cc8 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -63,7 +63,7 @@ where .load_cold_state_by_slot(lower_limit_slot)? .ok_or(HotColdDBError::MissingLowerLimitState(lower_limit_slot))?; - state.build_all_caches(&self.spec)?; + state.build_caches(&self.spec)?; process_results(block_root_iter, |iter| -> Result<(), Error> { let mut io_batch = vec![]; diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index bd20f24ee..afb93f365 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -2,11 +2,11 @@ name = "timer" version = "0.2.0" authors = ["Sigma Prime "] -edition = "2021" +edition = { workspace = true } [dependencies] -beacon_chain = { path = "../beacon_chain" } -slot_clock = { path = "../../common/slot_clock" } -tokio = { version = "1.14.0", features = ["full"] } -slog = "2.5.2" -task_executor = { path = "../../common/task_executor" } +beacon_chain = { workspace = true } +slot_clock = { workspace = true } +tokio = { workspace = true } +slog = { workspace = true } +task_executor = { workspace = true } diff --git a/book/src/LaTeX/full-withdrawal.tex b/book/src/LaTeX/full-withdrawal.tex index 2447ba097..a4b384872 100644 --- a/book/src/LaTeX/full-withdrawal.tex +++ b/book/src/LaTeX/full-withdrawal.tex @@ -37,7 +37,7 @@ \rput[bl](9.0,-3.49){27.3 hours} \rput[bl](8.8,-5.49){Varying time} \rput[bl](8.7,-5.99){validator sweep} - \rput[bl](8.9,-6.59){up to 5 days} + \rput[bl](8.9,-6.59){up to \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.6,-2.19)(8.0,-3.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](11.7,-4.79)(7.9,-6.89) \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-2.49)(0.0,-4.29) diff --git a/book/src/LaTeX/partial-withdrawal.tex b/book/src/LaTeX/partial-withdrawal.tex index 05db3b688..4d1d0b5f0 100644 --- a/book/src/LaTeX/partial-withdrawal.tex +++ b/book/src/LaTeX/partial-withdrawal.tex @@ -31,7 +31,7 @@ \rput[bl](0.9,-1.59){Beacon chain} \psframe[linecolor=black, linewidth=0.04, dimen=outer](10.7,-3.29)(6.8,-5.09) \rput[bl](7.6,-3.99){validator sweep} - \rput[bl](7.5,-4.69){$\sim$ every 5 days} + \rput[bl](7.82,-4.73){every \textit{n} days} \psframe[linecolor=black, linewidth=0.04, dimen=outer](3.7,-3.29)(0.0,-5.09) \rput[bl](1.3,-4.09){BLS to} \rput[bl](0.5,-4.69){execution change} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index bfd5a02a6..3f58d8aa4 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -12,16 +12,20 @@ * [Run a Node](./run_a_node.md) * [Become a Validator](./mainnet-validator.md) * [Validator Management](./validator-management.md) + * [The `validator-manager` Command](./validator-manager.md) + * [Creating validators](./validator-manager-create.md) + * [Moving validators](./validator-manager-move.md) * [Slashing Protection](./slashing-protection.md) * [Voluntary Exits](./voluntary-exit.md) * [Partial Withdrawals](./partial-withdrawal.md) * [Validator Monitoring](./validator-monitoring.md) * [Doppelganger Protection](./validator-doppelganger.md) * [Suggested Fee Recipient](./suggested-fee-recipient.md) + * [Validator Graffiti](./graffiti.md) * [APIs](./api.md) * [Beacon Node API](./api-bn.md) - * [/lighthouse](./api-lighthouse.md) - * [Validator Inclusion APIs](./validator-inclusion.md) + * [Lighthouse API](./api-lighthouse.md) + * [Validator Inclusion APIs](./validator-inclusion.md) * [Validator Client API](./api-vc.md) * [Endpoints](./api-vc-endpoints.md) * [Authorization Header](./api-vc-auth-header.md) @@ -36,21 +40,20 @@ * [Advanced Usage](./advanced.md) * [Checkpoint Sync](./checkpoint-sync.md) * [Custom Data Directories](./advanced-datadir.md) - * [Validator Graffiti](./graffiti.md) * [Proposer Only Beacon Nodes](./advanced-proposer-only.md) * [Remote Signing with Web3Signer](./validator-web3signer.md) * [Database Configuration](./advanced_database.md) * [Database Migrations](./database-migrations.md) - * [Key Management](./key-management.md) + * [Key Management (Deprecated)](./key-management.md) * [Key Recovery](./key-recovery.md) * [Advanced Networking](./advanced_networking.md) * [Running a Slasher](./slasher.md) * [Redundancy](./redundancy.md) - * [Pre-Releases](./advanced-pre-releases.md) * [Release Candidates](./advanced-release-candidates.md) - * [MEV and Lighthouse](./builders.md) + * [MEV](./builders.md) * [Merge Migration](./merge-migration.md) * [Late Block Re-orgs](./late-block-re-orgs.md) * [Contributing](./contributing.md) * [Development Environment](./setup.md) * [FAQs](./faq.md) +* [Protocol Developers](./developers.md) \ No newline at end of file diff --git a/book/src/advanced-pre-releases.md b/book/src/advanced-pre-releases.md deleted file mode 100644 index f3f4a5230..000000000 --- a/book/src/advanced-pre-releases.md +++ /dev/null @@ -1,4 +0,0 @@ -# Pre-Releases - -Pre-releases are now referred to as [Release Candidates](./advanced-release-candidates.md). The terms may -be used interchangeably. diff --git a/book/src/advanced-release-candidates.md b/book/src/advanced-release-candidates.md index 842bc4840..b2ff02136 100644 --- a/book/src/advanced-release-candidates.md +++ b/book/src/advanced-release-candidates.md @@ -7,7 +7,7 @@ [`v1.4.0`]: https://github.com/sigp/lighthouse/releases/tag/v1.4.0 From time-to-time, Lighthouse *release candidates* will be published on the [sigp/lighthouse] -repository. These releases have passed the usual automated testing, however the developers would +repository. Release candidates are previously known as Pre-Releases. These releases have passed the usual automated testing, however the developers would like to see it running "in the wild" in a variety of configurations before declaring it an official, stable release. Release candidates are also used by developers to get feedback from users regarding the ergonomics of new features or changes. @@ -36,8 +36,9 @@ Users may wish to try a release candidate for the following reasons: - To help detect bugs and regressions before they reach production. - To provide feedback on annoyances before they make it into a release and become harder to change or revert. +There can also be a scenario that a bug has been found and requires an urgent fix. An example of incidence is [v4.0.2-rc.0](https://github.com/sigp/lighthouse/releases/tag/v4.0.2-rc.0) which contains a hot-fix to address high CPU usage experienced after the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12th April 2023. In this scenario, we will announce the release candidate on [Github](https://github.com/sigp/lighthouse/releases) and also on [Discord](https://discord.gg/cyAszAh) to recommend users to update to the release candidate version. + ## When *not* to use a release candidate -It is not recommended to use release candidates for any critical tasks on mainnet (e.g., staking). -To test critical features, try one of the testnets (e.g., Prater). +Other than the above scenarios, it is generally not recommended to use release candidates for any critical tasks on mainnet (e.g., staking). To test new release candidate features, try one of the testnets (e.g., Goerli). diff --git a/book/src/advanced.md b/book/src/advanced.md index d46cae699..51416a3b7 100644 --- a/book/src/advanced.md +++ b/book/src/advanced.md @@ -6,4 +6,18 @@ elsewhere? This section provides detailed information about configuring Lighthouse for specific use cases, and tips about how things work under the hood. -* [Advanced Database Configuration](./advanced_database.md): understanding space-time trade-offs in the database. +* [Checkpoint Sync](./checkpoint-sync.md): quickly sync the beacon chain to perform validator duties. +* [Custom Data Directories](./advanced-datadir.md): modify the data directory to your preferred location. +* [Proposer Only Beacon Nodes](./advanced-proposer-only.md): beacon node only for proposer duty for increased anonymity. +* [Remote Signing with Web3Signer](./validator-web3signer.md): don't want to store your keystore in local node? Use web3signer. +* [Database Configuration](./advanced_database.md): understanding space-time trade-offs in the database. +* [Database Migrations](./database-migrations.md): have a look at all previous Lighthouse database scheme versions. +* [Key Management](./key-management.md): explore how to generate wallet with Lighthouse. +* [Key Recovery](./key-recovery.md): explore how to recover wallet and validator with Lighthouse. +* [Advanced Networking](./advanced_networking.md): open your ports to have a diverse and healthy set of peers. +* [Running a Slasher](./slasher.md): contribute to the health of the network by running a slasher. +* [Redundancy](./redundancy.md): want to have more than one beacon node as backup? This is for you. +* [Release Candidates](./advanced-release-candidates.md): latest release of Lighthouse to get feedback from users. +* [Maximal Extractable Value](./builders.md): use external builders for a potential higher rewards during block proposals +* [Merge Migration](./merge-migration.md): look at what you need to do during a significant network upgrade: The Merge +* [Late Block Re-orgs](./late-block-re-orgs.md): read information about Lighthouse late block re-orgs. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 57e49531c..20c5d7443 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -23,13 +23,17 @@ states to slow down dramatically. A lower _slots per restore point_ value (SPRP) frequent restore points, while a higher SPRP corresponds to less frequent. The table below shows some example values. -| Use Case | SPRP | Yearly Disk Usage | Load Historical State | -|--------------------------|------|-------------------|-----------------------| -| Block explorer/analysis | 32 | 1.4 TB | 155 ms | -| Hobbyist (prev. default) | 2048 | 23.1 GB | 10.2 s | -| Validator only (default) | 8192 | 5.7 GB | 41 s | +| Use Case | SPRP | Yearly Disk Usage* | Load Historical State | +|----------------------------|------|-------------------|-----------------------| +| Research | 32 | 3.4 TB | 155 ms | +| Block explorer/analysis | 128 | 851 GB | 620 ms | +| Enthusiast (prev. default) | 2048 | 53.6 GB | 10.2 s | +| Hobbyist | 4096 | 26.8 GB | 20.5 s | +| Validator only (default) | 8192 | 12.7 GB | 41 s | -As you can see, it's a high-stakes trade-off! The relationships to disk usage and historical state +*Last update: May 2023. + +As we can see, it's a high-stakes trade-off! The relationships to disk usage and historical state load time are both linear – doubling SPRP halves disk usage and doubles load time. The minimum SPRP is 32, and the maximum is 8192. @@ -38,9 +42,11 @@ The default value is 8192 for databases synced from scratch using Lighthouse v2. The values shown in the table are approximate, calculated using a simple heuristic: each `BeaconState` consumes around 18MB of disk space, and each block replayed takes around 5ms. The -**Yearly Disk Usage** column shows the approx size of the freezer DB _alone_ (hot DB not included), -and the **Load Historical State** time is the worst-case load time for a state in the last slot -before a restore point. +**Yearly Disk Usage** column shows the approximate size of the freezer DB _alone_ (hot DB not included), calculated proportionally using the total freezer database disk usage. +The **Load Historical State** time is the worst-case load time for a state in the last slot +before a restore point. + +As an example, we use an SPRP of 4096 to calculate the total size of the freezer database until May 2023. It has been about 900 days since the genesis, the total disk usage by the freezer database is therefore: 900/365*26.8 GB = 66 GB. ### Defaults @@ -68,6 +74,8 @@ The historical state cache size can be specified with the flag `--historic-state lighthouse beacon_node --historic-state-cache-size 4 ``` +> Note: This feature will cause high memory usage. + ## Glossary * _Freezer DB_: part of the database storing finalized states. States are stored in a sparser diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 08d276ba3..b1f05450c 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -38,7 +38,6 @@ large peer count will not speed up sync. For these reasons, we recommend users do not modify the `--target-peers` count drastically and use the (recommended) default. - ### NAT Traversal (Port Forwarding) Lighthouse, by default, uses port 9000 for both TCP and UDP. Lighthouse will @@ -51,11 +50,11 @@ peers for your node and overall improve the Ethereum consensus network. Lighthouse currently supports UPnP. If UPnP is enabled on your router, Lighthouse will automatically establish the port mappings for you (the beacon node will inform you of established routes in this case). If UPnP is not -enabled, we recommend you manually set up port mappings to both of Lighthouse's +enabled, we recommend you to manually set up port mappings to both of Lighthouse's TCP and UDP ports (9000 by default). > Note: Lighthouse needs to advertise its publicly accessible ports in -> order to inform its peers that it is contactable and how to connect to it. +> order to inform its peers that it is contactable and how to connect to it. > Lighthouse has an automated way of doing this for the UDP port. This means > Lighthouse can detect its external UDP port. There is no such mechanism for the > TCP port. As such, we assume that the external UDP and external TCP port is the @@ -63,6 +62,28 @@ TCP and UDP ports (9000 by default). > explicitly specify them using the `--enr-tcp-port` and `--enr-udp-port` as > explained in the following section. +### How to Open Ports + +The steps to do port forwarding depends on the router, but the general steps are given below: +1. Determine the default gateway IP: +- On Linux: open a terminal and run `ip route | grep default`, the result should look something similar to `default via 192.168.50.1 dev wlp2s0 proto dhcp metric 600`. The `192.168.50.1` is your router management default gateway IP. +- On MacOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. +- On Windows: open a command prompt and run `ipconfig` and look for the `Default Gateway` which will show you the gateway IP. + + The default gateway IP usually looks like 192.168.X.X. Once you obtain the IP, enter it to a web browser and it will lead you to the router management page. + +2. Login to the router management page. The login credentials are usually available in the manual or the router, or it can be found on a sticker underneath the router. You can also try the login credentials for some common router brands listed [here](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/). + +3. Navigate to the port forward settings in your router. The exact step depends on the router, but typically it will fall under the "Advanced" section, under the name "port forwarding" or "virtual server". + +4. Configure a port forwarding rule as below: +- Protocol: select `TCP/UDP` or `BOTH` +- External port: `9000` +- Internal port: `9000` +- IP address: Usually there is a dropdown list for you to select the device. Choose the device that is running Lighthouse + +5. To check that you have successfully open the ports, go to [yougetsignal](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. + ### ENR Configuration @@ -81,7 +102,82 @@ and if it is, it will update your ENR to the correct public IP and port address (meaning you do not need to set it manually). Lighthouse persists its ENR, so on reboot it will re-load the settings it had discovered previously. -Modifying the ENR settings can degrade the discovery of your node making it +Modifying the ENR settings can degrade the discovery of your node, making it harder for peers to find you or potentially making it harder for other peers to find each other. We recommend not touching these settings unless for a more advanced use case. + + +### IPv6 support + +As noted in the previous sections, two fundamental parts to ensure good +connectivity are: The parameters that configure the sockets over which +Lighthouse listens for connections, and the parameters used to tell other peers +how to connect to your node. This distinction is relevant and applies to most +nodes that do not run directly on a public network. + +#### Configuring Lighthouse to listen over IPv4/IPv6/Dual stack + +To listen over only IPv6 use the same parameters as done when listening over +IPv4 only: + +- `--listen-address :: --port 9909` will listen over IPv6 using port `9909` for +TCP and UDP. +- `--listen-address :: --port 9909 --discovery-port 9999` will listen over + IPv6 using port `9909` for TCP and port `9999` for UDP. + +To listen over both IPv4 and IPv6: +- Set two listening addresses using the `--listen-address` flag twice ensuring + the two addresses are one IPv4, and the other IPv6. When doing so, the + `--port` and `--discovery-port` flags will apply exclusively to IPv4. Note + that this behaviour differs from the Ipv6 only case described above. +- If necessary, set the `--port6` flag to configure the port used for TCP and + UDP over IPv6. This flag has no effect when listening over IPv6 only. +- If necessary, set the `--discovery-port6` flag to configure the IPv6 UDP + port. This will default to the value given to `--port6` if not set. This flag + has no effect when listening over IPv6 only. + +##### Configuration Examples + +- `--listen-address :: --listen-address 0.0.0.0 --port 9909` will listen + over IPv4 using port `9909` for TCP and UDP. It will also listen over IPv6 but + using the default value for `--port6` for UDP and TCP (`9090`). +- `--listen-address :: --listen-address --port 9909 --discovery-port6 9999` + will have the same configuration as before except for the IPv6 UDP socket, + which will use port `9999`. + +#### Configuring Lighthouse to advertise IPv6 reachable addresses +Lighthouse supports IPv6 to connect to other nodes both over IPv6 exclusively, +and dual stack using one socket for IPv4 and another socket for IPv6. In both +scenarios, the previous sections still apply. In summary: + +> Beacon nodes must advertise their publicly reachable socket address + +In order to do so, lighthouse provides the following CLI options/parameters. + +- `--enr-udp-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-udp6-port` Use this to advertise the port that is publicly reachable + over UDP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-tcp-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv4 address. This might differ from the + IPv4 port used to listen. +- `--enr-tcp6-port` Use this to advertise the port that is publicly reachable + over TCP with a publicly reachable IPv6 address. This might differ from the + IPv6 port used to listen. +- `--enr-addresses` Use this to advertise publicly reachable addresses. Takes at + most two values, one for IPv4 and one for IPv6. Note that a beacon node that + advertises some address, must be + reachable both over UDP and TCP. + +In the general case, a user will not require to set these explicitly. Update +these options only if you can guarantee your node is reachable with these +values. + +#### Known caveats + +IPv6 link local addresses are likely to have poor connectivity if used in +topologies with more than one interface. Use global addresses for the general +case. diff --git a/book/src/api-bn.md b/book/src/api-bn.md index b86e593bf..11a006493 100644 --- a/book/src/api-bn.md +++ b/book/src/api-bn.md @@ -5,7 +5,7 @@ specification][OpenAPI]. Please follow that link for a full description of each ## Starting the server -A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5052`. +A Lighthouse beacon node can be configured to expose an HTTP server by supplying the `--http` flag. The default listen address is `http://127.0.0.1:5052`. The following CLI flags control the HTTP server: @@ -55,11 +55,8 @@ Additional risks to be aware of include: ## CLI Example -Start the beacon node with the HTTP server listening on [http://localhost:5052](http://localhost:5052): +Start a beacon node and an execution node according to [Run a node](./run_a_node.md). Note that since [The Merge](https://ethereum.org/en/roadmap/merge/), an execution client is required to be running along with a beacon node. Hence, the query on Beacon Node APIs requires users to run both. While there are some Beacon Node APIs that you can query with only the beacon node, such as the [node version](https://ethereum.github.io/beacon-APIs/#/Node/getNodeVersion), in general an execution client is required to get the updated information about the beacon chain, such as [state root](https://ethereum.github.io/beacon-APIs/#/Beacon/getStateRoot), [headers](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeaders) and many others, which are dynamically progressing with time. -```bash -lighthouse bn --http -``` ## HTTP Request/Response Examples @@ -77,40 +74,46 @@ curl -X GET "http://localhost:5052/eth/v1/beacon/headers/head" -H "accept: appl ```json { + "execution_optimistic": false, + "finalized": false, "data": { - "root": "0x4381454174fc28c7095077e959dcab407ae5717b5dca447e74c340c1b743d7b2", + "root": "0x9059bbed6b8891e0ba2f656dbff93fc40f8c7b2b7af8fea9df83cfce5ee5e3d8", "canonical": true, "header": { "message": { - "slot": "3199", - "proposer_index": "19077", - "parent_root": "0xf1934973041c5896d0d608e52847c3cd9a5f809c59c64e76f6020e3d7cd0c7cd", - "state_root": "0xe8e468f9f5961655dde91968f66480868dab8d4147de9498111df2b7e4e6fe60", - "body_root": "0x6f183abc6c4e97f832900b00d4e08d4373bfdc819055d76b0f4ff850f559b883" + "slot": "6271829", + "proposer_index": "114398", + "parent_root": "0x1d2b4fa8247f754a7a86d36e1d0283a5e425491c431533716764880a7611d225", + "state_root": "0x2b48adea290712f56b517658dde2da5d36ee01c41aebe7af62b7873b366de245", + "body_root": "0x6fa74c995ce6f397fa293666cde054d6a9741f7ec280c640bee51220b4641e2d" }, - "signature": "0x988064a2f9cf13fe3aae051a3d85f6a4bca5a8ff6196f2f504e32f1203b549d5f86a39c6509f7113678880701b1881b50925a0417c1c88a750c8da7cd302dda5aabae4b941e3104d0cf19f5043c4f22a7d75d0d50dad5dbdaf6991381dc159ab" + "signature": "0x8258e64fea426033676a0045c50543978bf173114ba94822b12188e23cbc8d8e89e0b5c628a881bf3075d325bc11341105a4e3f9332ac031d89a93b422525b79e99325928a5262f17dfa6cc3ddf84ca2466fcad86a3c168af0d045f79ef52036" } } } ``` +The `jq` tool is used to format the JSON data properly. If it returns `jq: command not found`, then you can install `jq` with `sudo apt install -y jq`. After that, run the command again, and it should return the head state of the beacon chain. + ### View the status of a validator Shows the status of validator at index `1` at the `head` state. ```bash -curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" | jq +curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "accept: application/json" ``` ```json { + "execution_optimistic": false, + "finalized": false, "data": { "index": "1", - "balance": "63985937939", - "status": "Active", + "balance": "32004587169", + "status": "active_ongoing", "validator": { - "pubkey": "0x873e73ee8b3e4fcf1d2fb0f1036ba996ac9910b5b348f6438b5f8ef50857d4da9075d0218a9d1b99a9eae235a39703e1", - "withdrawal_credentials": "0x00b8cdcf79ba7e74300a07e9d8f8121dd0d8dd11dcfd6d3f2807c45b426ac968", + "pubkey": "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c", + "withdrawal_credentials": "0x01000000000000000000000015f4b914a0ccd14333d850ff311d6dafbfbaa32b", "effective_balance": "32000000000", "slashed": false, "activation_eligibility_epoch": "0", @@ -121,6 +124,7 @@ curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H " } } ``` +You can replace `1` in the above command with the validator index that you would like to query. Other API query can be done similarly by changing the link according to the Beacon API. ## Serving the HTTP API over TLS > **Warning**: This feature is currently experimental. @@ -147,9 +151,18 @@ openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 36 Note that currently Lighthouse only accepts keys that are not password protected. This means we need to run with the `-nodes` flag (short for 'no DES'). -Once generated, we can run Lighthouse: +Once generated, we can run Lighthouse and an execution node according to [Run a node](./run_a_node.md). In addition, add the flags `--http-enable-tls --http-tls-cert cert.pem --http-tls-key key.pem` to Lighthouse, the command should look like: + ```bash -lighthouse bn --http --http-enable-tls --http-tls-cert cert.pem --http-tls-key key.pem +lighthouse bn \ + --network mainnet \ + --execution-endpoint http://localhost:8551 \ + --execution-jwt /secrets/jwt.hex \ + --checkpoint-sync-url https://mainnet.checkpoint.sigp.io \ + --http \ + --http-enable-tls \ + --http-tls-cert cert.pem \ + --http-tls-key key.pem ``` Note that the user running Lighthouse must have permission to read the certificate and key. @@ -159,6 +172,7 @@ The API is now being served at `https://localhost:5052`. To test connectivity, you can run the following: ```bash curl -X GET "https://localhost:5052/eth/v1/node/version" -H "accept: application/json" --cacert cert.pem | jq + ``` ### Connecting a validator client In order to connect a validator client to a beacon node over TLS, the validator @@ -201,13 +215,13 @@ Ensure the `--http` flag has been supplied at the CLI. You can quickly check that the HTTP endpoint is up using `curl`: ```bash -curl -X GET "http://localhost:5052/eth/v1/node/version" -H "accept: application/json" | jq +curl -X GET "http://localhost:5052/eth/v1/node/version" -H "accept:application/json" ``` The beacon node should respond with its version: ```json -{"data":{"version":"Lighthouse/v0.2.9-6f7b4768a/x86_64-linux"}} +{"data":{"version":"Lighthouse/v4.1.0-693886b/x86_64-linux"} ``` If this doesn't work, the server might not be started or there might be a diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index 47fe62f50..7626d6401 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -1,8 +1,8 @@ # Lighthouse Non-Standard APIs Lighthouse fully supports the standardization efforts at -[github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs), -however sometimes development requires additional endpoints that shouldn't +[github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). +However, sometimes development requires additional endpoints that shouldn't necessarily be defined as a broad-reaching standard. Such endpoints are placed behind the `/lighthouse` path. @@ -16,10 +16,12 @@ Although we don't recommend that users rely on these endpoints, we document them briefly so they can be utilized by developers and researchers. + + ### `/lighthouse/health` +*Note: This endpoint is presently only available on Linux.* -*Presently only available on Linux.* - +Returns information regarding the health of the host machine. ```bash curl -X GET "http://localhost:5052/lighthouse/health" -H "accept: application/json" | jq ``` @@ -63,7 +65,7 @@ curl -X GET "http://localhost:5052/lighthouse/health" -H "accept: application/j ``` ### `/lighthouse/ui/health` - +Returns information regarding the health of the host machine. ```bash curl -X GET "http://localhost:5052/lighthouse/ui/health" -H "accept: application/json" | jq @@ -83,24 +85,24 @@ curl -X GET "http://localhost:5052/lighthouse/ui/health" -H "accept: applicatio "global_cpu_frequency": 3.4, "disk_bytes_total": 502390845440, "disk_bytes_free": 9981386752, - "network_name": "wlp0s20f3", - "network_bytes_total_received": 14105556611, - "network_bytes_total_transmit": 3649489389, - "nat_open": true, - "connected_peers": 80, - "sync_state": "Synced", "system_uptime": 660706, "app_uptime": 105, "system_name": "Arch Linux", "kernel_version": "5.19.13-arch1-1", "os_version": "Linux rolling Arch Linux", "host_name": "Computer1" + "network_name": "wlp0s20f3", + "network_bytes_total_received": 14105556611, + "network_bytes_total_transmit": 3649489389, + "nat_open": true, + "connected_peers": 80, + "sync_state": "Synced", } } ``` ### `/lighthouse/ui/validator_count` - +Returns an overview of validators. ```bash curl -X GET "http://localhost:5052/lighthouse/ui/validator_count" -H "accept: application/json" | jq ``` @@ -121,9 +123,9 @@ curl -X GET "http://localhost:5052/lighthouse/ui/validator_count" -H "accept: ap } ``` + ### `/lighthouse/ui/validator_metrics` -Re-exposes certain metrics from the validator monitor to the HTTP API. -Will only return metrics for the validators currently being monitored and are present in the POST data. +Re-exposes certain metrics from the validator monitor to the HTTP API. This API requires that the beacon node to have the flag `--validator-monitor-auto`. This API will only return metrics for the validators currently being monitored and present in the POST data, or the validators running in the validator client. ```bash curl -X POST "http://localhost:5052/lighthouse/ui/validator_metrics" -d '{"indices": [12345]}' -H "Content-Type: application/json" | jq ``` @@ -148,24 +150,40 @@ curl -X POST "http://localhost:5052/lighthouse/ui/validator_metrics" -d '{"indic } } ``` +Running this API without the flag `--validator-monitor-auto` in the beacon node will return null: +```json +{ + "data": { + "validators": {} + } +} +``` ### `/lighthouse/syncing` - +Returns the sync status of the beacon node. ```bash curl -X GET "http://localhost:5052/lighthouse/syncing" -H "accept: application/json" | jq ``` -```json -{ - "data": { - "SyncingFinalized": { - "start_slot": 3104, - "head_slot": 343744, - "head_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" +There are two possible outcomes, depending on whether the beacon node is syncing or synced. + +1. Syncing: + ```json + { + "data": { + "SyncingFinalized": { + "start_slot": "5478848", + "target_slot": "5478944" + } + } } - } -} -``` + ``` +1. Synced: + ```json + { + "data": "Synced" + } + ``` ### `/lighthouse/peers` @@ -173,96 +191,137 @@ curl -X GET "http://localhost:5052/lighthouse/syncing" -H "accept: application/ curl -X GET "http://localhost:5052/lighthouse/peers" -H "accept: application/json" | jq ``` + ```json [ { - "peer_id": "16Uiu2HAmA9xa11dtNv2z5fFbgF9hER3yq35qYNTPvN7TdAmvjqqv", + "peer_id": "16Uiu2HAm2ZoWQ2zkzsMFvf5o7nXa7R5F7H1WzZn2w7biU3afhgov", "peer_info": { - "_status": "Healthy", "score": { - "score": 0 + "Real": { + "lighthouse_score": 0, + "gossipsub_score": -18371.409037358582, + "ignore_negative_gossipsub_score": false, + "score": -21.816048231863316 + } }, "client": { "kind": "Lighthouse", - "version": "v0.2.9-1c9a055c", - "os_version": "aarch64-linux", - "protocol_version": "lighthouse/libp2p", - "agent_string": "Lighthouse/v0.2.9-1c9a055c/aarch64-linux" + "version": "v4.1.0-693886b", + "os_version": "x86_64-linux", + "protocol_version": "eth2/1.0.0", + "agent_string": "Lighthouse/v4.1.0-693886b/x86_64-linux" }, "connection_status": { "status": "disconnected", "connections_in": 0, "connections_out": 0, - "last_seen": 1082, + "last_seen": 9028, "banned_ips": [] }, "listening_addresses": [ - "/ip4/80.109.35.174/tcp/9000", - "/ip4/127.0.0.1/tcp/9000", - "/ip4/192.168.0.73/tcp/9000", - "/ip4/172.17.0.1/tcp/9000", - "/ip6/::1/tcp/9000" + "/ip4/212.102.59.173/tcp/23452", + "/ip4/23.124.84.197/tcp/23452", + "/ip4/127.0.0.1/tcp/23452", + "/ip4/192.168.0.2/tcp/23452", + "/ip4/192.168.122.1/tcp/23452" + ], + "seen_addresses": [ + "23.124.84.197:23452" ], "sync_status": { - "Advanced": { + "Synced": { "info": { - "status_head_slot": 343829, - "status_head_root": "0xe34e43efc2bb462d9f364bc90e1f7f0094e74310fd172af698b5a94193498871", - "status_finalized_epoch": 10742, - "status_finalized_root": "0x1b434b5ed702338df53eb5e3e24336a90373bb51f74b83af42840be7421dd2bf" + "head_slot": "5468141", + "head_root": "0x7acc017a199c0cf0693a19e0ed3a445a02165c03ea6f46cb5ffb8f60bf0ebf35", + "finalized_epoch": "170877", + "finalized_root": "0xbbc3541637976bd03b526de73e60a064e452a4b873b65f43fa91fefbba140410" } } }, "meta_data": { - "seq_number": 160, - "attnets": "0x0000000800000080" - } + "V2": { + "seq_number": 501, + "attnets": "0x0000020000000000", + "syncnets": "0x00" + } + }, + "subnets": [], + "is_trusted": false, + "connection_direction": "Outgoing", + "enr": "enr:-L64QI37ReMIki2Uqln3pcgQyAH8Y3ceSYrtJp1FlDEGSM37F7ngCpS9k-SKQ1bOHp0zFCkNxpvFlf_3o5OUkBRw0qyCAfqHYXR0bmV0c4gAAAIAAAAAAIRldGgykGKJQe8DABAg__________-CaWSCdjSCaXCEF3xUxYlzZWNwMjU2azGhAmoW921eIvf8pJhOvOwuxLSxKnpLY2inE_bUILdlZvhdiHN5bmNuZXRzAIN0Y3CCW5yDdWRwgluc" } } ] ``` ### `/lighthouse/peers/connected` - +Returns information about connected peers. ```bash curl -X GET "http://localhost:5052/lighthouse/peers/connected" -H "accept: application/json" | jq ``` + + ```json [ - { - "peer_id": "16Uiu2HAkzJC5TqDSKuLgVUsV4dWat9Hr8EjNZUb6nzFb61mrfqBv", + { + "peer_id": "16Uiu2HAmCAvpoYE6ABGdQJaW4iufVqNCTJU5AqzyZPB2D9qba7ZU", "peer_info": { - "_status": "Healthy", "score": { - "score": 0 + "Real": { + "lighthouse_score": 0, + "gossipsub_score": 0, + "ignore_negative_gossipsub_score": false, + "score": 0 + } }, "client": { "kind": "Lighthouse", - "version": "v0.2.8-87181204+", + "version": "v3.5.1-319cc61", "os_version": "x86_64-linux", - "protocol_version": "lighthouse/libp2p", - "agent_string": "Lighthouse/v0.2.8-87181204+/x86_64-linux" + "protocol_version": "eth2/1.0.0", + "agent_string": "Lighthouse/v3.5.1-319cc61/x86_64-linux" }, "connection_status": { "status": "connected", - "connections_in": 1, - "connections_out": 0, - "last_seen": 0, - "banned_ips": [] + "connections_in": 0, + "connections_out": 1, + "last_seen": 0 }, "listening_addresses": [ - "/ip4/34.204.178.218/tcp/9000", + "/ip4/144.91.92.17/tcp/9000", "/ip4/127.0.0.1/tcp/9000", - "/ip4/172.31.67.58/tcp/9000", - "/ip4/172.17.0.1/tcp/9000", - "/ip6/::1/tcp/9000" + "/ip4/172.19.0.3/tcp/9000" ], - "sync_status": "Unknown", + "seen_addresses": [ + "144.91.92.17:9000" + ], + "sync_status": { + "Synced": { + "info": { + "head_slot": "5468930", + "head_root": "0x25409073c65d2f6f5cee20ac2eff5ab980b576ca7053111456063f8ff8f67474", + "finalized_epoch": "170902", + "finalized_root": "0xab59473289e2f708341d8e5aafd544dd88e09d56015c90550ea8d16c50b4436f" + } + } + }, "meta_data": { - "seq_number": 1819, - "attnets": "0xffffffffffffffff" - } + "V2": { + "seq_number": 67, + "attnets": "0x0000000080000000", + "syncnets": "0x00" + } + }, + "subnets": [ + { + "Attestation": "39" + } + ], + "is_trusted": false, + "connection_direction": "Outgoing", + "enr": "enr:-Ly4QHd3RHJdkuR1iE6MtVtibC5S-aiWGPbwi4cG3wFGbqxRAkAgLDseTzPFQQIehQ7LmO7KIAZ5R1fotjMQ_LjA8n1Dh2F0dG5ldHOIAAAAAAAQAACEZXRoMpBiiUHvAwAQIP__________gmlkgnY0gmlwhJBbXBGJc2VjcDI1NmsxoQL4z8A7B-NS29zOgvkTX1YafKandwOtrqQ1XRnUJj3se4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA" } } ] @@ -297,7 +356,8 @@ 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_period_start_timestamp`. +`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. @@ -420,11 +480,11 @@ curl -X GET "http://localhost:5052/lighthouse/beacon/states/0/ssz" | jq ### `/lighthouse/liveness` POST request that checks if any of the given validators have attested in the given epoch. Returns a list -of objects, each including the validator index, epoch, and `is_live` status of a requested validator. +of objects, each including the validator index, epoch, and `is_live` status of a requested validator. -This endpoint is used in doppelganger detection, and will only provide accurate information for the -current, previous, or next epoch. +This endpoint is used in doppelganger detection, and can only provide accurate information for the current, previous, or next epoch. +> Note that for this API, if you insert an arbitrary epoch other than the previous, current or next epoch of the network, it will return `"code:400"` and `BAD_REQUEST`. ```bash curl -X POST "http://localhost:5052/lighthouse/liveness" -d '{"indices":["0","1"],"epoch":"1"}' -H "content-type: application/json" | jq @@ -442,6 +502,8 @@ curl -X POST "http://localhost:5052/lighthouse/liveness" -d '{"indices":["0","1" } ``` + + ### `/lighthouse/database/info` Information about the database's split point and anchor info. @@ -450,26 +512,29 @@ Information about the database's split point and anchor info. curl "http://localhost:5052/lighthouse/database/info" | jq ``` + ```json { - "schema_version": 5, + "schema_version": 16, "config": { - "slots_per_restore_point": 2048, + "slots_per_restore_point": 8192, + "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, "historic_state_cache_size": 1, "compact_on_init": false, - "compact_on_prune": true + "compact_on_prune": true, + "prune_payloads": true }, "split": { - "slot": "2034912", - "state_root": "0x11c8516aa7d4d1613e84121e3a557ceca34618b4c1a38f05b66ad045ff82b33b" + "slot": "5485952", + "state_root": "0xcfe5d41e6ab5a9dab0de00d89d97ae55ecaeed3b08e4acda836e69b2bef698b4" }, "anchor": { - "anchor_slot": "2034720", - "oldest_block_slot": "1958881", - "oldest_block_parent": "0x1fd3d855d03e9df28d8a41a0f9cb9d4c540832b3ca1c3e1d7e09cd75b874cc87", - "state_upper_limit": "2035712", - "state_lower_limit": "0" + "anchor_slot": "5414688", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "5414912", + "state_lower_limit": "8192" } } ``` @@ -504,12 +569,12 @@ Manually provide `SignedBeaconBlock`s to backfill the database. This is intended for use by Lighthouse developers during testing only. ### `/lighthouse/merge_readiness` - +Returns the current difficulty and terminal total difficulty of the network. Before [The Merge](https://ethereum.org/en/roadmap/merge/) on 15th September 2022, you will see that the current difficulty is less than the terminal total difficulty, An example is shown below: ```bash curl -X GET "http://localhost:5052/lighthouse/merge_readiness" | jq ``` -``` +```json { "data":{ "type":"ready", @@ -521,6 +586,21 @@ curl -X GET "http://localhost:5052/lighthouse/merge_readiness" | jq } ``` +As all testnets and Mainnet have been merged, both values will be the same after The Merge. An example of response on the Goerli testnet: + +```json +{ + "data": { + "type": "ready", + "config": { + "terminal_total_difficulty": "10790000" + }, + "current_difficulty": "10790000" + } +} +``` + + ### `/lighthouse/analysis/attestation_performance/{index}` Fetch information about the attestation performance of a validator index or all validators for a @@ -611,20 +691,35 @@ Two query parameters are required: Example: ```bash -curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=32" | jq +curl -X GET "http://localhost:5052/lighthouse/analysis/block_rewards?start_slot=1&end_slot=1" | jq ``` + +The first few lines of the response would look like: + ```json [ { - "block_root": "0x51576c2fcf0ab68d7d93c65e6828e620efbb391730511ffa35584d6c30e51410", - "attestation_rewards": { - "total": 4941156, + "total": 637260, + "block_root": "0x4a089c5e390bb98e66b27358f157df825128ea953cee9d191229c0bcf423a4f6", + "meta": { + "slot": "1", + "parent_slot": "0", + "proposer_index": 93, + "graffiti": "EF #vm-eth2-raw-iron-prater-101" }, - .. - }, - .. -] + "attestation_rewards": { + "total": 637260, + "prev_epoch_total": 0, + "curr_epoch_total": 637260, + "per_attestation_rewards": [ + { + "50102": 780, + } + ] + } + } +] ``` Caveats: @@ -653,6 +748,8 @@ Two query parameters are required: curl -X GET "http://localhost:5052/lighthouse/analysis/block_packing_efficiency?start_epoch=1&end_epoch=1" | jq ``` +An excerpt of the response looks like: + ```json [ { @@ -707,3 +804,16 @@ Should provide an output that emits log events as they occur: } } ``` + +### `/lighthouse/nat` +Checks if the ports are open. + +```bash +curl -X GET "http://localhost:5052/lighthouse/nat" | jq +``` + +An open port will return: +```json +{ + "data": true +} \ No newline at end of file diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index d5d76e4ef..ee0cfd200 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -17,8 +17,11 @@ HTTP Path | Description | [`POST /lighthouse/validators/mnemonic`](#post-lighthousevalidatorsmnemonic) | Create a new validator from an existing mnemonic. [`POST /lighthouse/validators/web3signer`](#post-lighthousevalidatorsweb3signer) | Add web3signer validators. +The query to Lighthouse API endpoints requires authorization, see [Authorization Header](./api-vc-auth-header.md). + In addition to the above endpoints Lighthouse also supports all of the [standard keymanager APIs](https://ethereum.github.io/keymanager-APIs/). + ## `GET /lighthouse/version` Returns the software version and `git` commit hash for the Lighthouse binary. @@ -32,15 +35,28 @@ Returns the software version and `git` commit hash for the Lighthouse binary. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/version" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body: + ```json { "data": { - "version": "Lighthouse/v0.2.11-fc0654fbe+/x86_64-linux" + "version": "Lighthouse/v4.1.0-693886b/x86_64-linux" } } ``` +> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/API-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. + +> As an alternative, you can also provide the API token directly, for example, `-H "Authorization: Bearer api-token-0x02dc2a13115cc8c83baf170f597f22b1eb2930542941ab902df3daadebcb8f8176`. In this case, you obtain the token from the file `API token.txt` and the command becomes: +```bash +curl -X GET "http://localhost:5062/lighthouse/version" -H "Authorization: Bearer api-token-0x02dc2a13115cc8c83baf170f597f22b1eb2930542941ab902df3daadebcb8f8176" | jq +``` ## `GET /lighthouse/health` @@ -57,24 +73,48 @@ Returns information regarding the health of the host machine. *Note: this endpoint is presently only available on Linux.* -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/health" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body: ```json { - "data": { - "pid": 1476293, - "pid_num_threads": 19, - "pid_mem_resident_set_size": 4009984, - "pid_mem_virtual_memory_size": 1306775552, - "sys_virt_mem_total": 33596100608, - "sys_virt_mem_available": 23073017856, - "sys_virt_mem_used": 9346957312, - "sys_virt_mem_free": 22410510336, - "sys_virt_mem_percent": 31.322334, - "sys_loadavg_1": 0.98, - "sys_loadavg_5": 0.98, - "sys_loadavg_15": 1.01 - } + "data": { + "sys_virt_mem_total": 8184274944, + "sys_virt_mem_available": 1532280832, + "sys_virt_mem_used": 6248341504, + "sys_virt_mem_free": 648790016, + "sys_virt_mem_percent": 81.27775, + "sys_virt_mem_cached": 1244770304, + "sys_virt_mem_buffers": 42373120, + "sys_loadavg_1": 2.33, + "sys_loadavg_5": 2.11, + "sys_loadavg_15": 2.47, + "cpu_cores": 4, + "cpu_threads": 8, + "system_seconds_total": 103095, + "user_seconds_total": 750734, + "iowait_seconds_total": 60671, + "idle_seconds_total": 3922305, + "cpu_time_total": 4794222, + "disk_node_bytes_total": 982820896768, + "disk_node_bytes_free": 521943703552, + "disk_node_reads_total": 376287830, + "disk_node_writes_total": 48232652, + "network_node_bytes_total_received": 143003442144, + "network_node_bytes_total_transmit": 185348289905, + "misc_node_boot_ts_seconds": 1681740973, + "misc_os": "linux", + "pid": 144072, + "pid_num_threads": 27, + "pid_mem_resident_set_size": 15835136, + "pid_mem_virtual_memory_size": 2179018752, + "pid_process_seconds_total": 54 + } } ``` @@ -91,7 +131,13 @@ Returns information regarding the health of the host machine. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/ui/health" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq + ``` + +Example Response Body ```json { @@ -130,7 +176,12 @@ Returns the graffiti that will be used for the next block proposal of each valid | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/ui/graffiti" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq + ``` +Example Response Body ```json { @@ -155,71 +206,115 @@ Returns the Ethereum proof-of-stake consensus specification loaded for this vali | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/spec" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body ```json { - "data": { - "CONFIG_NAME": "mainnet", - "MAX_COMMITTEES_PER_SLOT": "64", - "TARGET_COMMITTEE_SIZE": "128", - "MIN_PER_EPOCH_CHURN_LIMIT": "4", - "CHURN_LIMIT_QUOTIENT": "65536", - "SHUFFLE_ROUND_COUNT": "90", - "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "1024", - "MIN_GENESIS_TIME": "1601380800", - "GENESIS_DELAY": "172800", - "MIN_DEPOSIT_AMOUNT": "1000000000", - "MAX_EFFECTIVE_BALANCE": "32000000000", - "EJECTION_BALANCE": "16000000000", - "EFFECTIVE_BALANCE_INCREMENT": "1000000000", - "HYSTERESIS_QUOTIENT": "4", - "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", - "HYSTERESIS_UPWARD_MULTIPLIER": "5", - "PROPORTIONAL_SLASHING_MULTIPLIER": "3", - "GENESIS_FORK_VERSION": "0x00000002", - "BLS_WITHDRAWAL_PREFIX": "0x00", - "SECONDS_PER_SLOT": "12", - "MIN_ATTESTATION_INCLUSION_DELAY": "1", - "MIN_SEED_LOOKAHEAD": "1", - "MAX_SEED_LOOKAHEAD": "4", - "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", - "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", - "SHARD_COMMITTEE_PERIOD": "256", - "BASE_REWARD_FACTOR": "64", - "WHISTLEBLOWER_REWARD_QUOTIENT": "512", - "PROPOSER_REWARD_QUOTIENT": "8", - "INACTIVITY_PENALTY_QUOTIENT": "16777216", - "MIN_SLASHING_PENALTY_QUOTIENT": "32", - "SAFE_SLOTS_TO_UPDATE_JUSTIFIED": "8", - "DOMAIN_BEACON_PROPOSER": "0x00000000", - "DOMAIN_BEACON_ATTESTER": "0x01000000", - "DOMAIN_RANDAO": "0x02000000", - "DOMAIN_DEPOSIT": "0x03000000", - "DOMAIN_VOLUNTARY_EXIT": "0x04000000", - "DOMAIN_SELECTION_PROOF": "0x05000000", - "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", - "DOMAIN_APPLICATION_MASK": "0x00000001", - "MAX_VALIDATORS_PER_COMMITTEE": "2048", - "SLOTS_PER_EPOCH": "32", - "EPOCHS_PER_ETH1_VOTING_PERIOD": "32", - "SLOTS_PER_HISTORICAL_ROOT": "8192", - "EPOCHS_PER_HISTORICAL_VECTOR": "65536", - "EPOCHS_PER_SLASHINGS_VECTOR": "8192", - "HISTORICAL_ROOTS_LIMIT": "16777216", - "VALIDATOR_REGISTRY_LIMIT": "1099511627776", - "MAX_PROPOSER_SLASHINGS": "16", - "MAX_ATTESTER_SLASHINGS": "2", - "MAX_ATTESTATIONS": "128", - "MAX_DEPOSITS": "16", - "MAX_VOLUNTARY_EXITS": "16", - "ETH1_FOLLOW_DISTANCE": "1024", - "TARGET_AGGREGATORS_PER_COMMITTEE": "16", - "RANDOM_SUBNETS_PER_VALIDATOR": "1", - "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", - "SECONDS_PER_ETH1_BLOCK": "14", - "DEPOSIT_CONTRACT_ADDRESS": "0x48b597f4b53c21b48ad95c7256b49d1779bd5890" - } + "data": { + "CONFIG_NAME": "prater", + "PRESET_BASE": "mainnet", + "TERMINAL_TOTAL_DIFFICULTY": "10790000", + "TERMINAL_BLOCK_HASH": "0x0000000000000000000000000000000000000000000000000000000000000000", + "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615", + "SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY": "128", + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "16384", + "MIN_GENESIS_TIME": "1614588812", + "GENESIS_FORK_VERSION": "0x00001020", + "GENESIS_DELAY": "1919188", + "ALTAIR_FORK_VERSION": "0x01001020", + "ALTAIR_FORK_EPOCH": "36660", + "BELLATRIX_FORK_VERSION": "0x02001020", + "BELLATRIX_FORK_EPOCH": "112260", + "CAPELLA_FORK_VERSION": "0x03001020", + "CAPELLA_FORK_EPOCH": "162304", + "SECONDS_PER_SLOT": "12", + "SECONDS_PER_ETH1_BLOCK": "14", + "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", + "PROPOSER_SCORE_BOOST": "40", + "DEPOSIT_CHAIN_ID": "5", + "DEPOSIT_NETWORK_ID": "5", + "DEPOSIT_CONTRACT_ADDRESS": "0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b", + "MAX_COMMITTEES_PER_SLOT": "64", + "TARGET_COMMITTEE_SIZE": "128", + "MAX_VALIDATORS_PER_COMMITTEE": "2048", + "SHUFFLE_ROUND_COUNT": "90", + "HYSTERESIS_QUOTIENT": "4", + "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", + "HYSTERESIS_UPWARD_MULTIPLIER": "5", + "SAFE_SLOTS_TO_UPDATE_JUSTIFIED": "8", + "MIN_DEPOSIT_AMOUNT": "1000000000", + "MAX_EFFECTIVE_BALANCE": "32000000000", + "EFFECTIVE_BALANCE_INCREMENT": "1000000000", + "MIN_ATTESTATION_INCLUSION_DELAY": "1", + "SLOTS_PER_EPOCH": "32", + "MIN_SEED_LOOKAHEAD": "1", + "MAX_SEED_LOOKAHEAD": "4", + "EPOCHS_PER_ETH1_VOTING_PERIOD": "64", + "SLOTS_PER_HISTORICAL_ROOT": "8192", + "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", + "EPOCHS_PER_HISTORICAL_VECTOR": "65536", + "EPOCHS_PER_SLASHINGS_VECTOR": "8192", + "HISTORICAL_ROOTS_LIMIT": "16777216", + "VALIDATOR_REGISTRY_LIMIT": "1099511627776", + "BASE_REWARD_FACTOR": "64", + "WHISTLEBLOWER_REWARD_QUOTIENT": "512", + "PROPOSER_REWARD_QUOTIENT": "8", + "INACTIVITY_PENALTY_QUOTIENT": "67108864", + "MIN_SLASHING_PENALTY_QUOTIENT": "128", + "PROPORTIONAL_SLASHING_MULTIPLIER": "1", + "MAX_PROPOSER_SLASHINGS": "16", + "MAX_ATTESTER_SLASHINGS": "2", + "MAX_ATTESTATIONS": "128", + "MAX_DEPOSITS": "16", + "MAX_VOLUNTARY_EXITS": "16", + "INACTIVITY_PENALTY_QUOTIENT_ALTAIR": "50331648", + "MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR": "64", + "PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR": "2", + "SYNC_COMMITTEE_SIZE": "512", + "EPOCHS_PER_SYNC_COMMITTEE_PERIOD": "256", + "MIN_SYNC_COMMITTEE_PARTICIPANTS": "1", + "INACTIVITY_PENALTY_QUOTIENT_BELLATRIX": "16777216", + "MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX": "32", + "PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX": "3", + "MAX_BYTES_PER_TRANSACTION": "1073741824", + "MAX_TRANSACTIONS_PER_PAYLOAD": "1048576", + "BYTES_PER_LOGS_BLOOM": "256", + "MAX_EXTRA_DATA_BYTES": "32", + "MAX_BLS_TO_EXECUTION_CHANGES": "16", + "MAX_WITHDRAWALS_PER_PAYLOAD": "16", + "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP": "16384", + "DOMAIN_DEPOSIT": "0x03000000", + "BLS_WITHDRAWAL_PREFIX": "0x00", + "RANDOM_SUBNETS_PER_VALIDATOR": "1", + "DOMAIN_SYNC_COMMITTEE": "0x07000000", + "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", + "DOMAIN_BEACON_ATTESTER": "0x01000000", + "DOMAIN_VOLUNTARY_EXIT": "0x04000000", + "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": "0x08000000", + "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000", + "EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION": "256", + "TARGET_AGGREGATORS_PER_COMMITTEE": "16", + "DOMAIN_APPLICATION_MASK": "0x00000001", + "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "DOMAIN_RANDAO": "0x02000000", + "DOMAIN_SELECTION_PROOF": "0x05000000", + "DOMAIN_BEACON_PROPOSER": "0x00000000", + "SYNC_COMMITTEE_SUBNET_COUNT": "4" + } } ``` @@ -240,13 +335,13 @@ file may be read by a local user with access rights. | Required Headers | - | | Typical Responses | 200 | -### Example Path +Command: -``` -localhost:5062/lighthouse/auth +```bash +curl http://localhost:5062/lighthouse/auth | jq ``` -### Example Response Body +Example Response Body ```json { @@ -267,7 +362,14 @@ Lists all validators managed by this validator client. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200 | -### Example Response Body +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/validators/" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq +``` + +Example Response Body ```json { @@ -304,13 +406,14 @@ Get a validator by their `voting_pubkey`. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 400 | -### Example Path +Command: -``` -localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +```bash +DATADIR=/var/lib/lighthouse +curl -X GET "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" -H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" | jq ``` -### Example Response Body +Example Response Body ```json { @@ -323,7 +426,8 @@ localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc8 ## `PATCH /lighthouse/validators/:voting_pubkey` -Update some values for the validator with `voting_pubkey`. +Update some values for the validator with `voting_pubkey`. Possible fields: `enabled`, `gas_limit`, `builder_proposals`, +and `graffiti`. The following example updates a validator from `enabled: true` to `enabled: false`. ### HTTP Specification @@ -334,13 +438,8 @@ Update some values for the validator with `voting_pubkey`. | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 400 | -### Example Path -``` -localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde -``` - -### Example Request Body +Example Request Body ```json { @@ -348,12 +447,29 @@ localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc8 } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "{\"enabled\":false}" | jq +``` ### Example Response Body ```json null ``` +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will log: + +``` +INFO Disabled validator voting_pubkey: 0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde +INFO Modified key_cache saved successfully +``` + + ## `POST /lighthouse/validators/` Create any number of new validators, all of which will share a common mnemonic @@ -392,6 +508,28 @@ Validators are generated from the mnemonic according to ] ``` +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '[ + { + "enable": true, + "description": "validator_one", + "deposit_gwei": "32000000000", + "graffiti": "Mr F was here", + "suggested_fee_recipient": "0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d" + }, + { + "enable": false, + "description": "validator two", + "deposit_gwei": "34000000000" + } +]' | jq +``` + ### Example Response Body ```json @@ -416,6 +554,14 @@ Validators are generated from the mnemonic according to ] } } +``` + + `lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0x8ffbc881fb60841a4546b4b385ec5e9b5090fd1c4395e568d98b74b94b41a912c6101113da39d43c101369eeb9b48e50, signing_method: local_keystore +INFO Modified key_cache saved successfully +INFO Disabled validator voting_pubkey: 0xa9fadd620dc68e9fe0d6e1a69f6c54a0271ad65ab5a509e645e45c6e60ff8f4fc538f301781193a08b55821444801502 ``` ## `POST /lighthouse/validators/keystore` @@ -474,6 +620,19 @@ Import a keystore into the validator client. } ``` +We can use [JSON to String Converter](https://jsontostring.com/) so that the above data can be properly presented as a command. The command is as below: + +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/keystore \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "{\"enable\":true,\"password\":\"mypassword\",\"keystore\":{\"crypto\":{\"kdf\":{\"function\":\"scrypt\",\"params\":{\"dklen\":32,\"n\":262144,\"r\":8,\"p\":1,\"salt\":\"445989ec2f332bb6099605b4f1562c0df017488d8d7fb3709f99ebe31da94b49\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"abadc1285fd38b24a98ac586bda5b17a8f93fc1ff0778803dc32049578981236\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"65abb7e1d02eec9910d04299cc73efbe\"},\"message\":\"6b7931a4447be727a3bb5dc106d9f3c1ba50671648e522f213651d13450b6417\"}},\"uuid\":\"5cf2a1fb-dcd6-4095-9ebf-7e4ee0204cab\",\"path\":\"m/12381/3600/0/0/0\",\"pubkey\":\"b0d2f05014de27c6d7981e4a920799db1c512ee7922932be6bf55729039147cf35a090bd4ab378fe2d133c36cbbc9969\",\"version\":4,\"description\":\"\"}}" | jq +``` + +As this is an example for demonstration, the above command will return `InvalidPassword`. However, with a keystore file and correct password, running the above command will import the keystore to the validator client. An example of a success message is shown below: + ### Example Response Body ```json { @@ -484,6 +643,13 @@ Import a keystore into the validator client. } } +``` + + `lighthouse vc` will log: + +```bash +INFO Enabled validator voting_pubkey: 0xb0d2f05014de27c6d7981e4a920799db1c512ee7922932be6bf55729039147cf35a090bd4ab378fe2d133c36cbb, signing_method: local_keystore +INFO Modified key_cache saved successfully ``` ## `POST /lighthouse/validators/mnemonic` @@ -521,6 +687,16 @@ generated with the path `m/12381/3600/i/42`. } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/mnemonic \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{"mnemonic":" theme onion deal plastic claim silver fancy youth lock ordinary hotel elegant balance ridge web skill burger survey demand distance legal fish salad cloth","key_derivation_path_offset":0,"validators":[{"enable":true,"description":"validator_one","deposit_gwei":"32000000000"}]}' | jq +``` + ### Example Response Body ```json @@ -537,6 +713,13 @@ generated with the path `m/12381/3600/i/42`. } ``` +`lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380, signing_method: local_keystore +INFO Modified key_cache saved successfully +``` + ## `POST /lighthouse/validators/web3signer` Create any number of new validators, all of which will refer to a @@ -575,9 +758,29 @@ The following fields may be omitted or nullified to obtain default values: - `root_certificate_path` - `request_timeout_ms` +Command: +```bash +DATADIR=/var/lib/lighthouse +curl -X POST http://localhost:5062/lighthouse/validators/web3signer \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d "[{\"enable\":true,\"description\":\"validator_one\",\"graffiti\":\"Mr F was here\",\"suggested_fee_recipient\":\"0xa2e334e71511686bcfe38bb3ee1ad8f6babcc03d\",\"voting_public_key\":\"0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380\",\"url\":\"http://path-to-web3signer.com\",\"request_timeout_ms\":12000}]" +``` + + ### Example Response Body -*No data is included in the response body.* + +```json +null +``` + +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will log: + +``` +INFO Enabled validator voting_pubkey: 0xa062f95fee747144d5e511940624bc6546509eeaeae9383257a9c43e7ddc58c17c2bab4ae62053122184c381b90db380, signing_method: remote_signer +``` + ## `GET /lighthouse/logs` @@ -607,4 +810,4 @@ logs emitted are INFO level or higher. "total": 1 } } -``` \ No newline at end of file +``` diff --git a/book/src/api-vc.md b/book/src/api-vc.md index 74c493ebe..a3400016e 100644 --- a/book/src/api-vc.md +++ b/book/src/api-vc.md @@ -14,13 +14,13 @@ signers. It also includes some Lighthouse-specific endpoints which are described ## Starting the server -A Lighthouse validator client can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `127.0.0.1:5062`. +A Lighthouse validator client can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `http://127.0.0.1:5062`. The following CLI flags control the HTTP server: - `--http`: enable the HTTP server (required even if the following flags are provided). -- `--http-address`: specify the listen address of the server. It is almost always unsafe to use a non-default HTTP listen address. Use with caution. See the **Security** section below for more information. +- `--http-address`: specify the listen address of the server. It is almost always unsafe to use a non-default HTTP listen address. Use this with caution. See the **Security** section below for more information. - `--http-port`: specify the listen port of the server. - `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin` header. The default is to not supply a header. @@ -28,7 +28,7 @@ The following CLI flags control the HTTP server: ## Security The validator client HTTP server is **not encrypted** (i.e., it is **not HTTPS**). For -this reason, it will listen by default on `127.0.0.1`. +this reason, it will listen by default on `http://127.0.0.1`. It is unsafe to expose the validator client to the public Internet without additional transport layer security (e.g., HTTPS via nginx, SSH tunnels, etc.). diff --git a/book/src/builders.md b/book/src/builders.md index fc42f9b74..2be4841dd 100644 --- a/book/src/builders.md +++ b/book/src/builders.md @@ -1,4 +1,4 @@ -# MEV and Lighthouse +# Maximal Extractable Value (MEV) Lighthouse is able to interact with servers that implement the [builder API](https://github.com/ethereum/builder-specs), allowing it to produce blocks without having @@ -103,11 +103,32 @@ Each field is optional. } ``` +Command: + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{ + "builder_proposals": true, + "gas_limit": 30000001 +}' | jq +``` +If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"` + #### Example Response Body ```json null ``` + +A `null` response indicates that the request is successful. At the same time, `lighthouse vc` will show a log which looks like: + +``` +INFO Published validator registrations to the builder network, count: 3, service: preparation +``` + ### Fee Recipient Refer to [suggested fee recipient](suggested-fee-recipient.md) documentation. @@ -167,9 +188,20 @@ consider using it for the chance of out-sized rewards, this flag may be useful: The number provided indicates the minimum reward that an external payload must provide the proposer for it to be considered for inclusion in a proposal. For example, if you'd only like to use an external payload for a reward of >= 0.25 ETH, you would provide your beacon node with `--builder-profit-threshold 250000000000000000`. If it's your turn to propose and the -most valuable payload offered by builders is only 0.1 ETH, the local execution engine's payload will be used. Currently, -this threshold just looks at the value of the external payload. No comparison to the local payload is made, although -this feature will likely be added in the future. +most valuable payload offered by builders is only 0.1 ETH, the local execution engine's payload will be used. + +Since the [Capella](https://ethereum.org/en/history/#capella) upgrade, a comparison of the external payload and local payload will be made according to the [engine_getPayloadV2](https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#engine_getpayloadv2) API. The logic is as follows: + +``` +if local payload value >= builder payload value: + use local payload +else if builder payload value >= builder_profit_threshold or builder_profit_threshold == 0: + use builder payload +else: + use local payload +``` + +If you would like to always use the builder payload, you can add the flag `--always-prefer-builder-payload` to the beacon node. ## Checking your builder config diff --git a/book/src/checkpoint-sync.md b/book/src/checkpoint-sync.md index 47dc03b20..0c375a5f0 100644 --- a/book/src/checkpoint-sync.md +++ b/book/src/checkpoint-sync.md @@ -34,7 +34,7 @@ INFO Loaded checkpoint block and state state_root: 0xe8252c68784a8d5cc7e54 ``` > **Security Note**: You should cross-reference the `block_root` and `slot` of the loaded checkpoint -> against a trusted source like a friend's node, or a block explorer. +> against a trusted source like a friend's node, a block explorer or some [public endpoints](https://eth-clients.github.io/checkpoint-sync-endpoints/). Once the checkpoint is loaded Lighthouse will sync forwards to the head of the chain. @@ -48,6 +48,17 @@ The Ethereum community provides various [public endpoints](https://eth-clients.g lighthouse bn --checkpoint-sync-url https://example.com/ ... ``` +### Adjusting the timeout + +If the beacon node fails to start due to a timeout from the checkpoint sync server, you can try +running it again with a longer timeout by adding the flag `--checkpoint-sync-url-timeout`. + +``` +lighthouse bn --checkpoint-sync-url-timeout 300 --checkpoint-sync-url https://example.com/ ... +``` + +The flag takes a value in seconds. For more information see `lighthouse bn --help`. + ## Backfilling Blocks Once forwards sync completes, Lighthouse will commence a "backfill sync" to download the blocks @@ -62,6 +73,10 @@ INFO Downloading historical blocks est_time: 5 hrs 0 mins, speed: 111.96 slots/ Once backfill is complete, a `INFO Historical block download complete` log will be emitted. +> Note: Since [v4.1.0](https://github.com/sigp/lighthouse/releases/tag/v4.1.0), Lighthouse implements rate-limited backfilling to mitigate validator performance issues after a recent checkpoint sync. This means that the speed at which historical blocks are downloaded is limited, typically to less than 20 slots/sec. This will not affect validator performance. However, if you would still prefer to sync the chain as fast as possible, you can add the flag `--disable-backfill-rate-limiting` to the beacon node. + +> Note: Since [v4.2.0](https://github.com/sigp/lighthouse/releases/tag/v4.2.0), Lighthouse limits the backfill sync to only sync backwards to the weak subjectivity point (approximately 5 months). This will help to save disk space. However, if you would like to sync back to the genesis, you can add the flag `--genesis-backfill` to the beacon node. + ## FAQ 1. What if I have an existing database? How can I use checkpoint sync? @@ -101,8 +116,9 @@ states: database. Additionally, the genesis block is always available. * `state_lower_limit`: All states with slots _less than or equal to_ this value are available in the database. The minimum value is 0, indicating that the genesis state is always available. -* `state_upper_limit`: All states with slots _greater than or equal to_ this value are available - in the database. +* `state_upper_limit`: All states with slots _greater than or equal to_ `min(split.slot, + state_upper_limit)` are available in the database. In the case where the `state_upper_limit` is + higher than the `split.slot`, this means states are not being written to the freezer database. Reconstruction runs from the state lower limit to the upper limit, narrowing the window of unavailable states as it goes. It will log messages like the following to show its progress: @@ -138,18 +154,8 @@ To manually specify a checkpoint use the following two flags: * `--checkpoint-state`: accepts an SSZ-encoded `BeaconState` blob * `--checkpoint-block`: accepts an SSZ-encoded `SignedBeaconBlock` blob -_Both_ the state and block must be provided and **must** adhere to the [Alignment -Requirements](#alignment-requirements) described below. - -### Alignment Requirements - -* The block must be a finalized block from an epoch boundary, i.e. `block.slot() % 32 == 0`. -* The state must be the state corresponding to `block` with `state.slot() == block.slot()` - and `state.hash_tree_root() == block.state_root()`. - -These requirements are imposed to align with Lighthouse's database schema, and notably exclude -finalized blocks from skipped slots. You can avoid alignment issues by using -[Automatic Checkpoint Sync](#automatic-checkpoint-sync), which will search for a suitable block -and state pair. +_Both_ the state and block must be provided and the state **must** match the block. 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. [weak-subj]: https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/ diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index 5e0b89635..9b60ca2e1 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -29,6 +29,7 @@ validator client or the slasher**. | v3.4.0 | Jan 2023 | v13 | yes | | v3.5.0 | Feb 2023 | v15 | yes before Capella | | v4.0.1 | Mar 2023 | v16 | yes before Capella | +| v4.2.0 | May 2023 | v17 | yes | > **Note**: All point releases (e.g. v2.3.1) are schema-compatible with the prior minor release > (e.g. v2.3.0). @@ -82,24 +83,36 @@ on downgrades above. To check the schema version of a running Lighthouse instance you can use the HTTP API: ```bash -curl "http://localhost:5052/lighthouse/database/info" +curl "http://localhost:5052/lighthouse/database/info" | jq ``` ```json { - "schema_version": 8, + "schema_version": 16, "config": { "slots_per_restore_point": 8192, - "slots_per_restore_point_set_explicitly": true, + "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, "historic_state_cache_size": 1, "compact_on_init": false, - "compact_on_prune": true + "compact_on_prune": true, + "prune_payloads": true + }, + "split": { + "slot": "5485952", + "state_root": "0xcfe5d41e6ab5a9dab0de00d89d97ae55ecaeed3b08e4acda836e69b2bef698b4" + }, + "anchor": { + "anchor_slot": "5414688", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "5414912", + "state_lower_limit": "8192" } } ``` -The `schema_version` key indicates that this database is using schema version 8. +The `schema_version` key indicates that this database is using schema version 16. Alternatively, you can check the schema version with the `lighthouse db` command. @@ -118,7 +131,7 @@ Several conditions need to be met in order to run `lighthouse db`: 2. The command must run as the user that owns the beacon node database. If you are using systemd then your beacon node might run as a user called `lighthousebeacon`. 3. The `--datadir` flag must be set to the location of the Lighthouse data directory. -4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `prater` or `sepolia`. +4. The `--network` flag must be set to the correct network, e.g. `mainnet`, `goerli` or `sepolia`. The general form for a `lighthouse db` command is: diff --git a/book/src/developers.md b/book/src/developers.md new file mode 100644 index 000000000..2ba09bd34 --- /dev/null +++ b/book/src/developers.md @@ -0,0 +1,51 @@ +# For Protocol Developers + +_Documentation for protocol developers._ + +This section lists Lighthouse-specific decisions that are not strictly spec'd and may be useful for +other protocol developers wishing to interact with lighthouse. + + +## Custom ENR Fields + +Lighthouse currently uses the following ENR fields: + +### Ethereum Consensus Specified + +| Field | Description | +| ---- | ---- | +| `eth2` | The `ENRForkId` in SSZ bytes specifying which fork the node is on | +| `attnets` | An SSZ bitfield which indicates which of the 64 subnets the node is subscribed to for an extended period of time | +| `syncnets` | An SSZ bitfield which indicates which of the sync committee subnets the node is subscribed to | + + +### Lighthouse Custom Fields + +Lighthouse is currently using the following custom ENR fields. +| Field | Description | +| ---- | ---- | +| `quic` | The UDP port on which the QUIC transport is listening on IPv4 | +| `quic6` | The UDP port on which the QUIC transport is listening on IPv6 | + + +## Custom RPC Messages + +The specification leaves room for implementation-specific errors. Lighthouse uses the following +custom RPC error messages. + +### Goodbye Reason Codes + +| Code | Message | Description | +| ---- | ---- | ---- | +| 128 | Unable to Verify Network | Teku uses this, so we adopted it. It relates to having a fork mismatch | +| 129 | Too Many Peers | Lighthouse can close a connection because it has reached its peer-limit and pruned excess peers | +| 250 | Bad Score | The node has been dropped due to having a bad peer score | +| 251 | Banned | The peer has been banned and disconnected | +| 252 | Banned IP | The IP the node is connected to us with has been banned | + + +### Error Codes + +| Code | Message | Description | +| ---- | ---- | ---- | +| 139 | Rate Limited | The peer has been rate limited so we return this error as a response | \ No newline at end of file diff --git a/book/src/faq.md b/book/src/faq.md index b42e197a0..b3f3d55ed 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -1,29 +1,209 @@ # Frequently Asked Questions -- [Why does it take so long for a validator to be activated?](#why-does-it-take-so-long-for-a-validator-to-be-activated) -- [Do I need to set up any port mappings?](#do-i-need-to-set-up-any-port-mappings) -- [I have a low peer count and it is not increasing](#i-have-a-low-peer-count-and-it-is-not-increasing) -- [What should I do if I lose my slashing protection database?](#what-should-i-do-if-i-lose-my-slashing-protection-database) -- [How do I update lighthouse?](#how-do-i-update-lighthouse) -- [I can't compile lighthouse](#i-cant-compile-lighthouse) -- [What is "Syncing deposit contract block cache"?](#what-is-syncing-deposit-contract-block-cache) -- [Can I use redundancy in my staking setup?](#can-i-use-redundancy-in-my-staking-setup) -- [How can I monitor my validators?](#how-can-i-monitor-my-validators) -- [I see beacon logs showing `WARN: Execution engine called failed`, what should I do?](#i-see-beacon-logs-showing-warn-execution-engine-called-failed-what-should-i-do) -- [How do I check or update my withdrawal credentials?](#how-do-i-check-or-update-my-withdrawal-credentials) -- [I am missing attestations. Why?](#i-am-missing-attestations-why) -- [Sometimes I miss the attestation head vote, resulting in penalty. Is this normal?](#sometimes-i-miss-the-attestation-head-vote-resulting-in-penalty-is-this-normal) -- [My beacon node is stuck at downloading historical block using checkpoing sync. What can I do?](#my-beacon-node-is-stuck-at-downloading-historical-block-using-checkpoing-sync-what-can-i-do) +## [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) +- [My beacon node is stuck at downloading historical block using checkpoint sync. What should I do?](#bn-download-historical) +- [I proposed a block but the beacon node shows `could not publish message` with error `duplicate` as below, should I be worried?](#bn-duplicate) +- [I see beacon node logs `Head is optimistic` and I am missing attestations. What should I do?](#bn-optimistic) +- [My beacon node logs `CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout`, what should I do?](#bn-timeout) +- [My beacon node logs `WARN BlockProcessingFailure outcome: MissingBeaconBlock`, what should I do?](#bn-missing-beacon) +- [After checkpoint sync, the progress of `downloading historical blocks` is slow. Why?](#bn-download-slow) +- [My beacon node logs `WARN Error processing HTTP API request`, what should I do?](#bn-http) +- [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) -### Why does it take so long for a validator to be activated? +## [Validator](#validator-1) +- [Why does it take so long for a validator to be activated?](#vc-activation) +- [Can I use redundancy in my staking setup?](#vc-redundancy) +- [I am missing attestations. Why?](#vc-missed-attestations) +- [Sometimes I miss the attestation head vote, resulting in penalty. Is this normal?](#vc-head-vote) +- [Can I submit a voluntary exit message without a beacon node?](#vc-exit) +- [Does increasing the number of validators increase the CPU and other computer resources used?](#vc-resource) +- [I want to add new validators. Do I have to reimport the existing keys?](#vc-reimport) +- [Do I have to stop `lighthouse vc` the when importing new validator keys?](#vc-import) + + +## [Network, Monitoring and Maintenance](#network-monitoring-and-maintenance-1) +- [I have a low peer count and it is not increasing](#net-peer) +- [How do I update lighthouse?](#net-update) +- [Do I need to set up any port mappings (port forwarding)?](#net-port-forwarding) +- [How can I monitor my validators?](#net-monitor) +- [My beacon node and validator client are on different servers. How can I point the validator client to the beacon node?](#net-bn-vc) +- [Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address?](#net-ip) +- [How to change the TCP/UDP port 9000 that Lighthouse listens on?](#net-port) +- [Lighthouse `v4.3.0` introduces a change where a node will subscribe to only 2 subnets in total. I am worried that this will impact my validators return.](#net-subnet) + +## [Miscellaneous](#miscellaneous-1) +- [What should I do if I lose my slashing protection database?](#misc-slashing) +- [I can't compile lighthouse](#misc-compile) +- [How do I check the version of Lighthouse that is running?](#misc-version) +- [Does Lighthouse have pruning function like the execution client to save disk space?](#misc-prune) +- [Can I use a HDD for the freezer database and only have the hot db on SSD?](#misc-freezer) +- [Can Lighthouse log in local timestamp instead of UTC?](#misc-timestamp) + +## 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: + +``` +Nov 30 21:04:28.268 WARN Syncing deposit contract block cache est_blocks_remaining: initializing deposits, service: slot_notifier +``` + +or an error: + +``` +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: + +`error: Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Ipv4(127.0.0.1)), port: Some(8551), path: "/", query: None, fragment: None }, source: TimedOut }), service: exec` + +which says `TimedOut` at the end of the message. This means that the execution engine has not responded in time to the beacon node. One option is to add the flags `--execution-timeout-multiplier 3` and `--disable-lock-timeouts` to the beacon node. However, if the error persists, it is worth digging further to find out the cause. There are a few reasons why this can occur: +1. The execution engine is not synced. Check the log of the execution engine to make sure that it is synced. If it is syncing, wait until it is synced and the error will disappear. You will see the beacon node logs `INFO Execution engine online` when it is synced. +1. The computer is overloaded. Check the CPU and RAM usage to see if it has overloaded. You can use `htop` to check for CPU and RAM usage. +1. Your SSD is slow. Check if your SSD is in "The Bad" list [here](https://gist.github.com/yorickdowne/f3a3e79a573bf35767cd002cc977b038). If your SSD is in "The Bad" list, it means it cannot keep in sync to the network and you may want to consider upgrading to a better SSD. + +If the reason for the error message is caused by no. 1 above, you may want to look further. If the execution engine is out of sync suddenly, it is usually caused by ungraceful shutdown. The common causes for ungraceful shutdown are: +- Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. +- The service file is not stopped properly. To overcome this, make sure that the process is stopped properly, e.g., during client updates. +- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. When this occurs, the log file will show `Main process exited, code=killed, status=9/KILL`. You can also run `sudo journalctl -a --since "18 hours ago" | grep -i "killed process` to confirm that the execution client has been killed due to oom. If you are using geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. + +### My beacon node is stuck at downloading historical block using checkpoint sync. What should I do? + +After checkpoint forwards sync completes, the beacon node will start to download historical blocks. The log will look like: + +```bash +INFO Downloading historical blocks est_time: --, distance: 4524545 slots (89 weeks 5 days), service: slot_notifier +``` + +If the same log appears every minute and you do not see progress in downloading historical blocks, you can try one of the followings: + + - Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the port 9000 TCP/UDP to increase peer count. + - Restart the beacon node. + + +### I proposed a block but the beacon node shows `could not publish message` with error `duplicate` as below, should I be worried? + +``` +INFO Block from HTTP API already known` +WARN Could not publish message error: Duplicate, service: libp2p +``` + +This error usually happens when users are running mev-boost. The relay will publish the block on the network before returning it back to you. After the relay published the block on the network, it will propagate through nodes, and it happens quite often that your node will receive the block from your connected peers via gossip first, before getting the block from the relay, hence the message `duplicate`. + +In short, it is nothing to worry about. + +### I see beacon node logs `Head is optimistic`, and I am missing attestations. What should I do? + +The log looks like: + +``` +WARN Head is optimistic execution_block_hash: 0x47e7555f1d4215d1ad409b1ac188b008fcb286ed8f38d3a5e8078a0af6cbd6e1, info: chain not fully verified, block and attestation production disabled until execution engine syncs, service: slot_notifier +``` + +It means the beacon node will follow the chain, but it will not be able to attest or produce blocks. This is because the execution client is not synced, so the beacon chain cannot verify the authenticity of the chain head, hence the word `optimistic`. 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. + +### My beacon node logs `CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout, service: beacon`, what should I do? + +An example of the log is shown below: + +``` +CRIT Beacon block processing error error: ValidatorPubkeyCacheLockTimeout, service: beacon +WARN BlockProcessingFailure outcome: ValidatorPubkeyCacheLockTimeout, msg: unexpected condition in processing block. +``` + +A `Timeout` error suggests that the computer may be overloaded at the moment, for example, the execution client is still syncing. You may use the flag `--disable-lock-timeouts` to silence this error, although it will not fix the underlying slowness. Nevertheless, this is a relatively harmless log, and the error should go away once the resources used are back to normal. + +### My beacon node logs `WARN BlockProcessingFailure outcome: MissingBeaconBlock`, what should I do? + +An example of the full log is shown below: + +``` +WARN BlockProcessingFailure outcome: MissingBeaconBlock(0xbdba211f8d72029554e405d8e4906690dca807d1d7b1bc8c9b88d7970f1648bc), msg: unexpected condition in processing block. +``` + +`MissingBeaconBlock` suggests that the database has corrupted. You should wipe the database and use [Checkpoint Sync](./checkpoint-sync.md) to resync the beacon chain. + +### After checkpoint sync, the progress of `downloading historical blocks` is slow. Why? + +This is a normal behaviour. Since [v4.1.0](https://github.com/sigp/lighthouse/releases/tag/v4.1.0), Lighthouse implements rate-limited backfill sync to mitigate validator performance issues after a checkpoint sync. This is not something to worry about since backfill sync / historical data is not required for staking. However, if you opt to sync the chain as fast as possible, you can add the flag `--disable-backfill-rate-limiting` to the beacon node. + +### My beacon node logs `WARN Error processing HTTP API request`, what should I do? + +This warning usually comes with an http error code. Some examples are given below: + +1. The log shows: + +``` +WARN Error processing HTTP API request method: GET, path: /eth/v1/validator/attestation_data, status: 500 Internal Server Error, elapsed: 305.65µs +``` + +The error is `500 Internal Server Error`. This suggests that the execution client is not synced. Once the execution client is synced, the error will disappear. + +2. The log shows: + +``` +WARN Error processing HTTP API request method: POST, path: /eth/v1/validator/duties/attester/199565, status: 503 Service Unavailable, elapsed: 96.787µs +``` + +The error is `503 Service Unavailable`. This means that the beacon node is still syncing. When this happens, the validator client will log: + +``` +ERRO Failed to download attester duties err: FailedToDownloadAttesters("Some endpoints failed, num_failed: 2 http://localhost:5052/ => Unavailable(NotSynced), http://localhost:5052/ => RequestFailed(ServerMessage(ErrorMessage { code: 503, message: \"SERVICE_UNAVAILABLE: beacon node is syncing +``` + +This means that the validator client is sending requests to the beacon node. However, as the beacon node is still syncing, it is therefore unable to fulfil the request. The error will disappear once the beacon node is synced. + +### My beacon node logs `WARN Error signalling fork choice waiter`, what should I do? + +An example of the full log is shown below: + +``` +WARN Error signalling fork choice waiter slot: 6763073, error: ForkChoiceSignalOutOfOrder { current: Slot(6763074), latest: Slot(6763073) }, service: state_advance +``` + +This suggests that the computer resources are being overwhelmed. It could be due to high CPU usage or high disk I/O usage. This can happen, e.g., when the beacon node is downloading historical blocks, or when the execution client is syncing. The error will disappear when the resources used return to normal or when the node is synced. + + +### My beacon node logs `ERRO Aggregate attestation queue full`, what should I do? + +An example of the full log is shown below: +``` +ERRO Aggregate attestation queue full, queue_len: 4096, msg: the system has insufficient resources for load, module: network::beacon_processor:1542 +``` + +This suggests that the computer resources are being overwhelmed. It could be due to high CPU usage or high disk I/O usage. This can happen, e.g., when the beacon node is downloading historical blocks, or when the execution client is syncing. The error will disappear when the resources used return to normal or when the node is synced. + + +## Validator + +### Why does it take so long for a validator to be activated? After validators create their execution layer deposit transaction there are two waiting periods before they can start producing blocks and attestations: 1. Waiting for the beacon chain to recognise the execution layer block containing the - deposit (generally 4 to 7.4 hours). -1. Waiting in the queue for validator activation (generally 6.4 minutes for - every 4 validators in the queue). + deposit (generally takes ~13.6 hours). +1. Waiting in the queue for validator activation. Detailed answers below: @@ -32,33 +212,33 @@ Detailed answers below: Since the beacon chain uses the execution layer for validator on-boarding, beacon chain validators must listen to event logs from the deposit contract. Since the latest blocks of the execution chain are vulnerable to re-orgs due to minor network -partitions, beacon nodes follow the execution chain at a distance of 1,024 blocks -(~4 hours) (see -[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#misc)). +partitions, beacon nodes follow the execution chain at a distance of 2048 blocks +(~6.8 hours) (see +[`ETH1_FOLLOW_DISTANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#process-deposit)). This follow distance protects the beacon chain from on-boarding validators that are likely to be removed due to an execution chain re-org. -Now we know there's a 4 hours delay before the beacon nodes even _consider_ an +Now we know there's a 6.8 hours delay before the beacon nodes even _consider_ an execution layer block. Once they _are_ considering these blocks, there's a voting period where beacon validators vote on which execution block hash to include in the beacon chain. This -period is defined as 32 epochs (~3.4 hours, see -[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#time-parameters)). +period is defined as 64 epochs (~6.8 hours, see +[`ETH1_VOTING_PERIOD`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#time-parameters)). During this voting period, each beacon block producer includes an -[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#eth1data) +[`Eth1Data`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#eth1data) in their block which counts as a vote towards what that validator considers to be the head of the execution chain at the start of the voting period (with respect to `ETH1_FOLLOW_DISTANCE`, of course). You can see the exact voting logic -[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/validator.md#eth1-data). +[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#eth1-data). These two delays combined represent the time between an execution layer deposit being included in an execution data vote and that validator appearing in the beacon chain. -The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~4 hours and +The `ETH1_FOLLOW_DISTANCE` delay causes a minimum delay of ~6.8 hours and `ETH1_VOTING_PERIOD` means that if a validator deposit happens just _before_ the start of a new voting period then they might not notice this delay at all. However, if the validator deposit happens just _after_ the start of the new -voting period the validator might have to wait ~3.4 hours for next voting -period. In times of very, very severe network issues, the network may even fail -to vote in new execution layer blocks, stopping all new validator deposits! +voting period the validator might have to wait ~6.8 hours for next voting +period. In times of very severe network issues, the network may even fail +to vote in new execution layer blocks, thus stopping all new validator deposits and causing the wait to be longer. #### 2. Waiting for a validator to be activated @@ -68,30 +248,158 @@ They will simply be forgotten by the beacon chain! But, if those parameters were correct, once the execution layer delays have elapsed and the validator appears in the beacon chain, there's _another_ delay before the validator becomes "active" (canonical definition -[here](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. +[here](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#is_active_validator)) and can start producing blocks and attestations. Firstly, the validator won't become active until their beacon chain balance is equal to or greater than -[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#gwei-values) +[`MAX_EFFECTIVE_BALANCE`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#gwei-values) (32 ETH on mainnet, usually 3.2 ETH on testnets). Once this balance is reached, the validator must wait until the start of the next epoch (up to 6.4 minutes) for the -[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#registry-updates) +[`process_registry_updates`](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#registry-updates) routine to run. This routine activates validators with respect to a [churn -limit](https://github.com/ethereum/consensus-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_validator_churn_limit); +limit](https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/beacon-chain.md#get_validator_churn_limit); it will only allow the number of validators to increase (churn) by a certain -amount. Up until there are about 330,000 validators this churn limit is set to -4 and it starts to very slowly increase as the number of validators increases -from there. - -If a new validator isn't within the churn limit from the front of the queue, +amount. If a new validator isn't within the churn limit from the front of the queue, they will need to wait another epoch (6.4 minutes) for their next chance. This -repeats until the queue is cleared. +repeats until the queue is cleared. The churn limit is summarised in the table below: -Once a validator has been activated, there's no more waiting! It's time to +
+ +| Number of active validators | Validators activated per epoch | Validators activated per day | +|-------------------|--------------------------------------------|----| +| 327679 or less | 4 | 900 | +| 327680-393215 | 5 | 1125 | +| 393216-458751 | 6 | 1350 +| 458752-524287 | 7 | 1575 +| 524288-589823 | 8| 1800 | +| 589824-655359 | 9| 2025 | +| 655360-720895 | 10 | 2250| +| 720896-786431 | 11 | 2475 | +| 786432-851967 | 12 | 2700 | +| 851968-917503 | 13 | 2925 | +| 917504-983039 | 14 | 3150 | +| 983040-1048575 | 15 | 3375 | + +
+ +For example, the number of active validators on Mainnet is about 574000 on May 2023. This means that 8 validators can be activated per epoch or 1800 per day (it is noted that the same applies to the exit queue). If, for example, there are 9000 validators waiting to be activated, this means that the waiting time can take up to 5 days. + +Once a validator has been activated, congratulations! It's time to produce blocks and attestations! -### Do I need to set up any port mappings? +### Can I use redundancy in my staking setup? + +You should **never** use duplicate/redundant validator keypairs or validator clients (i.e., don't +duplicate your JSON keystores and don't run `lighthouse vc` twice). This will lead to slashing. + +However, there are some components which can be configured with redundancy. See the +[Redundancy](./redundancy.md) guide for more information. + +### I am missing attestations. Why? +The first thing is to ensure both consensus and execution clients are synced with the network. If they are synced, there may still be some issues with the node setup itself that is causing the missed attestations. Check the setup to ensure that: +- the clock is synced +- the computer has sufficient resources and is not overloaded +- 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). + +Another cause for missing attestations is delays during block processing. When this happens, the debug logs will show (debug logs can be found under `$datadir/beacon/logs`): + +``` +DEBG Delayed head block set_as_head_delay: Some(93.579425ms), imported_delay: Some(1.460405278s), observed_delay: Some(2.540811921s), block_delay: 4.094796624s, slot: 6837344, proposer_index: 211108, block_root: 0x2c52231c0a5a117401f5231585de8aa5dd963bc7cbc00c544e681342eedd1700, service: beacon +``` + +The fields to look for are `imported_delay > 1s` and `observed_delay < 3s`. The `imported_delay` is how long the node took to process the block. The `imported_delay` of larger than 1 second suggests that there is slowness in processing the block. It could be due to high CPU usage, high I/O disk usage or the clients are doing some background maintenance processes. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). The `observed_delay` of less than 3 seconds means that the block is not arriving late from the block proposer. Combining the above, this implies that the validator should have been able to attest to the block, but failed due to slowness in the node processing the block. + + +### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? + +In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone elses performance. + +You could also check for the sync aggregate participation percentage on block explorers such as [beaconcha.in](https://beaconcha.in/). A low sync aggregate participation percentage (e.g., 60-70%) indicates that the block that you are assigned to attest to may be published late. As a result, your validator fails to correctly attest to the block. + +Another possible reason for missing the head vote is due to a chain "reorg". A reorg can happen if the proposer publishes block `n` late, and the proposer of block `n+1` builds upon block `n-1` instead of `n`. This is called a "reorg". Due to the reorg, block `n` was never included in the chain. If you are assigned to attest at slot `n`, it is possible you may still attest to block `n` despite most of the network recognizing the block as being late. In this case you will miss the head reward. + + +### Can I submit a voluntary exit message without running a beacon node? + +Yes. Beaconcha.in provides the tool to broadcast the message. You can create the voluntary exit message file with [ethdo](https://github.com/wealdtech/ethdo/releases/tag/v1.30.0) and submit the message via the [beaconcha.in](https://beaconcha.in/tools/broadcast) website. A guide on how to use `ethdo` to perform voluntary exit can be found [here](https://github.com/eth-educators/ethstaker-guides/blob/main/voluntary-exit.md). + +It is also noted that you can submit your BLS-to-execution-change message to update your withdrawal credentials from type `0x00` to `0x01` using the same link. + +If you would like to still use Lighthouse to submit the message, you will need to run a beacon node and an execution client. For the beacon node, you can use checkpoint sync to quickly sync the chain under a minute. On the other hand, the execution client can be syncing and *needs not be synced*. This implies that it is possible to broadcast a voluntary exit message within a short time by quickly spinning up a node. + +### Does increasing the number of validators increase the CPU and other computer resources used? + +A computer with hardware specifications stated in the [Recommended System Requirements](./installation.md#recommended-system-requirements) can run hundreds validators with only marginal increase in cpu usage. When validators are active, there is a bit of an increase in resources used from validators 0-64, because you end up subscribed to more subnets. After that, the increase in resources plateaus when the number of validators go from 64 to ~500. + +### I want to add new validators. Do I have to reimport the existing keys? + +No. You can just import new validator keys to the destination directory. If the `validator_keys` folder contains existing keys, that's fine as well because Lighthouse will skip importing existing keys. + +### Do I have to stop `lighthouse vc` when importing new validator keys? + +Generally yes. + +If you do not want to stop `lighthouse vc`, you can use the [key manager API](./api-vc-endpoints.md) to import keys. + +## Network, Monitoring and Maintenance + +### I have a low peer count and it is not increasing + +If you cannot find *ANY* peers at all, it is likely that you have incorrect +network configuration settings. Ensure that the network you wish to connect to +is correct (the beacon node outputs the network it is connecting to in the +initial boot-up log lines). On top of this, ensure that you are not using the +same `datadir` as a previous network, i.e., if you have been running the +`Goerli` testnet and are now trying to join a new network but using the same +`datadir` (the `datadir` is also printed out in the beacon node's logs on +boot-up). + +If you find yourself with a low peer count and it's not reaching the target you +expect, there are a few things to check on: + +1. Ensure that port forward was correctly set up as described [here](./advanced_networking.md#nat-traversal-port-forwarding). + +To check that the ports are forwarded, run the command: + + ```bash + curl http://localhost:5052/lighthouse/nat + ``` + +It should return `{"data":true}`. If it returns `{"data":false}`, you may want to double check if the port forward was correctly set up. + +If the ports are open, you should have incoming peers. To check that you have incoming peers, run the command: + + ```bash + curl localhost:5052/lighthouse/peers | jq '.[] | select(.peer_info.connection_direction=="Incoming")' + ``` + +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. + +2. Check that you do not lower the number of peers using the flag `--target-peers`. The default is 80. 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. + +3. 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. + + +### How do I update lighthouse? + +If you are updating to new release binaries, it will be the same process as described [here.](./installation-binaries.md) + +If you are updating by rebuilding from source, see [here.](./installation-source.md#update-lighthouse) + +If you are running the docker image provided by Sigma Prime on Dockerhub, you can update to specific versions, for example: + +```bash +$ docker pull sigp/lighthouse:v1.0.0 +``` + +If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) +You just need to make sure the code you have checked out is up to date. + +### Do I need to set up any port mappings (port forwarding)? It is not strictly required to open any ports for Lighthouse to connect and participate in the network. Lighthouse should work out-of-the-box. However, if @@ -116,121 +424,129 @@ peers to join and degrades the overall connectivity of the global network. For these reasons, we recommend that you make your node publicly accessible. Lighthouse supports UPnP. If you are behind a NAT with a router that supports -UPnP you can simply ensure UPnP is enabled (Lighthouse will inform you in its -initial logs if a route has been established). You can also manually set up -port mappings in your router to your local Lighthouse instance. By default, +UPnP, you can simply ensure UPnP is enabled (Lighthouse will inform you in its +initial logs if a route has been established). You can also manually [set up port mappings/port forwarding](./advanced_networking.md#how-to-open-ports) in your router to your local Lighthouse instance. By default, Lighthouse uses port 9000 for both TCP and UDP. Opening both these ports will make your Lighthouse node maximally contactable. -### I have a low peer count and it is not increasing - -If you cannot find *ANY* peers at all. It is likely that you have incorrect -testnet configuration settings. Ensure that the network you wish to connect to -is correct (the beacon node outputs the network it is connecting to in the -initial boot-up log lines). On top of this, ensure that you are not using the -same `datadir` as a previous network. I.e if you have been running the -`prater` testnet and are now trying to join a new testnet but using the same -`datadir` (the `datadir` is also printed out in the beacon node's logs on -boot-up). - -If you find yourself with a low peer count and it's not reaching the target you -expect. Try setting up the correct port forwards as described -[here](./advanced_networking.md#nat-traversal-port-forwarding). - -### What should I do if I lose my slashing protection database? - -See [here](./slashing-protection.md#misplaced-slashing-database). - -### How do I update lighthouse? - -If you are updating to new release binaries, it will be the same process as described [here.](./installation-binaries.md) - -If you are updating by rebuilding from source, see [here.](./installation-source.md#update-lighthouse) - -If you are running the docker image provided by Sigma Prime on Dockerhub, you can update to specific versions, for example: - -```bash -$ docker pull sigp/lighthouse:v1.0.0 -``` - -If you are building a docker image, the process will be similar to the one described [here.](./docker.md#building-the-docker-image) -You will just also need to make sure the code you have checked out is up to date. - -### I can't compile lighthouse - -See [here.](./installation-source.md#troubleshooting) - -### What is "Syncing deposit contract block cache"? - -``` -Nov 30 21:04:28.268 WARN Syncing deposit contract block cache est_blocks_remaining: initializing deposits, service: slot_notifier -``` - -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 sporadically during operation, there may be an -issue with your execution client endpoint. - -### Can I use redundancy in my staking setup? - -You should **never** use duplicate/redundant validator keypairs or validator clients (i.e., don't -duplicate your JSON keystores and don't run `lighthouse vc` twice). This will lead to slashing. - -However, there are some components which can be configured with redundancy. See the -[Redundancy](./redundancy.md) guide for more information. - -### How can I monitor my validators? +### How can I monitor my validators? Apart from using block explorers, you may use the "Validator Monitor" built into Lighthouse which provides logging and Prometheus/Grafana metrics for individual validators. See [Validator Monitoring](./validator-monitoring.md) for more information. Lighthouse has also developed Lighthouse UI (Siren) to monitor performance, see [Lighthouse UI (Siren)](./lighthouse-ui.md). -### I see beacon logs showing `WARN: Execution engine called failed`, what should I do? +### My beacon node and validator client are on different servers. How can I point the validator client to the beacon node? + +The setting on the beacon node is the same for both cases below. In the beacon node, specify `lighthouse bn --http-address local_IP` so that the beacon node is listening on the local network rather than `localhost`. You can find the `local_IP` by running the command `hostname -I | awk '{print $1}'` on the server running the beacon node. + +1. If the beacon node and validator clients are on different servers *in the same network*, the setting in the validator client is as follows: + + Use the flag `--beacon-nodes` to point to the beacon node. For example, `lighthouse vc --beacon-nodes http://local_IP:5052` where `local_IP` is the local IP address of the beacon node and `5052` is the default `http-port` of the beacon node. + + If you have firewall setup, e.g., `ufw`, you will need to allow port 5052 (assuming that the default port is used) with `sudo ufw allow 5052`. Note: this will allow all IP addresses to access the HTTP API of the beacon node. If you are on an untrusted network (e.g., a university or public WiFi) or the host is exposed to the internet, use apply IP-address filtering as described later in this section. + + You can test that the setup is working with by running the following command on the validator client host: + + ```bash + curl "http://local_IP:5052/eth/v1/node/version" + ``` + + You can refer to [Redundancy](./redundancy.md) for more information. + +2. If the beacon node and validator clients are on different servers *and different networks*, it is necessary to perform port forwarding of the SSH port (e.g., the default port 22) on the router, and also allow firewall on the SSH port. The connection can be established via port forwarding on the router. + + + + In the validator client, use the flag `--beacon-nodes` to point to the beacon node. However, since the beacon node and the validator client are on different networks, the IP address to use is the public IP address of the beacon node, i.e., `lighthouse vc --beacon-nodes http://public_IP:5052`. You can get the public IP address of the beacon node by running the command ` dig +short myip.opendns.com @resolver1.opendns.com` on the server running the beacon node. + + Additionally, port forwarding of port 5052 on the router connected to the beacon node is required for the vc to connect to the bn. To do port forwarding, refer to [how to open ports](./advanced_networking.md#how-to-open-ports). + + + If you have firewall setup, e.g., `ufw`, you will need to allow connections to port 5052 (assuming that the default port is used). Since the beacon node HTTP/HTTPS API is public-facing (i.e., the 5052 port is now exposed to the internet due to port forwarding), we strongly recommend users to apply IP-address filtering to the BN/VC connection from malicious actors. This can be done using the command: + + ``` + sudo ufw allow from vc_IP_address proto tcp to any port 5052 + ``` + where `vc_IP_address` is the public IP address of the validator client. The command will only allow connections to the beacon node from the validator client IP address to prevent malicious attacks on the beacon node over the internet. + + +It is also worth noting that the `--beacon-nodes` flag can also be used for redundancy of beacon nodes. For example, let's say you have a beacon node and a validator client running on the same host, and a second beacon node on another server as a backup. In this case, you can use `lighthouse vc --beacon-nodes http://localhost:5052, http://IP-address:5052` on the validator client. + +### Should I do anything to the beacon node or validator client settings if I have a relocation of the node / change of IP address? +No. Lighthouse will auto-detect the change and update your Ethereum Node Record (ENR). You just need to make sure you are not manually setting the ENR with `--enr-address` (which, for common use cases, this flag is not used). + +### How to change the TCP/UDP port 9000 that Lighthouse listens on? +Use the flag ```--port ``` in the beacon node. This flag can be useful when you are running two beacon nodes at the same time. You can leave one beacon node as the default port 9000, and configure the second beacon node to listen on, e.g., ```--port 9001```. + +### Lighthouse `v4.3.0` introduces a change where a node will subscribe to only 2 subnets in total. I am worried that this will impact my validators return. + +Previously, having more validators means subscribing to more subnets. Since the change, a node will now only subscribe to 2 subnets in total. This will bring about significant reductions in bandwidth for nodes with multiple validators. + +While subscribing to more subnets can ensure you have peers on a wider range of subnets, these subscriptions consume resources and bandwidth. This does not significantly increase the performance of the node, however it does benefit other nodes on the network. + +If you would still like to subscribe to all subnets, you can use the flag `subscribe-all-subnets`. This may improve the block rewards by 1-5%, though it comes at the cost of a much higher bandwidth requirement. + +## Miscellaneous + +### What should I do if I lose my slashing protection database? + +See [here](./slashing-protection.md#misplaced-slashing-database). + +### I can't compile lighthouse + +See [here.](./installation-source.md#troubleshooting) + +### How do I check the version of Lighthouse that is running? + +If you build Lighthouse from source, run `lighthouse --version`. Example of output: + +```bash +Lighthouse v4.1.0-693886b +BLS library: blst-modern +SHA256 hardware acceleration: false +Allocator: jemalloc +Specs: mainnet (true), minimal (false), gnosis (true) +``` + +If you download the binary file, navigate to the location of the directory, for example, the binary file is in `/usr/local/bin`, run `/usr/local/bin/lighthouse --version`, the example of output is the same as above. + +Alternatively, if you have Lighthouse running, on the same computer, you can run: +```bash +curl "http://127.0.0.1:5052/eth/v1/node/version" +``` + +Example of output: +```bash +{"data":{"version":"Lighthouse/v4.1.0-693886b/x86_64-linux"}} +``` +which says that the version is v4.1.0. + +### Does Lighthouse have pruning function like the execution client to save disk space? + +There is no pruning of Lighthouse database for now. However, since v4.2.0, a feature to only sync back to the weak subjectivity point (approximately 5 months) when syncing via a checkpoint sync was added. This will help to save disk space since the previous behaviour will sync back to the genesis by default. + +### Can I use a HDD for the freezer database and only have the hot db on SSD? + +Yes, you can do so by using the flag `--freezer-dir /path/to/freezer_db` in the beacon node. + +### Can Lighthouse log in local timestamp instead of UTC? + +The reason why Lighthouse logs in UTC is due to the dependency on an upstream library that is [yet to be resolved](https://github.com/sigp/lighthouse/issues/3130). Alternatively, using the flag `disable-log-timestamp` in combination with systemd will suppress the UTC timestamps and print the logs in local timestamps. + + + + + + -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: -`error: Reqwest(reqwest::Error { kind: Request, url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Ipv4(127.0.0.1)), port: Some(8551), path: "/", query: None, fragment: None }, source: TimedOut }), service: exec` -which says `TimedOut` at the end of the message. This means that the execution engine has not responded in time to the beacon node. There are a few reasons why this can occur: -1. The execution engine is not synced. Check the log of the execution engine to make sure that it is synced. If it is syncing, wait until it is synced and the error will disappear. You will see the beacon node logs `INFO Execution engine online` when it is synced. -1. The computer is overloaded. Check the CPU and RAM usage to see if it has overloaded. You can use `htop` to check for CPU and RAM usage. -1. Your SSD is slow. Check if your SSD is in "The Bad" list [here](https://gist.github.com/yorickdowne/f3a3e79a573bf35767cd002cc977b038). If your SSD is in "The Bad" list, it means it cannot keep in sync to the network and you may want to consider upgrading to a better SSD. -If the reason for the error message is caused by no. 1 above, you may want to look further. If the execution engine is out of sync suddenly, it is usually caused by ungraceful shutdown. The common causes for ungraceful shutdown are: -- Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. -- The service file is not stopped properly. To overcome this, make sure that the process is stop properly, e.g., during client updates. -- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. When this occurs, the log file will show `Main process exited, code=killed, status=9/KILL`. You can also run `sudo journalctl -a --since "18 hours ago" | grep -i "killed process` to confirm that the execution client has been killed due to oom. If you are using geth as the execution client, a short term solution is to reduce the resources used, for example: (1) reduce the cache by adding the flag `--cache 2048` (2) connect to less peers using the flag `--maxpeers 10`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. -### How do I check or update my withdrawal credentials? -Withdrawals will be available after the Capella/Shanghai upgrades on 12th April 2023. To check that if you are eligible for withdrawals, go to [Staking launchpad](https://launchpad.ethereum.org/en/withdrawals), enter your validator index and click `verify on mainnet`: -- `withdrawals enabled` means you will automatically receive withdrawals to the withdrawal address that you set. -- `withdrawals not enabled` means you will need to update your withdrawal credentials from `0x00` type to `0x01` type. 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). -For the case of `withdrawals not enabled`, you can update your withdrawal credentials **anytime**, and there is no deadline for that. The catch is that as long as you do not update your withdrawal credentials, your rewards in the beacon chain will continue to be locked in the beacon chain. Only after you update the withdrawal credentials, will the rewards be withdrawn to the withdrawal address. -### I am missing attestations. Why? -The first thing is to ensure both consensus and execution clients are synced with the network. If they are synced, there may still be some issues with the node setup itself that is causing the missed attestations. Check the setup to ensure that: -- the clock is synced -- the computer has sufficient resources and is not overloaded -- 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). Once the above points are good, missing attestation should be a rare occurance. - -### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? - -In general it is unavoiadable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone else's performance. -### My beacon node is stuck at downloading historical block using checkpoing sync. What can I do? -Check the number of peers you are connected to. If you have low peers (less than 50), try to do port forwarding on the port 9000 TCP/UDP to increase peer count. \ No newline at end of file diff --git a/book/src/graffiti.md b/book/src/graffiti.md index 75c2a86dd..302f8f967 100644 --- a/book/src/graffiti.md +++ b/book/src/graffiti.md @@ -29,6 +29,8 @@ Lighthouse will first search for the graffiti corresponding to the public key of ### 2. Setting the graffiti in the `validator_definitions.yml` Users can set validator specific graffitis in `validator_definitions.yml` with the `graffiti` key. This option is recommended for static setups where the graffitis won't change on every new block proposal. +You can also update the graffitis in the `validator_definitions.yml` file using the [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey). See example in [Set Graffiti via HTTP](#set-graffiti-via-http). + Below is an example of the validator_definitions.yml with validator specific graffitis: ``` --- @@ -62,3 +64,25 @@ Usage: `lighthouse bn --graffiti fortytwo` > 3. If graffiti is not specified in `validator_definitions.yml`, load the graffiti passed in the `--graffiti` flag on the validator client. > 4. If the `--graffiti` flag on the validator client is not passed, load the graffiti passed in the `--graffiti` flag on the beacon node. > 4. If the `--graffiti` flag is not passed, load the default Lighthouse graffiti. + +### Set Graffiti via HTTP + +Use the [Lighthouse API](api-vc-endpoints.md) to set graffiti on a per-validator basis. This method updates the graffiti +both in memory and in the `validator_definitions.yml` file. The new graffiti will be used in the next block proposal +without requiring a validator client restart. + +Refer to [Lighthouse API](api-vc-endpoints.html#patch-lighthousevalidatorsvoting_pubkey) for API specification. + +#### Example Command + +```bash +DATADIR=/var/lib/lighthouse +curl -X PATCH "http://localhost:5062/lighthouse/validators/0xb0148e6348264131bf47bcd1829590e870c836dc893050fd0dadc7a28949f9d0a72f2805d027521b45441101f0cc1cde" \ +-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)" \ +-H "Content-Type: application/json" \ +-d '{ + "graffiti": "Mr F was here" +}' | jq +``` + +A `null` response indicates that the request is successful. \ No newline at end of file diff --git a/book/src/imgs/full-withdrawal.png b/book/src/imgs/full-withdrawal.png index 6fa2db6a9..c16d83526 100644 Binary files a/book/src/imgs/full-withdrawal.png and b/book/src/imgs/full-withdrawal.png differ diff --git a/book/src/imgs/partial-withdrawal.png b/book/src/imgs/partial-withdrawal.png index 0bf90b91d..5d318b4e6 100644 Binary files a/book/src/imgs/partial-withdrawal.png and b/book/src/imgs/partial-withdrawal.png differ diff --git a/book/src/imgs/ui-dash-logs.png b/book/src/imgs/ui-dash-logs.png new file mode 100644 index 000000000..3656ed5b2 Binary files /dev/null and b/book/src/imgs/ui-dash-logs.png differ diff --git a/book/src/imgs/ui-logs.png b/book/src/imgs/ui-logs.png new file mode 100644 index 000000000..2a5c230ab Binary files /dev/null and b/book/src/imgs/ui-logs.png differ diff --git a/book/src/installation-source.md b/book/src/installation-source.md index b9c9df163..58e6917ec 100644 --- a/book/src/installation-source.md +++ b/book/src/installation-source.md @@ -5,7 +5,7 @@ the instructions below, and then proceed to [Building Lighthouse](#build-lightho ## Dependencies -First, **install Rust** using [rustup](https://rustup.rs/): +First, **install Rust** using [rustup](https://rustup.rs/): ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -28,7 +28,7 @@ operating system. Install the following packages: ```bash -sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang protobuf-compiler +sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang ``` > Tips: @@ -51,10 +51,6 @@ After this, you are ready to [build Lighthouse](#build-lighthouse). brew install cmake ``` -1. Install protoc using Homebrew: -``` -brew install protobuf -``` [Homebrew]: https://brew.sh/ @@ -64,14 +60,14 @@ After this, you are ready to [build Lighthouse](#build-lighthouse). 1. Install [Git](https://git-scm.com/download/win). 1. Install the [Chocolatey](https://chocolatey.org/install) package manager for Windows. - > Tips: + > Tips: > - Use PowerShell to install. In Windows, search for PowerShell and run as administrator. > - You must ensure `Get-ExecutionPolicy` is not Restricted. To test this, run `Get-ExecutionPolicy` in PowerShell. If it returns `restricted`, then run `Set-ExecutionPolicy AllSigned`, and then run - ```bash + ```bash Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) ``` > - To verify that Chocolatey is ready, run `choco` and it should return the version. -1. Install Make, CMake, LLVM and protoc using Chocolatey: +1. Install Make, CMake and LLVM using Chocolatey: ``` choco install make @@ -85,10 +81,6 @@ choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' choco install llvm ``` -``` -choco install protoc -``` - These dependencies are for compiling Lighthouse natively on Windows. Lighthouse can also run successfully under the [Windows Subsystem for Linux (WSL)][WSL]. If using Ubuntu under WSL, you should follow the instructions for Ubuntu listed in the [Dependencies (Ubuntu)](#ubuntu) section. @@ -159,13 +151,13 @@ Commonly used features include: * `gnosis`: support for the Gnosis Beacon Chain. * `portable`: support for legacy hardware. * `modern`: support for exclusively modern hardware. -* `slasher-mdbx`: support for the MDBX slasher backend. Enabled by default. -* `slasher-lmdb`: support for the LMDB slasher backend. +* `slasher-lmdb`: support for the LMDB slasher backend. Enabled by default. +* `slasher-mdbx`: support for the MDBX slasher backend. * `jemalloc`: use [`jemalloc`][jemalloc] to allocate memory. Enabled by default on Linux and macOS. Not supported on Windows. * `spec-minimal`: support for the minimal preset (useful for testing). -Default features (e.g. `slasher-mdbx`) may be opted out of using the `--no-default-features` +Default features (e.g. `slasher-lmdb`) may be opted out of using the `--no-default-features` argument for `cargo`, which can be plumbed in via the `CARGO_INSTALL_EXTRA_FLAGS` environment variable. E.g. @@ -217,4 +209,3 @@ look into [cross compilation](./cross-compiling.md), or use a [pre-built binary](https://github.com/sigp/lighthouse/releases). If compilation fails with `error: linking with cc failed: exit code: 1`, try running `cargo clean`. - diff --git a/book/src/key-management.md b/book/src/key-management.md index 084b1fbe4..b2bb7737f 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -1,9 +1,30 @@ -# Key Management +# Key Management (Deprecated) [launchpad]: https://launchpad.ethereum.org/ -> -> **Note: While Lighthouse is able to generate the validator keys and the deposit data file to submit to the deposit contract, we strongly recommend using the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) to create validators keys and the deposit data file. This is because the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) which has the option to assign a withdrawal address during the key generation process, while Lighthouse wallet will always generate keys with withdrawal credentials of type 0x00. This means that users who created keys using Lighthouse will have to update their withdrawal credentials in the future to enable withdrawals. In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only `*.json` file. This means that users have to directly interact with the deposit contract to be able to submit the deposit if they were to generate the files using Lighthouse.** +**⚠️ The information on this page refers to tooling and process that have been deprecated. Please read the "Deprecation Notice". ⚠️** + +## Deprecation Notice + +This page recommends the use of the `lighthouse account-manager` tool to create +validators. This tool will always generate keys with the withdrawal credentials +of type `0x00`. This means the users who created keys using `lighthouse +account-manager` will have to update their withdrawal credentials in a +separate step to receive staking rewards. + +In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, +which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only +`*.json` file. This means that users have to directly interact with the deposit +contract to be able to submit the deposit if they were to generate the files +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 [validator-manager documentation](./validator-manager.md) for a Lighthouse-specific tool for streamlined validator management tools. + +## The `lighthouse account-manager` Lighthouse uses a _hierarchical_ key management system for producing validator keys. It is hierarchical because each validator key can be _derived_ from a diff --git a/book/src/merge-migration.md b/book/src/merge-migration.md index ec9aeaaee..bab520b56 100644 --- a/book/src/merge-migration.md +++ b/book/src/merge-migration.md @@ -1,15 +1,13 @@ # Merge Migration -This document provides detail for users who want to run a Lighthouse node on post-merge Ethereum. - -> The merge occurred on mainnet in September 2022. +[The Merge](https://ethereum.org/en/roadmap/merge/) has occurred on mainnet on 15th September 2022. This document provides detail of what users need to do in the past (before The Merge) to run a Lighthouse node on a post-merge Ethereum network. This document now serves as a record of the milestone upgrade. ## Necessary Configuration There are two configuration changes required for a Lighthouse node to operate correctly throughout the merge: -1. You *must* run your own execution engine such as Geth or Nethermind alongside Lighthouse. +1. You *must* run your own execution engine such as Besu, Erigon, Geth or Nethermind alongside Lighthouse. You *must* update your `lighthouse bn` configuration to connect to the execution engine using new flags which are documented on this page in the [Connecting to an execution engine](#connecting-to-an-execution-engine) section. @@ -23,12 +21,20 @@ engine to a merge-ready version. ## When? -You must configure your node to be merge-ready before the Bellatrix fork occurs on the network -on which your node is operating. +All networks (**Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**, **Chiado**, **Gnosis**) have successfully undergone the Bellatrix fork and transitioned to a post-merge Network. Your node must have a merge-ready configuration to continue operating. Table below lists the date at which Bellatrix and The Merge occurred: -* **Gnosis**: the Bellatrix fork has not yet been scheduled. -* **Mainnet**, **Goerli (Prater)**, **Ropsten**, **Sepolia**, **Kiln**: the Bellatrix fork has - already occurred. You must have a merge-ready configuration right now. +
+ +| 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 + +
## Connecting to an execution engine @@ -42,7 +48,7 @@ present in post-merge blocks. Two new flags are used to configure this connectio If you set up an execution engine with `--execution-endpoint` then you *must* provide a JWT secret using `--execution-jwt`. This is a mandatory form of authentication that ensures that Lighthouse -has authority to control the execution engine. +has the authority to control the execution engine. > Tip: the --execution-jwt-secret-key flag can be used instead of --execution-jwt . > This is useful, for example, for users who wish to inject the value into a Docker container without @@ -89,7 +95,7 @@ lighthouse \ beacon_node \ --http \ --execution-endpoint http://localhost:8551 - --execution-jwt ~/.ethereum/geth/jwtsecret + --execution-jwt /path/to/jwtsecret ``` The changes here are: @@ -105,8 +111,7 @@ The changes here are: not be `8551`, see their documentation for details. 3. Add the `--execution-jwt` flag. - This is the path to a file containing a 32-byte secret for authenticating the BN with the - execution engine. In this example our execution engine is Geth, so we've chosen the default - location for Geth. Your execution engine might have a different path. It is critical that both + execution engine. It is critical that both the BN and execution engine reference a file with the same value, otherwise they'll fail to communicate. @@ -118,7 +123,7 @@ a deprecation warning will be logged and Lighthouse *may* remove these flags in ### The relationship between `--eth1-endpoints` and `--execution-endpoint` Pre-merge users will be familiar with the `--eth1-endpoints` flag. This provides a list of Ethereum -"eth1" nodes (e.g., Geth, Nethermind, etc). Each beacon node (BN) can have multiple eth1 endpoints +"eth1" nodes (Besu, Erigon, Geth or Nethermind). Each beacon node (BN) can have multiple eth1 endpoints and each eth1 endpoint can have many BNs connection (many-to-many relationship). The eth1 node provides a source of truth for the [deposit contract](https://ethereum.org/en/staking/deposit-contract/) and beacon chain proposers include this @@ -129,7 +134,7 @@ achieve this. To progress through the Bellatrix upgrade nodes will need a *new* connection to an "eth1" node; `--execution-endpoint`. This connection has a few different properties. Firstly, the term "eth1 node" has been deprecated and replaced with "execution engine". Whilst "eth1 node" and "execution -engine" still refer to the same projects (Geth, Nethermind, etc) the former refers to the pre-merge +engine" still refer to the same projects (Besu, Erigon, Geth or Nethermind), the former refers to the pre-merge versions and the latter refers to post-merge versions. Secondly, there is a strict one-to-one relationship between Lighthouse and the execution engine; only one Lighthouse node can connect to one execution engine. Thirdly, it is impossible to fully verify the post-merge chain without an @@ -139,7 +144,7 @@ impossible to reliably *propose* blocks without it. Since an execution engine is a hard requirement in the post-merge chain and the execution engine contains the transaction history of the Ethereum chain, there is no longer a need for the `--eth1-endpoints` flag for information about the deposit contract. The `--execution-endpoint` can -be used for all such queries. Therefore we can say that where `--execution-endpoint` is included +be used for all such queries. Therefore we can say that where `--execution-endpoint` is included, `--eth1-endpoints` should be omitted. ## FAQ diff --git a/book/src/partial-withdrawal.md b/book/src/partial-withdrawal.md index db722d729..e5a0a97c6 100644 --- a/book/src/partial-withdrawal.md +++ b/book/src/partial-withdrawal.md @@ -16,7 +16,7 @@ After the [Capella](https://ethereum.org/en/history/#capella) upgrade on 12 3. Do I have to do anything to get my rewards after I update the withdrawal credentials to type `0x01`? - No. The "validator sweep" occurs automatically and you can expect to receive the rewards every few days. + No. The "validator sweep" occurs automatically and you can expect to receive the rewards every *n* days, [more information here](./voluntary-exit.md#4-when-will-i-get-my-staked-fund-after-voluntary-exit-if-my-validator-is-of-type-0x01). Figure below summarizes partial withdrawals. diff --git a/book/src/pi.md b/book/src/pi.md index d8d154d76..550415240 100644 --- a/book/src/pi.md +++ b/book/src/pi.md @@ -22,7 +22,7 @@ terminal and an Internet connection are necessary. Install the Ubuntu dependencies: ```bash -sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang protobuf-compiler +sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang ``` > Tips: diff --git a/book/src/redundancy.md b/book/src/redundancy.md index dcd2ecdea..77cec3253 100644 --- a/book/src/redundancy.md +++ b/book/src/redundancy.md @@ -8,7 +8,7 @@ There are three places in Lighthouse where redundancy is notable: 1. ❌ NOT SUPPORTED: Using a redundant execution node in `lighthouse bn --execution-endpoint` 1. ☠️ BAD: Running redundant `lighthouse vc` instances with overlapping keypairs. -I mention (3) since it is unsafe and should not be confused with the other two +We mention (3) since it is unsafe and should not be confused with the other two uses of redundancy. **Running the same validator keypair in more than one validator client (Lighthouse, or otherwise) will eventually lead to slashing.** See [Slashing Protection](./slashing-protection.md) for more information. @@ -49,6 +49,7 @@ There are a few interesting properties about the list of `--beacon-nodes`: > provided (if it is desired). It will only be used as default if no `--beacon-nodes` flag is > provided at all. + ### Configuring a redundant Beacon Node In our previous example, we listed `http://192.168.1.1:5052` as a redundant @@ -56,10 +57,9 @@ node. Apart from having sufficient resources, the backup node should have the following flags: - `--http`: starts the HTTP API server. -- `--http-address 0.0.0.0`: this allows *any* external IP address to access the - HTTP server (a firewall should be configured to deny unauthorized access to port - `5052`). This is only required if your backup node is on a different host. -- `--execution-endpoint`: see [Merge Migration](./merge-migration.md). +- `--http-address local_IP`: where `local_IP` is the private IP address of the computer running the beacon node. This is only required if your backup beacon node is on a different host. + > Note: You could also use `--http-address 0.0.0.0`, but this allows *any* external IP address to access the HTTP server. As such, a firewall should be configured to deny unauthorized access to port `5052`. + - `--execution-endpoint`: see [Merge Migration](./merge-migration.md). - `--execution-jwt`: see [Merge Migration](./merge-migration.md). For example one could use the following command to provide a backup beacon node: @@ -67,7 +67,7 @@ For example one could use the following command to provide a backup beacon node: ```bash lighthouse bn \ --http \ - --http-address 0.0.0.0 \ + --http-address local_IP \ --execution-endpoint http://localhost:8551 \ --execution-jwt /secrets/jwt.hex ``` diff --git a/book/src/run_a_node.md b/book/src/run_a_node.md index a31aedf78..1ea142733 100644 --- a/book/src/run_a_node.md +++ b/book/src/run_a_node.md @@ -58,7 +58,8 @@ Notable flags: - `lighthouse --network mainnet`: Mainnet. - `lighthouse --network goerli`: Goerli (testnet). - `lighthouse --network sepolia`: Sepolia (testnet). - - `lighthouse --network gnosis`: Gnosis chain + - `lighthouse --network chiado`: Chiado (testnet). + - `lighthouse --network gnosis`: Gnosis chain. > Note: Using the correct `--network` flag is very important; using the wrong flag can result in penalties, slashings or lost deposits. As a rule of thumb, *always* diff --git a/book/src/setup.md b/book/src/setup.md index 62580ac1b..d7eafbdf9 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -9,16 +9,16 @@ particularly useful for development but still a good way to ensure you have the base dependencies. The additional requirements for developers are: -- [`anvil`](https://github.com/foundry-rs/foundry/tree/master/anvil). This is used to +- [`anvil`](https://github.com/foundry-rs/foundry/tree/master/crates/anvil). This is used to simulate the execution chain during tests. You'll get failures during tests if you don't have `anvil` available on your `PATH`. - [`cmake`](https://cmake.org/cmake/help/latest/command/install.html). Used by some dependencies. See [`Installation Guide`](./installation.md) for more info. -- [`protoc`](https://github.com/protocolbuffers/protobuf/releases) required for - the networking stack. -- [`java 11 runtime`](https://openjdk.java.net/projects/jdk/). 11 is the minimum, +- [`java 17 runtime`](https://openjdk.java.net/projects/jdk/). 17 is the minimum, used by web3signer_tests. - +- [`libpq-dev`](https://www.postgresql.org/docs/devel/libpq.html). Also know as + `libpq-devel` on some systems. +- [`docker`](https://www.docker.com/). Some tests need docker installed and **running**. ## Using `make` Commands to run the test suite are available via the `Makefile` in the diff --git a/book/src/slasher.md b/book/src/slasher.md index 61dc4b327..41bc3baf7 100644 --- a/book/src/slasher.md +++ b/book/src/slasher.md @@ -8,10 +8,9 @@ extra income for your validators. However it is currently only recommended for e of the immaturity of the slasher UX and the extra resources required. ## Minimum System Requirements - * Quad-core CPU * 16 GB RAM -* 256 GB solid state storage (in addition to space for the beacon node DB) +* 256 GB solid state storage (in addition to the space requirement for the beacon node DB) ## How to Run @@ -28,7 +27,7 @@ messages are filtered for relevancy, and all relevant messages are checked for s to the slasher database. You **should** run with debug logs, so that you can see the slasher's internal machinations, and -provide logs to the devs should you encounter any bugs. +provide logs to the developers should you encounter any bugs. ## Configuration @@ -47,23 +46,49 @@ directory. * Flag: `--slasher-backend NAME` * Argument: one of `mdbx`, `lmdb` or `disabled` -* Default: `mdbx` +* Default: `lmdb` for new installs, `mdbx` if an MDBX database already exists -Since Lighthouse v2.6.0 it is possible to use one of several database backends with the slasher: +It is possible to use one of several database backends with the slasher: -- MDBX (default) -- LMDB +- LMDB (default) +- MDBX The advantage of MDBX is that it performs compaction, resulting in less disk usage over time. The -disadvantage is that upstream MDBX has removed support for Windows and macOS, so Lighthouse is stuck -on an older version. If bugs are found in our pinned version of MDBX it may be deprecated in future. +disadvantage is that upstream MDBX is unstable, so Lighthouse is pinned to a specific version. +If bugs are found in our pinned version of MDBX it may be deprecated in future. -LMDB does not have compaction but is more stable upstream than MDBX. It is not currently recommended -to use the LMDB backend on Windows. +LMDB does not have compaction but is more stable upstream than MDBX. If running with the LMDB +backend on Windows it is recommended to allow extra space due to this issue: +[sigp/lighthouse#2342](https://github.com/sigp/lighthouse/issues/2342). More backends may be added in future. -### Switching Backends +#### Backend Override + +The default backend was changed from MDBX to LMDB in Lighthouse v4.3.0. + +If an MDBX database is already found on disk, then Lighthouse will try to use it. This will result +in a log at start-up: + +``` +INFO Slasher backend overriden reason: database exists, configured_backend: lmdb, overriden_backend: mdbx +``` + +If the running Lighthouse binary doesn't have the MDBX backend enabled but an existing database is +found, then a warning will be logged and Lighthouse will use the LMDB backend and create a new database: + +``` +WARN Slasher backend override failed advice: delete old MDBX database or enable MDBX backend, path: /home/user/.lighthouse/mainnet/beacon/slasher_db/mdbx.dat +``` + +In this case you should either obtain a Lighthouse binary with the MDBX backend enabled, or delete +the files for the old backend. The pre-built Lighthouse binaries and Docker images have MDBX enabled, +or if you're [building from source](./installation-source.md) you can enable the `slasher-mdbx` feature. + +To delete the files, use the `path` from the `WARN` log, and then delete the `mbdx.dat` and +`mdbx.lck` files. + +#### Switching Backends If you change database backends and want to reclaim the space used by the old backend you can delete the following files from your `slasher_db` directory: @@ -97,7 +122,7 @@ Both database backends LMDB and MDBX place a hard limit on the size of the datab file. You can use the `--slasher-max-db-size` flag to set this limit. It can be adjusted after initialization if the limit is reached. -By default the limit is set to accommodate the default history length and around 300K validators but +By default the limit is set to accommodate the default history length and around 600K validators (with about 30% headroom) but you can set it lower if running with a reduced history length. The space required scales approximately linearly in validator count and history length, i.e. if you halve either you can halve the space required. @@ -108,7 +133,7 @@ If you want an estimate of the database size you can use this formula: 4.56 GB * (N / 256) * (V / 250000) ``` -where `V` is the validator count and `N` is the history length. +where `N` is the history length and `V` is the validator count. You should set the maximum size higher than the estimate to allow room for growth in the validator count. diff --git a/book/src/suggested-fee-recipient.md b/book/src/suggested-fee-recipient.md index f3ece8506..44accbd14 100644 --- a/book/src/suggested-fee-recipient.md +++ b/book/src/suggested-fee-recipient.md @@ -103,6 +103,8 @@ client. } ``` +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 @@ -115,11 +117,15 @@ curl -X POST \ http://localhost:5062/eth/v1/validator/${PUBKEY}/feerecipient | jq ``` +Note that an authorization header is required to interact with the API. This is specified with the header `-H "Authorization: Bearer $(cat ${DATADIR}/validators/api-token.txt)"` which read the API token to supply the authentication. Refer to [Authorization Header](./api-vc-auth-header.md) for more information. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. + #### Successful Response (202) ```json null ``` +A `null` response indicates that the request is successful. + ### Querying the fee recipient The same path with a `GET` request can be used to query the fee recipient for a given public key at any time. @@ -131,6 +137,8 @@ The same path with a `GET` request can be used to query the fee recipient for a | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 200, 404 | +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 @@ -163,6 +171,8 @@ This is useful if you want the fee recipient to fall back to the validator clien | Required Headers | [`Authorization`](./api-vc-auth-header.md) | | Typical Responses | 204, 404 | +Command: + ```bash DATADIR=$HOME/.lighthouse/mainnet PUBKEY=0xa9735061c84fc0003657e5bd38160762b7ef2d67d280e00347b1781570088c32c06f15418c144949f5d736b1d3a6c591 diff --git a/book/src/ui-configuration.md b/book/src/ui-configuration.md index 5b67b03b3..1a5daa544 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui-configuration.md @@ -9,12 +9,20 @@ following configuration screen. ## Connecting to the Clients -This allows you to enter the address and ports of the associated Lighthouse +Both the Beacon node and the Validator client need to have their HTTP APIs enabled. These ports should be accessible from the computer running Siren. This allows you to enter the address and ports of the associated Lighthouse Beacon node and Lighthouse Validator client. -> The Beacon Node must be run with the `--gui` flag set. To allow the browser -> to access the node beyond your local computer you also need to allow CORS in -> the http API. This can be done via `--http-allow-origin "*"`. +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. + +> The Beacon Node must be run with the `--gui` flag set. + +If you require accessibility from another machine within the network, configure the `--http-address` to match the local LAN IP of the system running the Beacon Node and Validator Client. + +> To access from another machine on the same network (192.168.0.200) set the Beacon Node and Validator Client `--http-address` as `192.168.0.200`. + +In a similar manner, the validator client requires activation of the `--http` flag, along with the optional consideration of configuring the `--http-address` flag. If `--http-address` flag is set on the Validator Client, then the `--unencrypted-http-transport` flag is required as well. These settings will ensure compatibility with Siren's connectivity requirements. + +If you run Siren in the browser (by entering `localhost` in the browser), you will need to allow CORS in the HTTP API. This can be done by adding the flag `--http-allow-origin "*"` for both beacon node and validator client. A green tick will appear once Siren is able to connect to both clients. You can specify different ports for each client by clicking on the advanced tab. @@ -26,19 +34,19 @@ The API Token is a secret key that allows you to connect to the validator client. The validator client's HTTP API is guarded by this key because it contains sensitive validator information and the ability to modify validators. Please see [`Validator Authorization`](./api-vc-auth-header.md) -for further details. +for further details. Siren requires this token in order to connect to the Validator client. The token is located in the default data directory of the validator client. The default path is `~/.lighthouse//validators/api-token.txt`. -The contents of this file for the desired valdiator client needs to be +The contents of this file for the desired validator client needs to be entered. ## Name -This is your name, it can be modified and is solely used for aesthetics. +This is your name, it can be modified and is solely used for aesthetics. ## Device diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index 51aa9385a..92e06270c 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -1,16 +1,61 @@ # Frequently Asked Questions ## 1. Are there any requirements to run Siren? -Yes, Siren requires Lighthouse v3.5.1 or higher to function properly. These releases can be found on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. +Yes, the most current Siren version requires Lighthouse v4.3.0 or higher to function properly. These releases can be found on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. ## 2. Where can I find my API token? -The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./ui-configuration.md#api-token). +The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). ## 3. How do I fix the Node Network Errors? -If you recieve a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). -## 4. How do I change my Beacon or Validator address after logging in? -Once you have successfully arrived to the main dashboard, use the sidebar to access the settings view. In the top right hand corner there is a `Configurtion` action button that will redirect you back to the configuration screen where you can make appropriate changes. +## 4. How do I connect Siren to Lighthouse from a different computer on the same network? +The most effective approach to enable access for a local network computer to Lighthouse's HTTP API ports is by configuring the `--http-address` to match the local LAN IP of the system running the beacon node and validator client. For instance, if the said node operates at `192.168.0.200`, this IP can be specified using the `--http-address` parameter as `--http-address 192.168.0.200`. +Subsequently, by designating the host as `192.168.0.200`, you can seamlessly connect Siren to this specific beacon node and validator client pair from any computer situated within the same network. -## 5. Why doesn't my validator balance graph show any data? +## 5. How can I use Siren to monitor my validators remotely when I am not at home? + +There are two primary methods to access your Beacon Node and Validator Client remotely: setting up a VPN or utilizing SSH-reverse tunneling. + +Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). + +In the absence of a VPN, an alternative approach involves utilizing an SSH tunnel. To achieve this, you need remote SSH access to the computer hosting the Beacon Node and Validator Client pair (which necessitates a port forward in your router). In this context, while it is not obligatory to set a `--http-address` flag on the Beacon Node and Validator Client, you can configure an SSH tunnel to the local ports on the node and establish a connection through the tunnel. For instructions on setting up an SSH tunnel, refer to [`Connecting Siren via SSH tunnel`](./ui-faqs.md#6-how-do-i-connect-siren-to-lighthouse-via-a-ssh-tunnel) for detailed guidance. + +## 6. How do I connect Siren to Lighthouse via a ssh tunnel? +If you would like to access Siren beyond the local network (i.e across the internet), we recommend using an SSH tunnel. This requires a tunnel for 3 ports: `80` (assuming the port is unchanged as per the [installation guide](./ui-installation.md#docker-recommended), `5052` (for beacon node) and `5062` (for validator client). You can use the command below to perform SSH tunneling: + +```bash + +ssh -N -L 80:127.0.0.1:80 -L 5052:127.0.0.1:5052 -L 5062:127.0.0.1:5062 username@local_ip + +``` + + +Where `username` is the username of the server and `local_ip` is the local IP address of the server. Note that with the `-N` option in an SSH session, you will not be able to execute commands in the CLI to avoid confusion with ordinary shell sessions. The connection will appear to be "hung" upon a successful connection, but that is normal. Once you have successfully connected to the server via SSH tunneling, you should be able to access Siren by entering `localhost` in a web browser. + + +You can also access Siren using the app downloaded in the [Siren release page](https://github.com/sigp/siren/releases). To access Siren beyond the local computer, you can use SSH tunneling for ports `5052` and `5062` using the command: + + +```bash + +ssh -N -L 5052:127.0.0.1:5052 -L 5062:127.0.0.1:5062 username@local_ip + +``` + +## 7. Does Siren support reverse proxy or DNS named addresses? +Yes, if you need to access your beacon or validator from an address such as `https://merp-server:9909/eth2-vc` you should follow the following steps for configuration: +1. Toggle `https` as your protocol +2. Add your address as `merp-server/eth2-vc` +3. Add your Beacon and Validator ports as `9909` + +If you have configured it correctly you should see a green checkmark indicating Siren is now connected to your Validator Client and Beacon Node. + +If you have separate address setups for your Validator Client and Beacon Node respectively you should access the `Advance Settings` on the configuration and repeat the steps above for each address. + + +## 8. How do I change my Beacon or Validator address after logging in? +Once you have successfully arrived to the main dashboard, use the sidebar to access the settings view. In the top right hand corner there is a `Configuration` action button that will redirect you back to the configuration screen where you can make appropriate changes. + +## 9. Why doesn't my validator balance graph show any data? 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. diff --git a/book/src/ui-usage.md b/book/src/ui-usage.md index e88c4677a..867a49a91 100644 --- a/book/src/ui-usage.md +++ b/book/src/ui-usage.md @@ -10,7 +10,7 @@ Siren's dashboard view provides a summary of all performance and key validator m The account earnings component accumulates reward data from all registered validators providing a summation of total rewards earned while staking. Given current conversion rates, this component also converts your balance into your selected fiat currency. -Below in the earning section, you can also view your total earnings or click the adjacent buttons to view your estimated earnings given a specific timeframe based on current device and network conditions. +Below in the earning section, you can also view your total earnings or click the adjacent buttons to view your estimated earnings given a specific time frame based on current device and network conditions. ![](imgs/ui-account-earnings.png) @@ -33,6 +33,7 @@ By clicking on the chart component you can filter selected validators in the ren + ## Hardware Usage and Device Diagnostics The hardware usage component gathers information about the device the Beacon Node is currently running. It displays the Disk usage, CPU metrics and memory usage of the Beacon Node device. The device diagnostics component provides the sync status of the execution client and beacon node. @@ -42,6 +43,12 @@ The hardware usage component gathers information about the device the Beacon Nod +## Log Statistics + +The log statistics present an hourly combined rate of critical, warning, and error logs from the validator client and beacon node. This analysis enables informed decision-making, troubleshooting, and proactive maintenance for optimal system performance. + + + # Validator Management Siren's validator management view provides a detailed overview of all validators with options to deposit to and/or add new validators. Each validator table row displays the validator name, index, balance, rewards, status and all available actions per validator. @@ -59,3 +66,12 @@ Clicking the validator icon activates a detailed validator modal component. This Siren's settings view provides access to the application theme, version, name, device name and important external links. From the settings page users can also access the configuration screen to adjust any beacon or validator node parameters. ![](imgs/ui-settings.png) + + +# Validator and Beacon Logs + +The logs page provides users with the functionality to access and review recorded logs for both validators and beacons. Users can conveniently observe log severity, messages, timestamps, and any additional data associated with each log entry. The interface allows for seamless switching between validator and beacon log outputs, and incorporates useful features such as built-in text search and the ability to pause log feeds. + +Additionally, users can obtain log statistics, which are also available on the main dashboard, thereby facilitating a comprehensive overview of the system's log data. Please note that Siren is limited to storing and displaying only the previous 1000 log messages. This also means the text search is limited to the logs that are currently stored within Siren's limit. + +![](imgs/ui-logs.png) \ No newline at end of file diff --git a/book/src/validator-doppelganger.md b/book/src/validator-doppelganger.md index 6eaddcc7b..b62086d4b 100644 --- a/book/src/validator-doppelganger.md +++ b/book/src/validator-doppelganger.md @@ -43,13 +43,14 @@ DP works by staying silent on the network for 2-3 epochs before starting to sign Staying silent and refusing to sign messages will cause the following: - 2-3 missed attestations, incurring penalties and missed rewards. -- 2-3 epochs of missed sync committee contributions (if the validator is in a sync committee, which is unlikely), incurring penalties and missed rewards. - Potentially missed rewards by missing a block proposal (if the validator is an elected block proposer, which is unlikely). +Notably, sync committee contributions are not slashable and will continue to be produced even when DP is suppressing other messages. + The loss of rewards and penalties incurred due to the missed duties will be very small in -dollar-values. Generally, they will equate to around one US dollar (at August 2021 figures) or about -2% of the reward for one validator for one day. Since DP costs so little but can protect a user from +dollar-values. Neglecting block proposals, generally they will equate to around 0.00002 ETH (equivalent to USD 0.04 assuming ETH is trading at USD 2000), or less than +1% of the reward for one validator for one day. Since DP costs so little but can protect a user from slashing, many users will consider this a worthwhile trade-off. The 2-3 epochs of missed duties will be incurred whenever the VC is started (e.g., after an update diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index 0793af20d..ef81b2b75 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -8,6 +8,8 @@ These endpoints are not stable or included in the Ethereum consensus standard AP they are subject to change or removal without a change in major release version. +In order to apply these APIs, you need to have historical states information in the database of your node. This means adding the flag `--reconstruct-historic-states` in the beacon node or using the [/lighthouse/database/reconstruct API](./api-lighthouse.md#lighthousedatabasereconstruct). Once the state reconstruction process is completed, you can apply these APIs to any epoch. + ## Endpoints HTTP Path | Description | @@ -29,7 +31,7 @@ is not the case for attestations from the _previous_ epoch. ``` `epoch` query parameter | - | --------- values are calcuated here + | --------- values are calculated here | | v v Epoch: |---previous---|---current---|---next---| diff --git a/book/src/validator-management.md b/book/src/validator-management.md index be34fef2c..df7c2ac47 100644 --- a/book/src/validator-management.md +++ b/book/src/validator-management.md @@ -13,6 +13,10 @@ standard directories and do not start their `lighthouse vc` with the this document. However, users with more complex needs may find this document useful. +The [lighthouse validator-manager](./validator-manager.md) command can be used +to create and import validators to a Lighthouse VC. It can also be used to move +validators between two Lighthouse VCs. + ## Introducing the `validator_definitions.yml` file The `validator_definitions.yml` file is located in the `validator-dir`, which diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md new file mode 100644 index 000000000..0cec150da --- /dev/null +++ b/book/src/validator-manager-create.md @@ -0,0 +1,204 @@ +# Creating and Importing Validators + +[Ethereum Staking Launchpad]: https://launchpad.ethereum.org/en/ + +The `lighthouse validator-manager create` command derives validators from a +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 + be used for deposit submission via the [Ethereum Staking + Launchpad][]. + +The `lighthouse validator-manager import` command accepts a `validators.json` +file (from the `create` command) and submits those validators to a running +Lighthouse Validator Client via the HTTP API. + +These two commands enable a workflow of: + +1. Creating the validators via the `create` command. +1. Importing the validators via the `import` command. +1. Depositing validators via the [Ethereum Staking + Launchpad][]. + +The separation of the `create` and `import` commands allows for running the +`create` command on an air-gapped host whilst performing the `import` command on +an internet-connected host. + +The `create` and `import` commands are recommended for advanced users who are +familiar with command line tools and the practicalities of managing sensitive +cryptographic material. **We recommend that novice users follow the workflow on +[Ethereum Staking Launchpad][] rather than using the `create` and `import` +commands.** + +## Simple Example + +Create validators from a mnemonic with: + +```bash +lighthouse \ + validator-manager \ + create \ + --network mainnet \ + --first-index 0 \ + --count 2 \ + --eth1-withdrawal-address
\ + --suggested-fee-recipient
\ + --output-path ./ +``` +> If the flag `--first-index` is not provided, it will default to using index 0. +> The `--suggested-fee-recipient` flag may be omitted to use whatever default +> value the VC uses. It does not necessarily need to be identical to +> `--eth1-withdrawal-address`. +> The command will create the `deposits.json` and `validators.json` in the present working directory. If you would like these files to be created in a different directory, change the value of `output-path`, for example `--output-path /desired/directory`. The directory will be created if the path does not exist. + +Then, import the validators to a running VC with: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token +``` +> This is assuming that `validators.json` is in the present working directory. If it is not, insert the directory of the file. +> Be sure to remove `./validators.json` after the import is successful since it +> contains unencrypted validator keystores. + +## Detailed Guide + +This guide will create two validators and import them to a VC. For simplicity, +the same host will be used to generate the keys and run the VC. In reality, +users may want to perform the `create` command on an air-gapped machine and then +move the `validators.json` and `deposits.json` files to an Internet-connected +host. This would help protect the mnemonic from being exposed to the Internet. + +### 1. Create the Validators + +Run the `create` command, substituting `
` for an execution address that +you control. This is where all the staked ETH and rewards will ultimately +reside, so it's very important that this address is secure, accessible and +backed-up. The `create` command: + +```bash +lighthouse \ + validator-manager \ + create \ + --first-index 0 \ + --count 2 \ + --eth1-withdrawal-address
\ + --output-path ./ +``` + +If successful, the command output will appear like below: + +```bash +Running validator manager for mainnet network + +Enter the mnemonic phrase: + +Valid mnemonic provided. + +Starting derivation of 2 keystores. Each keystore may take several seconds. +Completed 1/2: 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4 +Completed 2/2: 0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f +Keystore generation complete +Writing "./validators.json" +Writing "./deposits.json" +``` + +This command will create validators at indices `0, 1`. The exact indices created +can be influenced with the `--first-index` and `--count` flags. Use these flags +with caution to prevent creating the same validator twice, this may result in a +slashing! + +The command will create two files: + +- `./deposits.json`: this file does *not* contain sensitive information and may be uploaded to the [Ethereum Staking Launchpad]. +- `./validators.json`: this file contains **sensitive unencrypted validator keys, do not share it with anyone or upload it to any website**. + +### 2. Import the validators + +The VC which will receive the validators needs to have the following flags at a minimum: + +- `--http` +- `--enable-doppelganger-protection` + +Therefore, the VC command might look like: + +```bash +lighthouse \ + vc \ + --http \ + --enable-doppelganger-protection +``` + +In order to import the validators, the location of the VC `api-token.txt` file +must be known. The location of the file varies, but it is located in the +"validator directory" of your data directory. For example: +`~/.lighthouse/mainnet/validators/api-token.txt`. We will use `` +to subsitute this value. If you are unsure of the `api-token.txt` path, you can run `curl http://localhost:5062/lighthouse/auth` which will show the path. + + +Once the VC is running, use the `import` command to import the validators to the VC: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token +``` + +If successful, the command output will appear like below: + +```bash +Running validator manager for mainnet network +Validator client is reachable at http://localhost:5062/ and reports 0 validators +Starting to submit 2 validators to VC, each validator may take several seconds +Uploaded keystore 1 of 2 to the VC +Uploaded keystore 2 of 2 to the VC +``` + +The user should now *securely* delete the `validators.json` file (e.g., `shred -u validators.json`). +The `validators.json` contains the unencrypted validator keys and must not be +shared with anyone. +At the same time, `lighthouse vc` will log: +```bash +INFO Importing keystores via standard HTTP API, count: 1 +WARN No slashing protection data provided with keystores +INFO Enabled validator voting_pubkey: 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f, signing_method: local_keystore +INFO Modified key_cache saved successfully +``` +The WARN message means that the `validators.json` file does not contain the slashing protection data. This is normal if you are starting a new validator. The flag `--enable-doppelganger-protection` will also protect users from potential slashing risk. +The validators will now go through 2-3 epochs of [doppelganger +protection](./validator-doppelganger.md) and will automatically start performing +their duties when they are deposited and activated. + +If the host VC contains the same public key as the `validators.json` file, an error will be shown and the `import` process will stop: + +```bash +Duplicate validator 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f already exists on the destination validator client. This may indicate that some validators are running in two places at once, which can lead to slashing. If you are certain that there is no risk, add the --ignore-duplicates flag. +Err(DuplicateValidator(0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f)) +``` + +If you are certain that it is safe, you can add the flag `--ignore-duplicates` in the `import` command. The command becomes: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token \ + --ignore-duplicates +``` +and the output will be as follows: + +```bash +Duplicate validators are ignored, ignoring 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f which exists on the destination validator client +Re-uploaded keystore 1 of 6 to the VC +``` + +The guide is complete. \ No newline at end of file diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md new file mode 100644 index 000000000..15089d65c --- /dev/null +++ b/book/src/validator-manager-move.md @@ -0,0 +1,187 @@ +# Moving Validators + +The `lighthouse validator-manager move` command uses the VC HTTP API to move +validators from one VC (the "src" VC) to another VC (the "dest" VC). The move +operation is *comprehensive*; it will: + +- Disable the validators on the src VC. +- Remove the validator keystores from the src VC file system. +- Export the slashing database records for the appropriate validators from the src VC to the dest VC. +- Enable the validators on the dest VC. +- Generally result in very little or no validator downtime. + +It is capable of moving all validators on the src VC, a count of validators or +a list of pubkeys. + +The `move` command is only guaranteed to work between two Lighthouse VCs (i.e., +there is no guarantee that the commands will work between Lighthouse and Teku, for instance). + +The `move` command only supports moving validators using a keystore on the local +file system, it does not support `Web3Signer` validators. + +Although all efforts are taken to avoid it, it's possible for the `move` command +to fail in a way that removes the validator from the src VC without adding it to the +dest VC. Therefore, it is recommended to **never use the `move` command without +having a backup of all validator keystores (e.g. the mnemonic).** + +## Simple Example + +The following command will move all validators from the VC running at +`http://localhost:6062` to the VC running at `http://localhost:5062`. + +```bash +lighthouse \ + validator-manager \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ + --validators all +``` + +## Detailed Guide + +This guide describes the steps to move validators between two validator clients (VCs) which are +able to SSH between each other. This guide assumes experience with the Linux command line and SSH +connections. + +There will be two VCs in this example: + +- The *source* VC which contains the validators/keystores to be moved. +- The *destination* VC which is to take the validators/keystores from the source. + +There will be two hosts in this example: + +- Host 1 (*"source host"*): Is running the `src-vc`. +- Host 2 (*"destination host"*): Is running the `dest-vc`. + +The example assumes +that Host 1 is able to SSH to Host 2. + +In reality, many host configurations are possible. For example: + +- Both VCs on the same host. +- Both VCs on different hosts and the `validator-manager` being used on a third host. + +### 1. Configure the Source VC + +The source VC needs to have the following flags at a minimum: + +- `--http` +- `--http-allow-keystore-export` + +Therefore, the source VC command might look like: + +```bash +lighthouse \ + vc \ + --http \ + --http-allow-keystore-export +``` + +### 2. Configure the Destination VC + +The destination VC needs to have the following flags at a minimum: + +- `--http` +- `--enable-doppelganger-protection` + +Therefore, the destination VC command might look like: + +```bash +lighthouse \ + vc \ + --http \ + --enable-doppelganger-protection +``` + +> The `--enable-doppelganger-protection` flag is not *strictly* required, however +> it is recommended for an additional layer of safety. It will result in 2-3 +> epochs of downtime for the validator after it is moved, which is generally an +> inconsequential cost in lost rewards or penalties. +> +> Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have +> the import validator keystore passwords stored in separate files rather than in the +> `validator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. + +### 3. Obtain the Source API Token + +The VC API is protected by an *API token*. This is stored in a file on each of the hosts. Since +we'll be running our command on the destination host, it will need to have the API token for the +source host on its file-system. + +On the **source host**, find the location of the `api-token.txt` file and copy the contents. The +location of the file varies, but it is located in the "validator directory" of your data directory, +alongside validator keystores. For example: `~/.lighthouse/mainnet/validators/api-token.txt`. If you are unsure of the `api-token.txt` path, you can run `curl http://localhost:5062/lighthouse/auth` which will show the path. + +Copy the contents of that file into a new file on the **destination host** at `~/src-token.txt`. The +API token should be similar to `api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123`. + +### 4. Create an SSH Tunnel + +In the **source host**, open a terminal window, SSH to the **destination host** and establish a reverse-SSH connection +between the **destination host** and the **source host**. + +```bash +ssh dest-host +ssh -L 6062:localhost:5062 src-host +``` + +It's important that you leave this session open throughout the rest of this tutorial. If you close +this terminal window then the connection between the destination and source host will be lost. + +### 5. Move + +With the SSH tunnel established between the `dest-host` and `src-host`, from the **destination +host** run the command to move the validators: + +```bash +lighthouse \ + validator-manager \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ + --validators all +``` + +The command will provide information about the progress of the operation and +emit `Done.` when the operation has completed successfully. For example: + +```bash +Running validator manager for mainnet network +Validator client is reachable at http://localhost:5062/ and reports 2 validators +Validator client is reachable at http://localhost:6062/ and reports 0 validators +Moved keystore 1 of 2 +Moved keystore 2 of 2 +Done. +``` +At the same time, `lighthouse vc` will log: +```bash +INFO Importing keystores via standard HTTP API, count: 1 +INFO Enabled validator voting_pubkey: 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f, signing_method: local_keystore +INFO Modified key_cache saved successfully +``` + +Once the operation completes successfully, there is nothing else to be done. The +validators have been removed from the `src-host` and enabled at the `dest-host`. +If the `--enable-doppelganger-protection` flag was used it may take 2-3 epochs +for the validators to start attesting and producing blocks on the `dest-host`. +If you would only like to move some validators, you can replace the flag `--validators all` with one or more validator public keys. For example: + +```bash +lighthouse \ + validator-manager \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ + --validators 0x9096aab771e44da149bd7c9926d6f7bb96ef465c0eeb4918be5178cd23a1deb4aec232c61d85ff329b54ed4a3bdfff3a,0x90fc4f72d898a8f01ab71242e36f4545aaf87e3887be81632bb8ba4b2ae8fb70753a62f866344d7905e9a07f5a9cdda1 +``` + +Any errors encountered during the operation should include information on how to +proceed. Assistance is also available on our +[Discord](https://discord.gg/cyAszAh). \ No newline at end of file diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md new file mode 100644 index 000000000..e3cb74bd6 --- /dev/null +++ b/book/src/validator-manager.md @@ -0,0 +1,35 @@ +# Validator Manager + +[Ethereum Staking Launchpad]: https://launchpad.ethereum.org/en/ +[Import Validators]: #import-validators + +## Introduction + +The `lighthouse validator-manager` tool provides utilities for managing validators on a *running* +Lighthouse Validator Client. The validator manager performs operations via the HTTP API of the +validator client (VC). Due to limitations of the +[keymanager-APIs](https://ethereum.github.io/keymanager-APIs/), only Lighthouse VCs are fully +supported by this command. + +The validator manager tool is similar to the `lighthouse account-manager` tool, +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 +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. + +The `validator-manager` boasts the following features: + +- One-line command to arbitrarily move validators between two VCs, maintaining the slashing protection database. +- Generates deposit files compatible with the [Ethereum Staking Launchpad][]. +- Generally involves zero or very little downtime. +- The "key cache" is preserved whenever a validator is added with the validator + manager, preventing long waits at start up when a new validator is added. + +## Guides + +- [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) \ No newline at end of file diff --git a/book/src/validator-web3signer.md b/book/src/validator-web3signer.md index 103f1ccb3..00ef9a6b5 100644 --- a/book/src/validator-web3signer.md +++ b/book/src/validator-web3signer.md @@ -5,7 +5,7 @@ [Teku]: https://github.com/consensys/teku [Web3Signer] is a tool by Consensys which allows *remote signing*. Remote signing is when a -Validator Client (VC) out-sources the signing of messages to remote server (e.g., via HTTPS). This +Validator Client (VC) out-sources the signing of messages to a remote server (e.g., via HTTPS). This means that the VC does not hold the validator private keys. ## Warnings @@ -47,7 +47,7 @@ remote signer: client_identity_password: "password" ``` -When using this file, the Lighthouse VC will perform duties for the `0xa5566..` validator and defer +When using this file, the Lighthouse VC will perform duties for the `0xa5566..` validator and refer to the `https://my-remote-signer.com:1234` server to obtain any signatures. It will load a "self-signed" SSL certificate from `/home/paul/my-certificates/my-remote-signer.pem` (on the filesystem of the VC) to encrypt the communications between the VC and Web3Signer. It will use diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md index d90395c07..8d61c1770 100644 --- a/book/src/voluntary-exit.md +++ b/book/src/voluntary-exit.md @@ -97,7 +97,26 @@ 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 5 days (at the time of writing with ~560,000 validators on the mainnet). The "validator sweep" is the process of skimming through all validators by index number for eligible 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 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. + +
+ +| 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. diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index 5c2dc9b4f..0c815ca9a 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -1,27 +1,27 @@ [package] name = "boot_node" -version = "4.2.0" +version = "4.5.0" authors = ["Sigma Prime "] -edition = "2021" +edition = { workspace = true } [dependencies] -beacon_node = { path = "../beacon_node" } -clap = "2.33.3" -clap_utils = { path = "../common/clap_utils" } -lighthouse_network = { path = "../beacon_node/lighthouse_network" } -types = { path = "../consensus/types" } -ethereum_ssz = "0.5.0" -slog = "2.5.2" -tokio = "1.14.0" -log = "0.4.11" -slog-term = "2.6.0" -logging = { path = "../common/logging" } -slog-async = "2.5.0" +beacon_node = { workspace = true } +clap = { workspace = true } +clap_utils = { workspace = true } +lighthouse_network = { workspace = true } +types = { workspace = true } +ethereum_ssz = { workspace = true } +slog = { workspace = true } +tokio = { workspace = true } +log = { workspace = true } +slog-term = { workspace = true } +logging = { workspace = true } +slog-async = { workspace = true } slog-scope = "4.3.0" slog-stdlog = "4.0.0" -hex = "0.4.2" -serde = "1.0.116" +hex = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -serde_json = "1.0.66" -serde_yaml = "0.8.13" -eth2_network_config = { path = "../common/eth2_network_config" } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +eth2_network_config = { workspace = true } diff --git a/boot_node/src/cli.rs b/boot_node/src/cli.rs index c3d7ac48a..d7ea5ab0b 100644 --- a/boot_node/src/cli.rs +++ b/boot_node/src/cli.rs @@ -13,13 +13,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .settings(&[clap::AppSettings::ColoredHelp]) .arg( Arg::with_name("enr-address") - .value_name("IP-ADDRESS") - .help("The external IP address/ DNS address to broadcast to other peers on how to reach this node. \ - If a DNS address is provided, the enr-address is set to the IP address it resolves to and \ - does not auto-update based on PONG responses in discovery.") + .long("enr-address") + .value_name("ADDRESS") + .help("The IP address/ DNS address to broadcast to other peers on how to reach \ + this node. If a DNS address is provided, the enr-address is set to the IP \ + address it resolves to and does not auto-update based on PONG responses in \ + discovery. Set this only if you are sure other nodes can connect to your \ + local node on this address. This will update the `ip4` or `ip6` ENR fields \ + accordingly. To update both, set this flag twice with the different values.") + .multiple(true) + .max_values(2) .required(true) - .takes_value(true) .conflicts_with("network-dir") + .takes_value(true), ) .arg( Arg::with_name("port") @@ -29,11 +35,29 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("9000") .takes_value(true) ) + .arg( + Arg::with_name("port6") + .long("port6") + .value_name("PORT") + .help("The UDP port to listen on over IpV6 when listening over both Ipv4 and \ + Ipv6. Defaults to 9090 when required.") + .default_value("9090") + .takes_value(true), + ) .arg( Arg::with_name("listen-address") .long("listen-address") .value_name("ADDRESS") - .help("The address the bootnode will listen for UDP connections.") + .help("The address the bootnode will listen for UDP communications. To listen \ + over IpV4 and IpV6 set this flag twice with the different values.\n\ + Examples:\n\ + - --listen-address '0.0.0.0' will listen over Ipv4.\n\ + - --listen-address '::' will listen over Ipv6.\n\ + - --listen-address '0.0.0.0' --listen-address '::' will listen over both \ + Ipv4 and Ipv6. The order of the given addresses is not relevant. However, \ + multiple Ipv4, or multiple Ipv6 addresses will not be accepted.") + .multiple(true) + .max_values(2) .default_value("0.0.0.0") .takes_value(true) ) @@ -59,6 +83,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("PORT") .help("The UDP6 port of the local ENR. Set this only if you are sure other nodes \ can connect to your local node on this port over IpV6.") + .conflicts_with("network-dir") .takes_value(true), ) .arg( @@ -77,7 +102,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Arg::with_name("network-dir") .value_name("NETWORK_DIR") .long("network-dir") - .help("The directory which contains the enr and it's assoicated private key") + .help("The directory which contains the enr and it's associated private key") .takes_value(true) ) } diff --git a/boot_node/src/config.rs b/boot_node/src/config.rs index d3ee58a90..5d7853bd2 100644 --- a/boot_node/src/config.rs +++ b/boot_node/src/config.rs @@ -2,7 +2,6 @@ use beacon_node::{get_data_dir, set_network_config}; use clap::ArgMatches; use eth2_network_config::Eth2NetworkConfig; use lighthouse_network::discovery::create_enr_builder_from_config; -use lighthouse_network::discv5::IpMode; use lighthouse_network::discv5::{enr::CombinedKey, Discv5Config, Enr}; use lighthouse_network::{ discovery::{load_enr_from_disk, use_or_load_enr}, @@ -10,13 +9,13 @@ use lighthouse_network::{ }; use serde_derive::{Deserialize, Serialize}; use ssz::Encode; -use std::net::SocketAddr; +use std::net::{SocketAddrV4, SocketAddrV6}; +use std::time::Duration; use std::{marker::PhantomData, path::PathBuf}; use types::EthSpec; /// A set of configuration parameters for the bootnode, established from CLI arguments. pub struct BootNodeConfig { - pub listen_socket: SocketAddr, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -26,7 +25,7 @@ pub struct BootNodeConfig { } impl BootNodeConfig { - pub fn new( + pub async fn new( matches: &ArgMatches<'_>, eth2_network_config: &Eth2NetworkConfig, ) -> Result { @@ -59,12 +58,12 @@ impl BootNodeConfig { set_network_config(&mut network_config, matches, &data_dir, &logger)?; - // Set the Enr UDP ports to the listening ports if not present. + // Set the Enr Discovery ports to the listening ports if not present. if let Some(listening_addr_v4) = network_config.listen_addrs().v4() { network_config.enr_udp4_port = Some( network_config .enr_udp4_port - .unwrap_or(listening_addr_v4.udp_port), + .unwrap_or(listening_addr_v4.disc_port), ) }; @@ -72,7 +71,7 @@ impl BootNodeConfig { network_config.enr_udp6_port = Some( network_config .enr_udp6_port - .unwrap_or(listening_addr_v6.udp_port), + .unwrap_or(listening_addr_v6.disc_port), ) }; @@ -81,33 +80,8 @@ impl BootNodeConfig { network_config.discv5_config.enr_update = false; } - // the address to listen on - let listen_socket = match network_config.listen_addrs().clone() { - lighthouse_network::ListenAddress::V4(v4_addr) => { - // Set explicitly as ipv4 otherwise - network_config.discv5_config.ip_mode = IpMode::Ip4; - v4_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::V6(v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: false, - }; - - v6_addr.udp_socket_addr() - } - lighthouse_network::ListenAddress::DualStack(_v4_addr, v6_addr) => { - // create ipv6 sockets and enable ipv4 mapped addresses. - network_config.discv5_config.ip_mode = IpMode::Ip6 { - enable_mapped_addresses: true, - }; - - v6_addr.udp_socket_addr() - } - }; - let private_key = load_private_key(&network_config, &logger); - let local_key = CombinedKey::from_libp2p(&private_key)?; + let local_key = CombinedKey::from_libp2p(private_key)?; let local_enr = if let Some(dir) = matches.value_of("network-dir") { let network_dir: PathBuf = dir.into(); @@ -117,8 +91,19 @@ impl BootNodeConfig { let enr_fork = { let spec = eth2_network_config.chain_spec::()?; - if eth2_network_config.beacon_state_is_known() { - let genesis_state = eth2_network_config.beacon_state::()?; + let genesis_state_url: Option = + clap_utils::parse_optional(matches, "genesis-state-url")?; + let genesis_state_url_timeout = + clap_utils::parse_required(matches, "genesis-state-url-timeout") + .map(Duration::from_secs)?; + + if eth2_network_config.genesis_state_is_known() { + let genesis_state = eth2_network_config + .genesis_state::(genesis_state_url.as_deref(), genesis_state_url_timeout, &logger).await? + .ok_or_else(|| { + "The genesis state for this network is not known, this is an unsupported mode" + .to_string() + })?; slog::info!(logger, "Genesis state found"; "root" => genesis_state.canonical_root().to_string()); let enr_fork = spec.enr_fork_id::( @@ -143,7 +128,7 @@ impl BootNodeConfig { let mut builder = create_enr_builder_from_config(&network_config, enable_tcp); // If we know of the ENR field, add it to the initial construction if let Some(enr_fork_bytes) = enr_fork { - builder.add_value("eth2", enr_fork_bytes.as_slice()); + builder.add_value("eth2", &enr_fork_bytes); } builder .build(&local_key) @@ -155,7 +140,6 @@ impl BootNodeConfig { }; Ok(BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -170,7 +154,8 @@ impl BootNodeConfig { /// Its fields are a subset of the fields of `BootNodeConfig`, some of them are copied from `Discv5Config`. #[derive(Serialize, Deserialize)] pub struct BootNodeConfigSerialization { - pub listen_socket: SocketAddr, + pub ipv4_listen_socket: Option, + pub ipv6_listen_socket: Option, // TODO: Generalise to multiaddr pub boot_nodes: Vec, pub local_enr: Enr, @@ -183,7 +168,6 @@ impl BootNodeConfigSerialization { /// relevant fields of `config` pub fn from_config_ref(config: &BootNodeConfig) -> Self { let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key: _, @@ -191,8 +175,27 @@ impl BootNodeConfigSerialization { phantom: _, } = config; + let (ipv4_listen_socket, ipv6_listen_socket) = match discv5_config.listen_config { + lighthouse_network::discv5::ListenConfig::Ipv4 { ip, port } => { + (Some(SocketAddrV4::new(ip, port)), None) + } + lighthouse_network::discv5::ListenConfig::Ipv6 { ip, port } => { + (None, Some(SocketAddrV6::new(ip, port, 0, 0))) + } + lighthouse_network::discv5::ListenConfig::DualStack { + ipv4, + ipv4_port, + ipv6, + ipv6_port, + } => ( + Some(SocketAddrV4::new(ipv4, ipv4_port)), + Some(SocketAddrV6::new(ipv6, ipv6_port, 0, 0)), + ), + }; + BootNodeConfigSerialization { - listen_socket: *listen_socket, + ipv4_listen_socket, + ipv6_listen_socket, boot_nodes: boot_nodes.clone(), local_enr: local_enr.clone(), disable_packet_filter: !discv5_config.enable_packet_filter, diff --git a/boot_node/src/lib.rs b/boot_node/src/lib.rs index 3d9dada0f..d76e7906b 100644 --- a/boot_node/src/lib.rs +++ b/boot_node/src/lib.rs @@ -7,7 +7,7 @@ mod cli; pub mod config; mod server; pub use cli::cli_app; -use config::{BootNodeConfig, BootNodeConfigSerialization}; +use config::BootNodeConfig; use types::{EthSpec, EthSpecId}; const LOG_CHANNEL_SIZE: usize = 2048; @@ -81,20 +81,13 @@ fn main( .build() .map_err(|e| format!("Failed to build runtime: {}", e))?; - // parse the CLI args into a useable config - let config: BootNodeConfig = BootNodeConfig::new(bn_matches, eth2_network_config)?; - - // Dump configs if `dump-config` or `dump-chain-config` flags are set - let config_sz = BootNodeConfigSerialization::from_config_ref(&config); - clap_utils::check_dump_configs::<_, T>( - lh_matches, - &config_sz, - ð2_network_config.chain_spec::()?, - )?; - // Run the boot node - if !lh_matches.is_present("immediate-shutdown") { - runtime.block_on(server::run(config, log)); - } + runtime.block_on(server::run::( + lh_matches, + bn_matches, + eth2_network_config, + log, + ))?; + Ok(()) } diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 3f5419c2c..5a5729dc0 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -1,6 +1,9 @@ //! The main bootnode server execution. use super::BootNodeConfig; +use crate::config::BootNodeConfigSerialization; +use clap::ArgMatches; +use eth2_network_config::Eth2NetworkConfig; use lighthouse_network::{ discv5::{enr::NodeId, Discv5, Discv5Event}, EnrExt, Eth2Enr, @@ -8,9 +11,28 @@ use lighthouse_network::{ use slog::info; use types::EthSpec; -pub async fn run(config: BootNodeConfig, log: slog::Logger) { +pub async fn run( + lh_matches: &ArgMatches<'_>, + bn_matches: &ArgMatches<'_>, + eth2_network_config: &Eth2NetworkConfig, + log: slog::Logger, +) -> Result<(), String> { + // parse the CLI args into a useable config + let config: BootNodeConfig = BootNodeConfig::new(bn_matches, eth2_network_config).await?; + + // Dump configs if `dump-config` or `dump-chain-config` flags are set + let config_sz = BootNodeConfigSerialization::from_config_ref(&config); + clap_utils::check_dump_configs::<_, T>( + lh_matches, + &config_sz, + ð2_network_config.chain_spec::()?, + )?; + + if lh_matches.is_present("immediate-shutdown") { + return Ok(()); + } + let BootNodeConfig { - listen_socket, boot_nodes, local_enr, local_key, @@ -31,7 +53,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { let pretty_v6_socket = enr_v6_socket.as_ref().map(|addr| addr.to_string()); info!( log, "Configuration parameters"; - "listening_address" => %listen_socket, + "listening_address" => ?discv5_config.listen_config, "advertised_v4_address" => ?pretty_v4_socket, "advertised_v6_address" => ?pretty_v6_socket, "eth2" => eth2_field @@ -41,6 +63,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { // build the contactable multiaddr list, adding the p2p protocol info!(log, "Contact information"; "enr" => local_enr.to_base64()); + info!(log, "Enr details"; "enr" => ?local_enr); info!(log, "Contact information"; "multiaddrs" => ?local_enr.multiaddr_p2p()); // construct the discv5 server @@ -64,9 +87,8 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { } // start the server - if let Err(e) = discv5.start(listen_socket).await { - slog::crit!(log, "Could not start discv5 server"; "error" => %e); - return; + if let Err(e) = discv5.start().await { + return Err(format!("Could not start discv5 server: {e:?}")); } // if there are peers in the local routing table, establish a session by running a query @@ -82,8 +104,7 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { let mut event_stream = match discv5.event_stream().await { Ok(stream) => stream, Err(e) => { - slog::crit!(log, "Failed to obtain event stream"; "error" => %e); - return; + return Err(format!("Failed to obtain event stream: {e:?}")); } }; diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index ccff88cee..10113ab32 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -2,22 +2,22 @@ name = "account_utils" version = "0.1.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.8.5" -eth2_wallet = { path = "../../crypto/eth2_wallet" } -eth2_keystore = { path = "../../crypto/eth2_keystore" } -filesystem = { path = "../filesystem" } -zeroize = { version = "1.4.2", features = ["zeroize_derive"] } -serde = "1.0.116" +rand = { workspace = true } +eth2_wallet = { workspace = true } +eth2_keystore = { workspace = true } +filesystem = { workspace = true } +zeroize = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -serde_yaml = "0.8.13" -slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } -types = { path = "../../consensus/types" } -validator_dir = { path = "../validator_dir" } -regex = "1.5.5" +serde_yaml = { workspace = true } +slog = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } +regex = { workspace = true } rpassword = "5.0.0" -directory = { path = "../directory" } +directory = { workspace = true } diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 89de38038..e566d7cdd 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -13,6 +13,9 @@ use std::fs::{self, File}; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; +use std::str::from_utf8; +use std::thread::sleep; +use std::time::Duration; use zeroize::Zeroize; pub mod validator_definitions; @@ -30,6 +33,8 @@ pub const MINIMUM_PASSWORD_LEN: usize = 12; /// array of length 32. const DEFAULT_PASSWORD_LEN: usize = 48; +pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; + /// Returns the "default" path where a wallet should store its password file. pub fn default_wallet_password_path>(wallet_name: &str, secrets_dir: P) -> PathBuf { secrets_dir.as_ref().join(format!("{}.pass", wallet_name)) @@ -59,6 +64,18 @@ pub fn read_password>(path: P) -> Result { fs::read(path).map(strip_off_newlines).map(Into::into) } +/// Reads a password file into a `ZeroizeString` struct, with new-lines removed. +pub fn read_password_string>(path: P) -> Result { + fs::read(path) + .map_err(|e| format!("Error opening file: {:?}", e)) + .map(strip_off_newlines) + .and_then(|bytes| { + String::from_utf8(bytes) + .map_err(|e| format!("Error decoding utf8: {:?}", e)) + .map(Into::into) + }) +} + /// Write a file atomically by using a temporary file as an intermediate. /// /// Care is taken to preserve the permissions of the file at `file_path` being written. @@ -220,6 +237,46 @@ impl AsRef<[u8]> for ZeroizeString { } } +pub fn read_mnemonic_from_cli( + mnemonic_path: Option, + stdin_inputs: bool, +) -> Result { + let mnemonic = match mnemonic_path { + Some(path) => fs::read(&path) + .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) + .and_then(|bytes| { + let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); + let phrase = from_utf8(bytes_no_newlines.as_ref()) + .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; + Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + phrase, e + ) + }) + })?, + None => loop { + eprintln!(); + eprintln!("{}", MNEMONIC_PROMPT); + + let mnemonic = read_input_from_user(stdin_inputs)?; + + match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { + Ok(mnemonic_m) => { + eprintln!("Valid mnemonic provided."); + eprintln!(); + sleep(Duration::from_secs(1)); + break mnemonic_m; + } + Err(_) => { + eprintln!("Invalid mnemonic"); + } + } + }, + }; + Ok(mnemonic) +} + #[cfg(test)] mod test { use super::*; diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 6ce2517fb..c91e717d1 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -3,7 +3,9 @@ //! Serves as the source-of-truth of which validators this validator client should attempt (or not //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. -use crate::{default_keystore_password_path, write_file_via_temporary, ZeroizeString}; +use crate::{ + default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString, +}; use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; @@ -43,6 +45,18 @@ pub enum Error { UnableToOpenKeystore(eth2_keystore::Error), /// The validator directory could not be created. UnableToCreateValidatorDir(PathBuf), + UnableToReadKeystorePassword(String), + KeystoreWithoutPassword, +} + +/// Defines how a password for a validator keystore will be persisted. +pub enum PasswordStorage { + /// Store the password in the `validator_definitions.yml` file. + ValidatorDefinitions(ZeroizeString), + /// Store the password in a separate, dedicated file (likely in the "secrets" directory). + File(PathBuf), + /// Don't store the password at all. + None, } #[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] @@ -92,6 +106,34 @@ impl SigningDefinition { pub fn is_local_keystore(&self) -> bool { matches!(self, SigningDefinition::LocalKeystore { .. }) } + + pub fn voting_keystore_password(&self) -> Result, Error> { + match self { + SigningDefinition::LocalKeystore { + voting_keystore_password: Some(password), + .. + } => Ok(Some(password.clone())), + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => read_password_string(path) + .map(Into::into) + .map(Option::Some) + .map_err(Error::UnableToReadKeystorePassword), + SigningDefinition::LocalKeystore { .. } => Err(Error::KeystoreWithoutPassword), + SigningDefinition::Web3Signer(_) => Ok(None), + } + } + + pub fn voting_keystore_password_path(&self) -> Option<&PathBuf> { + match self { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => Some(path), + _ => None, + } + } } /// A validator that may be initialized by this validator client. @@ -129,7 +171,7 @@ impl ValidatorDefinition { /// This function does not check the password against the keystore. pub fn new_keystore_with_password>( voting_keystore_path: P, - voting_keystore_password: Option, + voting_keystore_password_storage: PasswordStorage, graffiti: Option, suggested_fee_recipient: Option
, gas_limit: Option, @@ -139,6 +181,12 @@ impl ValidatorDefinition { let keystore = Keystore::from_json_file(&voting_keystore_path).map_err(Error::UnableToOpenKeystore)?; let voting_public_key = keystore.public_key().ok_or(Error::InvalidKeystorePubkey)?; + let (voting_keystore_password_path, voting_keystore_password) = + match voting_keystore_password_storage { + PasswordStorage::ValidatorDefinitions(password) => (None, Some(password)), + PasswordStorage::File(path) => (Some(path), None), + PasswordStorage::None => (None, None), + }; Ok(ValidatorDefinition { enabled: true, @@ -150,7 +198,7 @@ impl ValidatorDefinition { builder_proposals, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, - voting_keystore_password_path: None, + voting_keystore_password_path, voting_keystore_password, }, }) @@ -346,6 +394,13 @@ impl ValidatorDefinitions { pub fn as_mut_slice(&mut self) -> &mut [ValidatorDefinition] { self.0.as_mut_slice() } + + // Returns an iterator over all the `voting_keystore_password_paths` in self. + pub fn iter_voting_keystore_password_paths(&self) -> impl Iterator { + self.0 + .iter() + .filter_map(|def| def.signing_definition.voting_keystore_password_path()) + } } /// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to diff --git a/common/clap_utils/Cargo.toml b/common/clap_utils/Cargo.toml index a882b7ce6..e4dfb2a55 100644 --- a/common/clap_utils/Cargo.toml +++ b/common/clap_utils/Cargo.toml @@ -2,18 +2,18 @@ name = "clap_utils" version = "0.1.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "2.33.3" -hex = "0.4.2" -dirs = "3.0.1" -eth2_network_config = { path = "../eth2_network_config" } -ethereum_ssz = "0.5.0" -ethereum-types = "0.14.1" -serde = "1.0.116" -serde_json = "1.0.59" -serde_yaml = "0.8.13" -types = { path = "../../consensus/types"} +clap = { workspace = true } +hex = { workspace = true } +dirs = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum-types = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +types = { workspace = true } diff --git a/common/compare_fields/Cargo.toml b/common/compare_fields/Cargo.toml index 58527b571..8df989e72 100644 --- a/common/compare_fields/Cargo.toml +++ b/common/compare_fields/Cargo.toml @@ -2,10 +2,10 @@ name = "compare_fields" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -compare_fields_derive = { path = "../compare_fields_derive" } +compare_fields_derive = { workspace = true } [package.metadata.cargo-udeps.ignore] development = ["compare_fields_derive"] # used in doc-tests diff --git a/common/compare_fields_derive/Cargo.toml b/common/compare_fields_derive/Cargo.toml index 7696d3606..b4bbbaa43 100644 --- a/common/compare_fields_derive/Cargo.toml +++ b/common/compare_fields_derive/Cargo.toml @@ -2,11 +2,11 @@ name = "compare_fields_derive" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [lib] proc-macro = true [dependencies] -syn = "1.0.42" -quote = "1.0.7" +syn = { workspace = true } +quote = { workspace = true } diff --git a/common/deposit_contract/Cargo.toml b/common/deposit_contract/Cargo.toml index aabc07fc5..a03ac2178 100644 --- a/common/deposit_contract/Cargo.toml +++ b/common/deposit_contract/Cargo.toml @@ -2,18 +2,18 @@ name = "deposit_contract" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } build = "build.rs" [build-dependencies] -reqwest = { version = "0.11.0", features = ["blocking", "json", "native-tls-vendored"] } -serde_json = "1.0.58" -sha2 = "0.10" -hex = "0.4.2" +reqwest = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } [dependencies] -types = { path = "../../consensus/types"} -ethereum_ssz = "0.5.0" -tree_hash = "0.5.0" +types = { workspace = true } +ethereum_ssz = { workspace = true } +tree_hash = { workspace = true } ethabi = "16.0.0" diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml index f7b77ab7b..f72433726 100644 --- a/common/directory/Cargo.toml +++ b/common/directory/Cargo.toml @@ -2,11 +2,11 @@ name = "directory" version = "0.1.0" authors = ["pawan "] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "2.33.3" -clap_utils = {path = "../clap_utils"} -eth2_network_config = { path = "../eth2_network_config" } +clap = { workspace = true } +clap_utils = { workspace = true } +eth2_network_config = { workspace = true } diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 2c5e7060b..f2911fd3d 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -2,36 +2,41 @@ name = "eth2" version = "0.1.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { version = "1.0.116", features = ["derive"] } -serde_json = "1.0.58" -types = { path = "../../consensus/types" } -reqwest = { version = "0.11.0", features = ["json","stream"] } -lighthouse_network = { path = "../../beacon_node/lighthouse_network" } -proto_array = { path = "../../consensus/proto_array", optional = true } -ethereum_serde_utils = "0.5.0" -eth2_keystore = { path = "../../crypto/eth2_keystore" } -libsecp256k1 = "0.7.0" -ring = "0.16.19" -bytes = "1.0.1" -account_utils = { path = "../../common/account_utils" } -sensitive_url = { path = "../../common/sensitive_url" } -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" +serde = { workspace = true } +serde_json = { workspace = true } +types = { workspace = true } +reqwest = { workspace = true } +lighthouse_network = { workspace = true } +proto_array = { workspace = true } +ethereum_serde_utils = { workspace = true } +eth2_keystore = { workspace = true } +libsecp256k1 = { workspace = true } +ring = { workspace = true } +bytes = { workspace = true } +account_utils = { workspace = true } +sensitive_url = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } futures-util = "0.3.8" -futures = "0.3.8" -store = { path = "../../beacon_node/store", optional = true } -slashing_protection = { path = "../../validator_client/slashing_protection", optional = true } +futures = { workspace = true } +store = { workspace = true } +slashing_protection = { workspace = true } +mediatype = "0.19.13" mime = "0.3.16" +pretty_reqwest_error = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.2", optional = true } -procinfo = { version = "0.4.2", optional = true } +procfs = { version = "0.15.1", optional = true } [features] default = ["lighthouse"] -lighthouse = ["proto_array", "psutil", "procinfo", "store", "slashing_protection"] +lighthouse = ["psutil", "procfs"] diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e03cc2e9b..5b43ee3a7 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -19,11 +19,16 @@ use self::types::{Error as ResponseError, *}; use futures::Stream; use futures_util::StreamExt; use lighthouse_network::PeerId; +use pretty_reqwest_error::PrettyReqwestError; pub use reqwest; -use reqwest::{IntoUrl, RequestBuilder, Response}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + Body, IntoUrl, RequestBuilder, Response, +}; pub use reqwest::{StatusCode, Url}; pub use sensitive_url::{SensitiveError, SensitiveUrl}; use serde::{de::DeserializeOwned, Serialize}; +use ssz::Encode; use std::convert::TryFrom; use std::fmt; use std::iter::Iterator; @@ -39,7 +44,7 @@ pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; #[derive(Debug)] pub enum Error { /// The `reqwest` client raised an error. - Reqwest(reqwest::Error), + HttpClient(PrettyReqwestError), /// 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. @@ -70,7 +75,7 @@ pub enum Error { impl From for Error { fn from(error: reqwest::Error) -> Self { - Error::Reqwest(error) + Error::HttpClient(error.into()) } } @@ -78,7 +83,7 @@ impl Error { /// If the error has a HTTP status code, return it. pub fn status(&self) -> Option { match self { - Error::Reqwest(error) => error.status(), + Error::HttpClient(error) => error.inner().status(), Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), @@ -115,6 +120,7 @@ pub struct Timeouts { pub get_beacon_blocks_ssz: Duration, pub get_debug_beacon_states: Duration, pub get_deposit_snapshot: Duration, + pub get_validator_block_ssz: Duration, } impl Timeouts { @@ -130,6 +136,7 @@ impl Timeouts { get_beacon_blocks_ssz: timeout, get_debug_beacon_states: timeout, get_deposit_snapshot: timeout, + get_validator_block_ssz: timeout, } } } @@ -218,7 +225,11 @@ impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_opt(&self, url: U) -> Result, Error> { - match self.get_response(url, |b| b).await.optional()? { + match self + .get_response(url, |b| b.accept(Accept::Json)) + .await + .optional()? + { Some(response) => Ok(Some(response.json().await?)), None => Ok(None), } @@ -231,7 +242,7 @@ impl BeaconNodeHttpClient { timeout: Duration, ) -> Result, Error> { let opt_response = self - .get_response(url, |b| b.timeout(timeout)) + .get_response(url, |b| b.timeout(timeout).accept(Accept::Json)) .await .optional()?; match opt_response { @@ -274,7 +285,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Into::into) } /// Perform a HTTP POST request with a custom timeout. @@ -299,7 +310,7 @@ impl BeaconNodeHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Generic POST function supporting arbitrary responses and timeouts. @@ -317,6 +328,70 @@ impl BeaconNodeHttpClient { ok_or_error(response).await } + /// Generic POST function supporting arbitrary responses and timeouts. + async fn post_generic_with_ssz_body, U: IntoUrl>( + &self, + url: U, + body: T, + timeout: Option, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let response = builder + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .await?; + ok_or_error(response).await + } + + /// Generic POST function supporting arbitrary responses and timeouts. + async fn post_generic_with_consensus_version( + &self, + url: U, + body: &T, + timeout: Option, + fork: ForkName, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let response = builder + .header(CONSENSUS_VERSION_HEADER, fork.to_string()) + .json(body) + .send() + .await?; + ok_or_error(response).await + } + + /// Generic POST function supporting arbitrary responses and timeouts. + async fn post_generic_with_consensus_version_and_ssz_body, U: IntoUrl>( + &self, + url: U, + body: T, + timeout: Option, + fork: ForkName, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&fork.to_string()).expect("Failed to create header value"), + ); + headers.insert( + "Content-Type", + HeaderValue::from_static("application/octet-stream"), + ); + let response = builder.headers(headers).body(body).send().await?; + ok_or_error(response).await + } + /// `GET beacon/genesis` /// /// ## Errors @@ -629,6 +704,26 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/blocks` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_blocks_ssz>( + &self, + block: &SignedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks"); + + self.post_generic_with_ssz_body(path, block.as_ssz_bytes(), Some(self.timeouts.proposal)) + .await?; + + Ok(()) + } + /// `POST beacon/blinded_blocks` /// /// Returns `Ok(None)` on a 404 error. @@ -649,6 +744,130 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST beacon/blinded_blocks` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_blinded_blocks_ssz>( + &self, + block: &SignedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blinded_blocks"); + + self.post_generic_with_ssz_body(path, block.as_ssz_bytes(), Some(self.timeouts.proposal)) + .await?; + + Ok(()) + } + + pub fn post_beacon_blocks_v2_path( + &self, + validation_level: Option, + ) -> Result { + let mut path = self.eth_path(V2)?; + path.path_segments_mut() + .map_err(|_| Error::InvalidUrl(self.server.clone()))? + .extend(&["beacon", "blocks"]); + + path.set_query( + validation_level + .map(|v| format!("broadcast_validation={}", v)) + .as_deref(), + ); + + Ok(path) + } + + pub fn post_beacon_blinded_blocks_v2_path( + &self, + validation_level: Option, + ) -> Result { + let mut path = self.eth_path(V2)?; + path.path_segments_mut() + .map_err(|_| Error::InvalidUrl(self.server.clone()))? + .extend(&["beacon", "blinded_blocks"]); + + path.set_query( + validation_level + .map(|v| format!("broadcast_validation={}", v)) + .as_deref(), + ); + + Ok(path) + } + + /// `POST v2/beacon/blocks` + pub async fn post_beacon_blocks_v2>( + &self, + block: &SignedBeaconBlock, + validation_level: Option, + ) -> Result<(), Error> { + self.post_generic_with_consensus_version( + self.post_beacon_blocks_v2_path(validation_level)?, + block, + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + + /// `POST v2/beacon/blocks` + pub async fn post_beacon_blocks_v2_ssz>( + &self, + block: &SignedBeaconBlock, + validation_level: Option, + ) -> Result<(), Error> { + self.post_generic_with_consensus_version_and_ssz_body( + self.post_beacon_blocks_v2_path(validation_level)?, + block.as_ssz_bytes(), + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + + /// `POST v2/beacon/blinded_blocks` + pub async fn post_beacon_blinded_blocks_v2( + &self, + block: &SignedBlindedBeaconBlock, + validation_level: Option, + ) -> Result<(), Error> { + self.post_generic_with_consensus_version( + self.post_beacon_blinded_blocks_v2_path(validation_level)?, + block, + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + + /// `POST v2/beacon/blinded_blocks` + pub async fn post_beacon_blinded_blocks_v2_ssz( + &self, + 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)?, + block.as_ssz_bytes(), + Some(self.timeouts.proposal), + block.message().body().fork_name(), + ) + .await?; + + Ok(()) + } + /// Path for `v2/beacon/blocks` pub fn get_beacon_blocks_path(&self, block_id: BlockId) -> Result { let mut path = self.eth_path(V2)?; @@ -982,16 +1201,14 @@ impl BeaconNodeHttpClient { /// `GET beacon/deposit_snapshot` pub async fn get_deposit_snapshot(&self) -> Result, Error> { - use ssz::Decode; 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_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_deposit_snapshot) - .await? - .map(|bytes| DepositTreeSnapshot::from_ssz_bytes(&bytes).map_err(Error::InvalidSsz)) - .transpose() + self.get_opt_with_timeout::, _>(path, self.timeouts.get_deposit_snapshot) + .await + .map(|opt| opt.map(|r| r.data)) } /// `POST beacon/rewards/sync_committee` @@ -1046,6 +1263,23 @@ impl BeaconNodeHttpClient { Ok(()) } + // GET builder/states/{state_id}/expected_withdrawals + pub async fn get_expected_withdrawals( + &self, + state_id: &StateId, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("builder") + .push("states") + .push(&state_id.to_string()) + .push("expected_withdrawals"); + + self.get(path).await + } + /// `POST validator/contribution_and_proofs` pub async fn post_validator_contribution_and_proofs( &self, @@ -1366,14 +1600,14 @@ impl BeaconNodeHttpClient { .await } - /// `GET v2/validator/blocks/{slot}` - pub async fn get_validator_blocks_modular>( + /// returns `GET v2/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_path>( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result { let mut path = self.eth_path(V2)?; path.path_segments_mut() @@ -1395,9 +1629,66 @@ impl BeaconNodeHttpClient { .append_pair("skip_randao_verification", ""); } + Ok(path) + } + + /// `GET v2/validator/blocks/{slot}` + pub async fn get_validator_blocks_modular>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result>, Error> { + let path = self + .get_validator_blocks_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + self.get(path).await } + /// `GET v2/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_ssz>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + ) -> Result>, Error> { + self.get_validator_blocks_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + ) + .await + } + + /// `GET v2/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_modular_ssz>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result>, Error> { + let path = self + .get_validator_blocks_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + + self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block_ssz) + .await + } + /// `GET v2/validator/blinded_blocks/{slot}` pub async fn get_validator_blinded_blocks>( &self, @@ -1414,17 +1705,14 @@ impl BeaconNodeHttpClient { .await } - /// `GET v1/validator/blinded_blocks/{slot}` - pub async fn get_validator_blinded_blocks_modular< - T: EthSpec, - Payload: AbstractExecPayload, - >( + /// returns `GET v1/validator/blinded_blocks/{slot}` URL path + pub async fn get_validator_blinded_blocks_path>( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result { let mut path = self.eth_path(V1)?; path.path_segments_mut() @@ -1446,9 +1734,71 @@ impl BeaconNodeHttpClient { .append_key_only("skip_randao_verification"); } + Ok(path) + } + + /// `GET v1/validator/blinded_blocks/{slot}` + pub async fn get_validator_blinded_blocks_modular< + T: EthSpec, + Payload: AbstractExecPayload, + >( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result>, Error> { + let path = self + .get_validator_blinded_blocks_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + self.get(path).await } + /// `GET v2/validator/blinded_blocks/{slot}` in ssz format + pub async fn get_validator_blinded_blocks_ssz>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + ) -> Result>, Error> { + self.get_validator_blinded_blocks_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + ) + .await + } + + pub async fn get_validator_blinded_blocks_modular_ssz< + T: EthSpec, + Payload: AbstractExecPayload, + >( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result>, Error> { + let path = self + .get_validator_blinded_blocks_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + + self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block_ssz) + .await + } + /// `GET validator/attestation_data?slot,committee_index` pub async fn get_validator_attestation_data( &self, @@ -1543,6 +1893,24 @@ impl BeaconNodeHttpClient { .await } + /// `POST validator/liveness/{epoch}` + pub async fn post_validator_liveness_epoch( + &self, + epoch: Epoch, + indices: Vec, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("liveness") + .push(&epoch.to_string()); + + self.post_with_timeout_and_response(path, &indices, self.timeouts.liveness) + .await + } + /// `POST validator/duties/attester/{epoch}` pub async fn post_validator_duties_attester( &self, @@ -1643,7 +2011,7 @@ impl BeaconNodeHttpClient { .bytes_stream() .map(|next| match next { Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), - Err(e) => Err(Error::Reqwest(e)), + Err(e) => Err(Error::HttpClient(e.into())), })) } diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index bb933dbe1..dfc19db49 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -95,8 +95,8 @@ pub struct ValidatorInclusionData { #[cfg(target_os = "linux")] use { - procinfo::pid, psutil::cpu::os::linux::CpuTimesExt, - psutil::memory::os::linux::VirtualMemoryExt, psutil::process::Process, + psutil::cpu::os::linux::CpuTimesExt, psutil::memory::os::linux::VirtualMemoryExt, + psutil::process::Process, }; /// Reports on the health of the Lighthouse instance. @@ -238,7 +238,7 @@ pub struct ProcessHealth { /// The pid of this process. pub pid: u32, /// The number of threads used by this pid. - pub pid_num_threads: i32, + pub pid_num_threads: i64, /// The total resident memory used by this pid. pub pid_mem_resident_set_size: u64, /// The total virtual memory used by this pid. @@ -262,7 +262,12 @@ impl ProcessHealth { .memory_info() .map_err(|e| format!("Unable to get process memory info: {:?}", e))?; - let stat = pid::stat_self().map_err(|e| format!("Unable to get stat: {:?}", e))?; + let me = procfs::process::Process::myself() + .map_err(|e| format!("Unable to get process: {:?}", e))?; + let stat = me + .stat() + .map_err(|e| format!("Unable to get stat: {:?}", e))?; + let process_times = process .cpu_times() .map_err(|e| format!("Unable to get process cpu times : {:?}", e))?; @@ -364,12 +369,12 @@ pub struct DatabaseInfo { impl BeaconNodeHttpClient { /// Perform a HTTP GET request, returning `None` on a 404 error. async fn get_bytes_opt(&self, url: U) -> Result>, Error> { - let response = self.client.get(url).send().await.map_err(Error::Reqwest)?; + let response = self.client.get(url).send().await.map_err(Error::from)?; match ok_or_error(response).await { Ok(resp) => Ok(Some( resp.bytes() .await - .map_err(Error::Reqwest)? + .map_err(Error::from)? .into_iter() .collect::>(), )), diff --git a/common/eth2/src/lighthouse/attestation_rewards.rs b/common/eth2/src/lighthouse/attestation_rewards.rs index bebd1c661..fa3f93d06 100644 --- a/common/eth2/src/lighthouse/attestation_rewards.rs +++ b/common/eth2/src/lighthouse/attestation_rewards.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_utils::quoted_u64::Quoted; // Details about the rewards paid for attestations // All rewards in GWei @@ -17,6 +18,12 @@ pub struct IdealAttestationRewards { // Ideal attester's reward for source vote in gwei #[serde(with = "serde_utils::quoted_u64")] pub source: u64, + // Ideal attester's inclusion_delay reward in gwei (phase0 only) + #[serde(skip_serializing_if = "Option::is_none")] + pub inclusion_delay: Option>, + // Ideal attester's inactivity penalty in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub inactivity: i64, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] @@ -25,16 +32,20 @@ pub struct TotalAttestationRewards { #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, // attester's reward for head vote in gwei - #[serde(with = "serde_utils::quoted_u64")] - pub head: u64, + #[serde(with = "serde_utils::quoted_i64")] + pub head: i64, // attester's reward for target vote in gwei #[serde(with = "serde_utils::quoted_i64")] pub target: i64, // attester's reward for source vote in gwei #[serde(with = "serde_utils::quoted_i64")] pub source: i64, - // TBD attester's inclusion_delay reward in gwei (phase0 only) - // pub inclusion_delay: u64, + // attester's inclusion_delay reward in gwei (phase0 only) + #[serde(skip_serializing_if = "Option::is_none")] + pub inclusion_delay: Option>, + // attester's inactivity penalty in gwei + #[serde(with = "serde_utils::quoted_i64")] + pub inactivity: i64, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index e576cfcb3..b2d53c5e0 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -16,6 +16,7 @@ use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; +use types::graffiti::GraffitiString; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). @@ -169,7 +170,7 @@ impl ValidatorClientHttpClient { .map_err(|_| Error::InvalidSignatureHeader)? .to_string(); - let body = response.bytes().await.map_err(Error::Reqwest)?; + let body = response.bytes().await.map_err(Error::from)?; let message = Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes"); @@ -221,7 +222,7 @@ impl ValidatorClientHttpClient { .headers(self.headers()?) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -235,7 +236,7 @@ impl ValidatorClientHttpClient { .await? .json() .await - .map_err(Error::Reqwest) + .map_err(Error::from) } /// Perform a HTTP GET request, returning `None` on a 404 error. @@ -265,7 +266,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -296,7 +297,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; let response = ok_or_error(response).await?; self.signed_body(response).await?; Ok(()) @@ -315,7 +316,7 @@ impl ValidatorClientHttpClient { .json(body) .send() .await - .map_err(Error::Reqwest)?; + .map_err(Error::from)?; ok_or_error(response).await } @@ -467,6 +468,7 @@ impl ValidatorClientHttpClient { enabled: Option, gas_limit: Option, builder_proposals: Option, + graffiti: Option, ) -> Result<(), Error> { let mut path = self.server.full.clone(); @@ -482,11 +484,27 @@ impl ValidatorClientHttpClient { enabled, gas_limit, builder_proposals, + graffiti, }, ) .await } + /// `DELETE eth/v1/keystores` + pub async fn delete_lighthouse_keystores( + &self, + req: &DeleteKeystoresRequest, + ) -> Result { + let mut path = self.server.full.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("keystores"); + + self.delete_with_unsigned_response(path, req).await + } + fn make_keystores_url(&self) -> Result { let mut url = self.server.full.clone(); url.path_segments_mut() @@ -648,7 +666,7 @@ impl ValidatorClientHttpClient { &self, pubkey: &PublicKeyBytes, epoch: Option, - ) -> Result { + ) -> Result, Error> { let mut path = self.server.full.clone(); path.path_segments_mut() diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index 0d67df47a..33e2f764e 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -1,9 +1,10 @@ use account_utils::ZeroizeString; use eth2_keystore::Keystore; use serde::{Deserialize, Serialize}; -use slashing_protection::interchange::Interchange; use types::{Address, PublicKeyBytes}; +pub use slashing_protection::interchange::Interchange; + #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct GetFeeRecipientResponse { pub pubkey: PublicKeyBytes, @@ -27,7 +28,7 @@ pub struct ListKeystoresResponse { pub data: Vec, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct SingleKeystoreResponse { pub validating_pubkey: PublicKeyBytes, pub derivation_path: Option, diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index dd2ed0322..f1a91b4ef 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -83,6 +83,9 @@ pub struct ValidatorPatchRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub builder_proposals: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub graffiti: Option, } #[derive(Clone, PartialEq, Serialize, Deserialize)] @@ -149,3 +152,19 @@ pub struct UpdateGasLimitRequest { pub struct VoluntaryExitQuery { pub epoch: Option, } + +#[derive(Deserialize, Serialize)] +pub struct ExportKeystoresResponse { + pub data: Vec, + #[serde(with = "serde_utils::json_str")] + pub slashing_protection: Interchange, +} + +#[derive(Deserialize, Serialize)] +pub struct SingleExportKeystoresResponse { + pub status: Status, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore_password: Option, +} diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index d7150bff7..822f88179 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -3,11 +3,10 @@ use crate::Error as ServerError; use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; -use mime::{Mime, APPLICATION, JSON, OCTET_STREAM, STAR}; +use mediatype::{names, MediaType, MediaTypeList}; use serde::{Deserialize, Serialize}; -use std::cmp::Reverse; use std::convert::TryFrom; -use std::fmt; +use std::fmt::{self, Display}; use std::str::{from_utf8, FromStr}; use std::time::Duration; pub use types::*; @@ -582,6 +581,11 @@ pub struct SyncingData { pub sync_distance: Slot, } +#[derive(Serialize, Deserialize)] +pub struct ExpectedWithdrawalsQuery { + pub proposal_slot: Option, +} + #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(try_from = "String", bound = "T: FromStr")] pub struct QueryVec { @@ -1172,35 +1176,65 @@ impl FromStr for Accept { type Err = String; fn from_str(s: &str) -> Result { - let mut mimes = parse_accept(s)?; + let media_type_list = MediaTypeList::new(s); // [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 // find the highest q-factor supported accept type - mimes.sort_by_key(|m| { - Reverse(m.get_param("q").map_or(1000_u16, |n| { - (n.as_ref().parse::().unwrap_or(0_f32) * 1000_f32) as u16 - })) + let mut highest_q = 0_u16; + let mut accept_type = None; + + const APPLICATION: &str = names::APPLICATION.as_str(); + const OCTET_STREAM: &str = names::OCTET_STREAM.as_str(); + const JSON: &str = names::JSON.as_str(); + const STAR: &str = names::_STAR.as_str(); + const Q: &str = names::Q.as_str(); + + media_type_list.into_iter().for_each(|item| { + if let Ok(MediaType { + ty, + subty, + suffix: _, + params, + }) = item + { + let q_accept = match (ty.as_str(), subty.as_str()) { + (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), + (APPLICATION, JSON) => Some(Accept::Json), + (STAR, STAR) => Some(Accept::Any), + _ => None, + } + .map(|item_accept_type| { + let q_val = params + .iter() + .find_map(|(n, v)| match n.as_str() { + Q => { + Some((v.as_str().parse::().unwrap_or(0_f32) * 1000_f32) as u16) + } + _ => None, + }) + .or(Some(1000_u16)); + + (q_val.unwrap(), item_accept_type) + }); + + match q_accept { + Some((q, accept)) if q > highest_q => { + highest_q = q; + accept_type = Some(accept); + } + _ => (), + } + } }); - mimes - .into_iter() - .find_map(|m| match (m.type_(), m.subtype()) { - (APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), - (APPLICATION, JSON) => Some(Accept::Json), - (STAR, STAR) => Some(Accept::Any), - _ => None, - }) - .ok_or_else(|| "accept header is not supported".to_string()) + accept_type.ok_or_else(|| "accept header is not supported".to_string()) } } -fn parse_accept(accept: &str) -> Result, String> { - accept - .split(',') - .map(|part| { - part.parse() - .map_err(|e| format!("error parsing Accept header: {}", e)) - }) - .collect() +#[derive(PartialEq, Debug, Serialize, Deserialize)] +pub struct StandardLivenessResponseData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub is_live: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -1238,6 +1272,70 @@ pub struct ForkChoiceNode { pub execution_block_hash: Option, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BroadcastValidation { + 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 { + Self::Gossip => write!(f, "gossip"), + Self::Consensus => write!(f, "consensus"), + Self::ConsensusAndEquivocation => write!(f, "consensus_and_equivocation"), + } + } +} + +impl FromStr for BroadcastValidation { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "gossip" => Ok(Self::Gossip), + "consensus" => Ok(Self::Consensus), + "consensus_and_equivocation" => Ok(Self::ConsensusAndEquivocation), + _ => Err("Invalid broadcast validation level"), + } + } +} + +#[derive(Default, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct BroadcastValidationQuery { + #[serde(default)] + pub broadcast_validation: BroadcastValidation, +} + +pub mod serde_status_code { + use crate::StatusCode; + use serde::{de::Error, Deserialize, Serialize}; + + pub fn serialize(status_code: &StatusCode, ser: S) -> Result + where + S: serde::Serializer, + { + status_code.as_u16().serialize(ser) + } + + pub fn deserialize<'de, D>(de: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let status_code = u16::deserialize(de)?; + StatusCode::try_from(status_code).map_err(D::Error::custom) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1268,6 +1366,11 @@ mod tests { assert_eq!( Accept::from_str("text/plain"), Err("accept header is not supported".to_string()) - ) + ); + + assert_eq!( + Accept::from_str("application/json;message=\"Hello, world!\";q=0.3,*/*;q=0.6").unwrap(), + Accept::Any + ); } } diff --git a/common/eth2_config/Cargo.toml b/common/eth2_config/Cargo.toml index 08f8c9a39..20c3b0b6f 100644 --- a/common/eth2_config/Cargo.toml +++ b/common/eth2_config/Cargo.toml @@ -2,8 +2,8 @@ name = "eth2_config" version = "0.2.0" authors = ["Paul Hauner "] -edition = "2021" +edition = { workspace = true } [dependencies] -types = { path = "../../consensus/types" } -paste = "1.0.5" +types = { workspace = true } +paste = { workspace = true } diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index 7e5506667..bf707c4d1 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -23,6 +23,23 @@ pub const PREDEFINED_NETWORKS_DIR: &str = predefined_networks_dir!(); pub const GENESIS_FILE_NAME: &str = "genesis.ssz"; pub const GENESIS_ZIP_FILE_NAME: &str = "genesis.ssz.zip"; +const HOLESKY_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url { + urls: &[ + // This is an AWS S3 bucket hosted by Sigma Prime. See Paul Hauner for + // more details. + "https://sigp-public-genesis-states.s3.ap-southeast-2.amazonaws.com/holesky/", + ], + checksum: "0xd750639607c337bbb192b15c27f447732267bf72d1650180a0e44c2d93a80741", + genesis_validators_root: "0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1", +}; + +const CHIADO_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url { + // No default checkpoint sources are provided. + urls: &[], + checksum: "0xd4a039454c7429f1dfaa7e11e397ef3d0f50d2d5e4c0e4dc04919d153aa13af1", + genesis_validators_root: "0x9d642dac73058fbf39c0ae41ab1e34e4d889043cb199851ded7095bc99eb4c1e", +}; + /// The core configuration of a Lighthouse beacon node. #[derive(Debug, Clone)] pub struct Eth2Config { @@ -62,6 +79,32 @@ impl Eth2Config { } } +/// Describes how a genesis state may be obtained. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum GenesisStateSource { + /// The genesis state for this network is not yet known. + Unknown, + /// The genesis state for this network is included in the binary via + /// `include_bytes!` or by loading from a testnet dir. + IncludedBytes, + /// The genesis state for this network should be downloaded from a URL. + Url { + /// URLs to try to download the file from, in order. + urls: &'static [&'static str], + /// The SHA256 of the genesis state bytes. This is *not* a hash tree + /// root to simplify the types (i.e., to avoid getting EthSpec + /// involved). + /// + /// The format should be 0x-prefixed ASCII bytes. + checksum: &'static str, + /// The `genesis_validators_root` of the genesis state. Used to avoid + /// downloading the state for simple signing operations. + /// + /// The format should be 0x-prefixed ASCII bytes. + genesis_validators_root: &'static str, + }, +} + /// A directory that can be built by downloading files via HTTP. /// /// Used by the `eth2_network_config` crate to initialize the network directories during build and @@ -70,7 +113,7 @@ impl Eth2Config { pub struct Eth2NetArchiveAndDirectory<'a> { pub name: &'a str, pub config_dir: &'a str, - pub genesis_is_known: bool, + pub genesis_state_source: GenesisStateSource, } impl<'a> Eth2NetArchiveAndDirectory<'a> { @@ -89,15 +132,11 @@ impl<'a> Eth2NetArchiveAndDirectory<'a> { } } -/// Indicates that the `genesis.ssz.zip` file is present on the filesystem. This means that the -/// deposit ceremony has concluded and the final genesis `BeaconState` is known. -const GENESIS_STATE_IS_KNOWN: bool = true; - -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct HardcodedNet { pub name: &'static str, pub config_dir: &'static str, - pub genesis_is_known: bool, + pub genesis_state_source: GenesisStateSource, pub config: &'static [u8], pub deploy_block: &'static [u8], pub boot_enr: &'static [u8], @@ -109,7 +148,7 @@ pub struct HardcodedNet { /// It also defines a `include__file!` macro which provides a wrapper around /// `std::include_bytes`, allowing the inclusion of bytes from the specific testnet directory. macro_rules! define_archive { - ($name_ident: ident, $config_dir: tt, $genesis_is_known: ident) => { + ($name_ident: ident, $config_dir: tt, $genesis_state_source: path) => { paste! { #[macro_use] pub mod $name_ident { @@ -118,7 +157,7 @@ macro_rules! define_archive { pub const ETH2_NET_DIR: Eth2NetArchiveAndDirectory = Eth2NetArchiveAndDirectory { name: stringify!($name_ident), config_dir: $config_dir, - genesis_is_known: $genesis_is_known, + genesis_state_source: $genesis_state_source, }; /// A wrapper around `std::include_bytes` which includes a file from a specific network @@ -151,7 +190,7 @@ macro_rules! define_net { $this_crate::HardcodedNet { name: ETH2_NET_DIR.name, config_dir: ETH2_NET_DIR.config_dir, - genesis_is_known: ETH2_NET_DIR.genesis_is_known, + genesis_state_source: ETH2_NET_DIR.genesis_state_source, config: $this_crate::$include_file!($this_crate, "../", "config.yaml"), deploy_block: $this_crate::$include_file!($this_crate, "../", "deploy_block.txt"), boot_enr: $this_crate::$include_file!($this_crate, "../", "boot_enr.yaml"), @@ -199,9 +238,9 @@ macro_rules! define_nets { /// `build.rs` which will unzip the genesis states. Then, that `eth2_network_configs` crate can /// perform the final step of using `std::include_bytes` to bake the files (bytes) into the binary. macro_rules! define_hardcoded_nets { - ($(($name_ident: ident, $config_dir: tt, $genesis_is_known: ident)),+) => { + ($(($name_ident: ident, $config_dir: tt, $genesis_state_source: path)),+) => { $( - define_archive!($name_ident, $config_dir, $genesis_is_known); + define_archive!($name_ident, $config_dir, $genesis_state_source); )+ pub const ETH2_NET_DIRS: &[Eth2NetArchiveAndDirectory<'static>] = &[$($name_ident::ETH2_NET_DIR,)+]; @@ -242,9 +281,8 @@ define_hardcoded_nets!( // The name of the directory in the `eth2_network_config/built_in_network_configs` // directory where the configuration files are located for this network. "mainnet", - // Set to `true` if the genesis state can be found in the `built_in_network_configs` - // directory. - GENESIS_STATE_IS_KNOWN + // Describes how the genesis state can be obtained. + GenesisStateSource::IncludedBytes ), ( // Network name (must be unique among all networks). @@ -252,9 +290,8 @@ define_hardcoded_nets!( // The name of the directory in the `eth2_network_config/built_in_network_configs` // directory where the configuration files are located for this network. "prater", - // Set to `true` if the genesis state can be found in the `built_in_network_configs` - // directory. - GENESIS_STATE_IS_KNOWN + // Describes how the genesis state can be obtained. + GenesisStateSource::IncludedBytes ), ( // Network name (must be unique among all networks). @@ -264,9 +301,8 @@ define_hardcoded_nets!( // // The Goerli network is effectively an alias to Prater. "prater", - // Set to `true` if the genesis state can be found in the `built_in_network_configs` - // directory. - GENESIS_STATE_IS_KNOWN + // Describes how the genesis state can be obtained. + GenesisStateSource::IncludedBytes ), ( // Network name (must be unique among all networks). @@ -274,9 +310,18 @@ define_hardcoded_nets!( // The name of the directory in the `eth2_network_config/built_in_network_configs` // directory where the configuration files are located for this network. "gnosis", + // Describes how the genesis state can be obtained. + GenesisStateSource::IncludedBytes + ), + ( + // Network name (must be unique among all networks). + chiado, + // The name of the directory in the `eth2_network_config/built_in_network_configs` + // directory where the configuration files are located for this network. + "chiado", // Set to `true` if the genesis state can be found in the `built_in_network_configs` // directory. - GENESIS_STATE_IS_KNOWN + CHIADO_GENESIS_STATE_SOURCE ), ( // Network name (must be unique among all networks). @@ -284,8 +329,16 @@ define_hardcoded_nets!( // The name of the directory in the `eth2_network_config/built_in_network_configs` // directory where the configuration files are located for this network. "sepolia", - // Set to `true` if the genesis state can be found in the `built_in_network_configs` - // directory. - GENESIS_STATE_IS_KNOWN + // Describes how the genesis state can be obtained. + GenesisStateSource::IncludedBytes + ), + ( + // Network name (must be unique among all networks). + holesky, + // The name of the directory in the `eth2_network_config/built_in_network_configs` + // directory where the configuration files are located for this network. + "holesky", + // Describes how the genesis state can be obtained. + HOLESKY_GENESIS_STATE_SOURCE ) ); diff --git a/common/eth2_interop_keypairs/Cargo.toml b/common/eth2_interop_keypairs/Cargo.toml index 7a376568e..ded62653e 100644 --- a/common/eth2_interop_keypairs/Cargo.toml +++ b/common/eth2_interop_keypairs/Cargo.toml @@ -2,19 +2,19 @@ name = "eth2_interop_keypairs" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lazy_static = "1.4.0" +lazy_static = { workspace = true } num-bigint = "0.4.2" -ethereum_hashing = "1.0.0-beta.2" -hex = "0.4.2" -serde_yaml = "0.8.13" -serde = "1.0.116" +ethereum_hashing = { workspace = true } +hex = { workspace = true } +serde_yaml = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -bls = { path = "../../crypto/bls" } +bls = { workspace = true } [dev-dependencies] base64 = "0.13.0" diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index f8382c95d..18dfe19da 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -2,20 +2,29 @@ name = "eth2_network_config" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } build = "build.rs" [build-dependencies] -zip = "0.5.8" -eth2_config = { path = "../eth2_config"} +zip = { workspace = true } +eth2_config = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } +tokio = { workspace = true } [dependencies] -serde_yaml = "0.8.13" -types = { path = "../../consensus/types"} -ethereum_ssz = "0.5.0" -eth2_config = { path = "../eth2_config"} -discv5 = "0.2.2" +serde_yaml = { workspace = true } +types = { workspace = true } +ethereum_ssz = { workspace = true } +eth2_config = { workspace = true } +discv5 = { workspace = true } +reqwest = { workspace = true } +pretty_reqwest_error = { workspace = true } +sha2 = { workspace = true } +url = { workspace = true } +sensitive_url = { workspace = true } +slog = { workspace = true } +logging = { workspace = true } +bytes = { workspace = true } diff --git a/common/eth2_network_config/build.rs b/common/eth2_network_config/build.rs index fa45fafa4..3165930f4 100644 --- a/common/eth2_network_config/build.rs +++ b/common/eth2_network_config/build.rs @@ -1,5 +1,7 @@ //! Extracts zipped genesis states on first run. -use eth2_config::{Eth2NetArchiveAndDirectory, ETH2_NET_DIRS, GENESIS_FILE_NAME}; +use eth2_config::{ + Eth2NetArchiveAndDirectory, GenesisStateSource, ETH2_NET_DIRS, GENESIS_FILE_NAME, +}; use std::fs::File; use std::io; use zip::ZipArchive; @@ -26,7 +28,7 @@ fn uncompress_state(network: &Eth2NetArchiveAndDirectory<'static>) -> Result<(), return Ok(()); } - if network.genesis_is_known { + if network.genesis_state_source == GenesisStateSource::IncludedBytes { // Extract genesis state from genesis.ssz.zip let archive_path = network.genesis_state_archive(); let archive_file = File::open(&archive_path) @@ -46,7 +48,8 @@ fn uncompress_state(network: &Eth2NetArchiveAndDirectory<'static>) -> Result<(), io::copy(&mut file, &mut outfile) .map_err(|e| format!("Error writing file {:?}: {}", genesis_ssz_path, e))?; } else { - // Create empty genesis.ssz if genesis is unknown + // Create empty genesis.ssz if genesis is unknown or to be downloaded via URL. + // This is a bit of a hack to make `include_bytes!` easier to deal with. File::create(genesis_ssz_path) .map_err(|e| format!("Failed to create {}: {}", GENESIS_FILE_NAME, e))?; } diff --git a/common/eth2_network_config/built_in_network_configs/chiado/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/chiado/boot_enr.yaml new file mode 100644 index 000000000..96baffde6 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/chiado/boot_enr.yaml @@ -0,0 +1,8 @@ +# chiado-teku-0 +- "enr:-Ly4QLYLNqrjvSxD3lpAPBUNlxa6cIbe79JqLZLFcZZjWoCjZcw-85agLUErHiygG2weRSCLnd5V460qTbLbwJQsfZkoh2F0dG5ldHOI__________-EZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhKq7mu-Jc2VjcDI1NmsxoQP900YAYa9kdvzlSKGjVo-F3XVzATjOYp3BsjLjSophO4hzeW5jbmV0cw-DdGNwgiMog3VkcIIjKA" +# chiado-teku-1 +- "enr:-Ly4QCGeYvTCNOGKi0mKRUd45rLj96b4pH98qG7B9TCUGXGpHZALtaL2-XfjASQyhbCqENccI4PGXVqYTIehNT9KJMQgh2F0dG5ldHOI__________-EZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhIuQrVSJc2VjcDI1NmsxoQP9iDchx2PGl3JyJ29B9fhLCvVMN6n23pPAIIeFV-sHOIhzeW5jbmV0cw-DdGNwgiMog3VkcIIjKA" +#GnosisDAO Bootnode: 3.71.132.231 +- "enr:-Ly4QAtr21x5Ps7HYhdZkIBRBgcBkvlIfEel1YNjtFWf4cV3au2LgBGICz9PtEs9-p2HUl_eME8m1WImxTxSB3AkCMwBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhANHhOeJc2VjcDI1NmsxoQNLp1QPV8-pyMCohOtj6xGtSBM_GtVTqzlbvNsCF4ezkYhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA" +#GnosisDAO Bootnode: 3.69.35.13 +- "enr:-Ly4QLgn8Bx6faigkKUGZQvd1HDToV2FAxZIiENK-lczruzQb90qJK-4E65ADly0s4__dQOW7IkLMW7ZAyJy2vtiLy8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAxNnBDAgAAb___________gmlkgnY0gmlwhANFIw2Jc2VjcDI1NmsxoQMa-fWEy9UJHfOl_lix3wdY5qust78sHAqZnWwEiyqKgYhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA" 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 new file mode 100644 index 000000000..47b285a65 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -0,0 +1,154 @@ +# Extends the mainnet preset +PRESET_BASE: gnosis +# needs to exist because of Prysm. Otherwise it conflicts with mainnet genesis +CONFIG_NAME: chiado + +# Genesis +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 6000 +# 10 October 2022 10:00:00 GMT+0000 +MIN_GENESIS_TIME: 1665396000 +GENESIS_DELAY: 300 + +# Projected time: 2022-11-04T15:00:00.000Z, block: 680928 +TERMINAL_TOTAL_DIFFICULTY: 231707791542740786049188744689299064356246512 + +# Deposit contract +# --------------------------------------------------------------- +# NOTE: Don't use a value too high, or Teku rejects it (4294906129 NOK) +DEPOSIT_CHAIN_ID: 10200 +DEPOSIT_NETWORK_ID: 10200 +DEPOSIT_CONTRACT_ADDRESS: 0xb97036A26259B7147018913bD58a774cf91acf25 + +# Misc +# --------------------------------------------------------------- +# 2**6 (= 64) +MAX_COMMITTEES_PER_SLOT: 64 +# 2**7 (= 128) +TARGET_COMMITTEE_SIZE: 128 +# 2**11 (= 2,048) +MAX_VALIDATORS_PER_COMMITTEE: 2048 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**12 (= 4096) +CHURN_LIMIT_QUOTIENT: 4096 +# See issue 563 +SHUFFLE_ROUND_COUNT: 90 +# 4 +HYSTERESIS_QUOTIENT: 4 +# 1 (minus 0.25) +HYSTERESIS_DOWNWARD_MULTIPLIER: 1 +# 5 (plus 1.25) +HYSTERESIS_UPWARD_MULTIPLIER: 5 +# Validator +# --------------------------------------------------------------- +# 2**10 (= 1024) ~1.4 hour +ETH1_FOLLOW_DISTANCE: 1024 +# 2**4 (= 16) +TARGET_AGGREGATORS_PER_COMMITTEE: 16 +# 2**0 (= 1) +RANDOM_SUBNETS_PER_VALIDATOR: 1 +# 2**8 (= 256) +EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION: 256 +# 6 (estimate from xDai mainnet) +SECONDS_PER_ETH1_BLOCK: 6 + +# Gwei values +# --------------------------------------------------------------- +# 2**0 * 10**9 (= 1,000,000,000) Gwei +MIN_DEPOSIT_AMOUNT: 1000000000 +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE: 32000000000 +# 2**4 * 10**9 (= 16,000,000,000) Gwei +EJECTION_BALANCE: 16000000000 +# 2**0 * 10**9 (= 1,000,000,000) Gwei +EFFECTIVE_BALANCE_INCREMENT: 1000000000 +# Initial values +# --------------------------------------------------------------- +# GBC area code +GENESIS_FORK_VERSION: 0x0000006f +BLS_WITHDRAWAL_PREFIX: 0x00 +# Time parameters +# --------------------------------------------------------------- +# 5 seconds +SECONDS_PER_SLOT: 5 +# 2**0 (= 1) slots 12 seconds +MIN_ATTESTATION_INCLUSION_DELAY: 1 +# 2**4 (= 16) slots 1.87 minutes +SLOTS_PER_EPOCH: 16 +# 2**0 (= 1) epochs 1.87 minutes +MIN_SEED_LOOKAHEAD: 1 +# 2**2 (= 4) epochs 7.47 minutes +MAX_SEED_LOOKAHEAD: 4 +# 2**6 (= 64) epochs ~2 hours +EPOCHS_PER_ETH1_VOTING_PERIOD: 64 +# 2**13 (= 8,192) slots ~15.9 hours +SLOTS_PER_HISTORICAL_ROOT: 8192 +# 2**8 (= 256) epochs ~8 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~8 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**2 (= 4) epochs 7.47 minutes +MIN_EPOCHS_TO_INACTIVITY_PENALTY: 4 + +# State vector lengths +# --------------------------------------------------------------- +# 2**16 (= 65,536) epochs ~85 days +EPOCHS_PER_HISTORICAL_VECTOR: 65536 +# 2**13 (= 8,192) epochs ~10.6 days +EPOCHS_PER_SLASHINGS_VECTOR: 8192 +# 2**24 (= 16,777,216) historical roots, ~15,243 years +HISTORICAL_ROOTS_LIMIT: 16777216 +# 2**40 (= 1,099,511,627,776) validator spots +VALIDATOR_REGISTRY_LIMIT: 1099511627776 +# Reward and penalty quotients +# --------------------------------------------------------------- +# 25 +BASE_REWARD_FACTOR: 25 +# 2**9 (= 512) +WHISTLEBLOWER_REWARD_QUOTIENT: 512 +# 2**3 (= 8) +PROPOSER_REWARD_QUOTIENT: 8 +# 2**26 (= 67,108,864) +INACTIVITY_PENALTY_QUOTIENT: 67108864 +# 2**7 (= 128) (lower safety margin at Phase 0 genesis) +MIN_SLASHING_PENALTY_QUOTIENT: 128 +# 1 (lower safety margin at Phase 0 genesis) +PROPORTIONAL_SLASHING_MULTIPLIER: 1 +# Max operations per block +# --------------------------------------------------------------- +# 2**4 (= 16) +MAX_PROPOSER_SLASHINGS: 16 +# 2**1 (= 2) +MAX_ATTESTER_SLASHINGS: 2 +# 2**7 (= 128) +MAX_ATTESTATIONS: 128 +# 2**4 (= 16) +MAX_DEPOSITS: 16 +# 2**4 (= 16) +MAX_VOLUNTARY_EXITS: 16 +# Signature domains +# --------------------------------------------------------------- +DOMAIN_BEACON_PROPOSER: 0x00000000 +DOMAIN_BEACON_ATTESTER: 0x01000000 +DOMAIN_RANDAO: 0x02000000 +DOMAIN_DEPOSIT: 0x03000000 +DOMAIN_VOLUNTARY_EXIT: 0x04000000 +DOMAIN_SELECTION_PROOF: 0x05000000 +DOMAIN_AGGREGATE_AND_PROOF: 0x06000000 +DOMAIN_SYNC_COMMITTEE: 0x07000000 +DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF: 0x08000000 +DOMAIN_CONTRIBUTION_AND_PROOF: 0x09000000 + +# Altair +ALTAIR_FORK_VERSION: 0x0100006f +ALTAIR_FORK_EPOCH: 90 # Mon Oct 10 2022 12:00:00 GMT+0000 +# Bellatrix +BELLATRIX_FORK_VERSION: 0x0200006f +BELLATRIX_FORK_EPOCH: 180 # Mon Oct 10 2022 14:00:00 GMT+0000 +# Capella +CAPELLA_FORK_VERSION: 0x0300006f +CAPELLA_FORK_EPOCH: 244224 # Wed May 24 2023 13:12:00 GMT+0000 + +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 diff --git a/common/eth2_network_config/built_in_network_configs/chiado/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/chiado/deploy_block.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/chiado/deploy_block.txt @@ -0,0 +1 @@ +0 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 ca1d1e88a..8e7a9dd07 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 @@ -38,7 +38,7 @@ BELLATRIX_FORK_VERSION: 0x02000064 BELLATRIX_FORK_EPOCH: 385536 # Capella CAPELLA_FORK_VERSION: 0x03000064 -CAPELLA_FORK_EPOCH: 18446744073709551615 +CAPELLA_FORK_EPOCH: 648704 # Sharding SHARDING_FORK_VERSION: 0x03000064 SHARDING_FORK_EPOCH: 18446744073709551615 @@ -86,3 +86,17 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 100 DEPOSIT_NETWORK_ID: 100 DEPOSIT_CONTRACT_ADDRESS: 0x0B98057eA310F4d31F2a452B414647007d1645d9 + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 4 +GOSSIP_MAX_SIZE: 10485760 +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +MAX_CHUNK_SIZE: 10485760 +TTFB_TIMEOUT: 5 +RESP_TIMEOUT: 10 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +ATTESTATION_SUBNET_PREFIX_BITS: 6 diff --git a/common/eth2_network_config/built_in_network_configs/holesky/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/holesky/boot_enr.yaml new file mode 100644 index 000000000..4dd280810 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/holesky/boot_enr.yaml @@ -0,0 +1,9 @@ +# EF +- enr:-Ku4QFo-9q73SspYI8cac_4kTX7yF800VXqJW4Lj3HkIkb5CMqFLxciNHePmMt4XdJzHvhrCC5ADI4D_GkAsxGJRLnQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAhnTT-AQFwAP__________gmlkgnY0gmlwhLKAiOmJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyk +- enr:-Ku4QPG7F72mbKx3gEQEx07wpYYusGDh-ni6SNkLvOS-hhN-BxIggN7tKlmalb0L5JPoAfqD-akTZ-gX06hFeBEz4WoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpAhnTT-AQFwAP__________gmlkgnY0gmlwhJK-DYCJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyk +- enr:-LK4QPxe-mDiSOtEB_Y82ozvxn9aQM07Ui8A-vQHNgYGMMthfsfOabaaTHhhJHFCBQQVRjBww_A5bM1rf8MlkJU_l68Eh2F0dG5ldHOIAADAAAAAAACEZXRoMpBpt9l0BAFwAAABAAAAAAAAgmlkgnY0gmlwhLKAiOmJc2VjcDI1NmsxoQJu6T9pclPObAzEVQ53DpVQqjadmVxdTLL-J3h9NFoCeIN0Y3CCIyiDdWRwgiMo +- enr:-Ly4QGbOw4xNel5EhmDsJJ-QhC9XycWtsetnWoZ0uRy381GHdHsNHJiCwDTOkb3S1Ade0SFQkWJX_pgb3g8Jfh93rvMBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpBpt9l0BAFwAAABAAAAAAAAgmlkgnY0gmlwhJK-DYCJc2VjcDI1NmsxoQOxKv9sv3zKF8GDewgFGGHKP5HCZZpPpTrwl9eXKAWGxIhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA +# Teku +- enr:-LS4QG0uV4qvcpJ-HFDJRGBmnlD3TJo7yc4jwK8iP7iKaTlfQ5kZvIDspLMJhk7j9KapuL9yyHaZmwTEZqr10k9XumyCEcmHYXR0bmV0c4gAAAAABgAAAIRldGgykGm32XQEAXAAAAEAAAAAAACCaWSCdjSCaXCErK4j-YlzZWNwMjU2azGhAgfWRBEJlb7gAhXIB5ePmjj2b8io0UpEenq1Kl9cxStJg3RjcIIjKIN1ZHCCIyg +# Sigma Prime +- enr:-Le4QLoE1wFHSlGcm48a9ZESb_MRLqPPu6G0vHqu4MaUcQNDHS69tsy-zkN0K6pglyzX8m24mkb-LtBcbjAYdP1uxm4BhGV0aDKQabfZdAQBcAAAAQAAAAAAAIJpZIJ2NIJpcIQ5gR6Wg2lwNpAgAUHQBwEQAAAAAAAAADR-iXNlY3AyNTZrMaEDPMSNdcL92uNIyCsS177Z6KTXlbZakQqxv3aQcWawNXeDdWRwgiMohHVkcDaCI4I 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 new file mode 100644 index 000000000..845d20830 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -0,0 +1,113 @@ +# Extends the mainnet preset +PRESET_BASE: 'mainnet' +CONFIG_NAME: holesky + +# Genesis +# --------------------------------------------------------------- +# `2**14` (= 16,384) +MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 +# Sep-28-2023 11:55:00 +UTC +MIN_GENESIS_TIME: 1695902100 +GENESIS_FORK_VERSION: 0x01017000 +# Genesis delay 5 mins +GENESIS_DELAY: 300 + + +# Forking +# --------------------------------------------------------------- +# Some forks are disabled for now: +# - These may be re-assigned to another fork-version later +# - Temporarily set to max uint64 value: 2**64 - 1 + +# Altair +ALTAIR_FORK_VERSION: 0x02017000 +ALTAIR_FORK_EPOCH: 0 +# Merge +BELLATRIX_FORK_VERSION: 0x03017000 +BELLATRIX_FORK_EPOCH: 0 +TERMINAL_TOTAL_DIFFICULTY: 0 +TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 +TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 + +# Capella +CAPELLA_FORK_VERSION: 0x04017000 +CAPELLA_FORK_EPOCH: 256 + +# Time parameters +# --------------------------------------------------------------- +# 12 seconds +SECONDS_PER_SLOT: 12 +# 14 (estimate from Eth1 mainnet) +SECONDS_PER_ETH1_BLOCK: 14 +# 2**8 (= 256) epochs ~27 hours +MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256 +# 2**8 (= 256) epochs ~27 hours +SHARD_COMMITTEE_PERIOD: 256 +# 2**11 (= 2,048) Eth1 blocks ~8 hours +ETH1_FOLLOW_DISTANCE: 2048 + + +# Validator cycle +# --------------------------------------------------------------- +# 2**2 (= 4) +INACTIVITY_SCORE_BIAS: 4 +# 2**4 (= 16) +INACTIVITY_SCORE_RECOVERY_RATE: 16 +# 28,000,000,000 Gwei to ensure quicker ejection +EJECTION_BALANCE: 28000000000 +# 2**2 (= 4) +MIN_PER_EPOCH_CHURN_LIMIT: 4 +# 2**16 (= 65,536) +CHURN_LIMIT_QUOTIENT: 65536 + +# Fork choice +# --------------------------------------------------------------- +# 40% +PROPOSER_SCORE_BOOST: 40 + +# Deposit contract +# --------------------------------------------------------------- +DEPOSIT_CHAIN_ID: 17000 +DEPOSIT_NETWORK_ID: 17000 +DEPOSIT_CONTRACT_ADDRESS: 0x4242424242424242424242424242424242424242 + +# Networking +# --------------------------------------------------------------- +# `10 * 2**20` (= 10485760, 10 MiB) +GOSSIP_MAX_SIZE: 10485760 +# `2**10` (= 1024) +MAX_REQUEST_BLOCKS: 1024 +# `2**8` (= 256) +EPOCHS_PER_SUBNET_SUBSCRIPTION: 256 +# `MIN_VALIDATOR_WITHDRAWABILITY_DELAY + CHURN_LIMIT_QUOTIENT // 2` (= 33024, ~5 months) +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +# `10 * 2**20` (=10485760, 10 MiB) +MAX_CHUNK_SIZE: 10485760 +# 5s +TTFB_TIMEOUT: 5 +# 10s +RESP_TIMEOUT: 10 +ATTESTATION_PROPAGATION_SLOT_RANGE: 32 +# 500ms +MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +# 2 subnets per node +SUBNETS_PER_NODE: 2 +# 2**8 (= 64) +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +# ceillog2(ATTESTATION_SUBNET_COUNT) + ATTESTATION_SUBNET_EXTRA_BITS +ATTESTATION_SUBNET_PREFIX_BITS: 6 + +# Deneb +# `2**7` (=128) +MAX_REQUEST_BLOCKS_DENEB: 128 +# MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK +MAX_REQUEST_BLOB_SIDECARS: 768 +# `2**12` (= 4096 epochs, ~18 days) +MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 +# `6` +BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 diff --git a/common/eth2_network_config/built_in_network_configs/holesky/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/holesky/deploy_block.txt new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/common/eth2_network_config/built_in_network_configs/holesky/deploy_block.txt @@ -0,0 +1 @@ +0 diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml index 196629cb8..7442f6576 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/boot_enr.yaml @@ -1,6 +1,8 @@ # Lighthouse Team (Sigma Prime) -- enr:-Jq4QItoFUuug_n_qbYbU0OY04-np2wT8rUCauOOXNi0H3BWbDj-zbfZb7otA7jZ6flbBpx1LNZK2TDebZ9dEKx84LYBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISsaa0ZiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMo -- enr:-Jq4QN_YBsUOqQsty1OGvYv48PMaiEt1AzGD1NkYQHaxZoTyVGqMYXg0K9c0LPNWC9pkXmggApp8nygYLsQwScwAgfgBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISLosQxiXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMo +- enr:-Le4QPUXJS2BTORXxyx2Ia-9ae4YqA_JWX3ssj4E_J-3z1A-HmFGrU8BpvpqhNabayXeOZ2Nq_sbeDgtzMJpLLnXFgAChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISsaa0Zg2lwNpAkAIkHAAAAAPA8kv_-awoTiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMohHVkcDaCI4I +- enr:-Le4QLHZDSvkLfqgEo8IWGG96h6mxwe_PsggC20CL3neLBjfXLGAQFOPSltZ7oP6ol54OvaNqO02Rnvb8YmDR274uq8ChGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLosQxg2lwNpAqAX4AAAAAAPA8kv_-ax65iXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMohHVkcDaCI4I +- enr:-Le4QH6LQrusDbAHPjU_HcKOuMeXfdEB5NJyXgHWFadfHgiySqeDyusQMvfphdYWOzuSZO9Uq2AMRJR5O4ip7OvVma8BhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY9ncg2lwNpAkAh8AgQIBAAAAAAAAAAmXiXNlY3AyNTZrMaECDYCZTZEksF-kmgPholqgVt8IXr-8L7Nu7YrZ7HUpgxmDdWRwgiMohHVkcDaCI4I +- enr:-Le4QIqLuWybHNONr933Lk0dcMmAB5WgvGKRyDihy1wHDIVlNuuztX62W51voT4I8qD34GcTEOTmag1bcdZ_8aaT4NUBhGV0aDKQtTA_KgEAAAAAIgEAAAAAAIJpZIJ2NIJpcISLY04ng2lwNpAkAh8AgAIBAAAAAAAAAA-fiXNlY3AyNTZrMaEDscnRV6n1m-D9ID5UsURk0jsoKNXt1TIrj8uKOGW6iluDdWRwgiMohHVkcDaCI4I # EF Team - enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg - enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg @@ -8,7 +10,9 @@ - enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg # Teku team (Consensys) - enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA -- enr:-KG4QDyytgmE4f7AnvW-ZaUOIi9i79qX4JwjRAiXBZCU65wOfBu-3Nb5I7b_Rmg3KCOcZM_C3y5pg7EBU5XGrcLTduQEhGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaEDKnz_-ps3UUOfHWVYaskI5kWYO_vtYMGYCQRAR3gHDouDdGNwgiMog3VkcIIjKA +- enr:-KG4QL-eqFoHy0cI31THvtZjpYUu_Jdw_MO7skQRJxY1g5HTN1A0epPCU6vi0gLGUgrzpU-ygeMSS8ewVxDpKfYmxMMGhGV0aDKQtTA_KgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaED8GJ2vzUqgL6-KD1xalo1CsmY4X1HaDnyl6Y_WayCo9GDdGNwgiMog3VkcIIjKA +- enr:-KG4QMOEswP62yzDjSwWS4YEjtTZ5PO6r65CPqYBkgTTkrpaedQ8uEUo1uMALtJIvb2w_WWEVmg5yt1UAuK1ftxUU7QDhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQEnfA2iXNlY3AyNTZrMaEDfol8oLr6XJ7FsdAYE7lpJhKMls4G_v6qQOGKJUWGb_uDdGNwgiMog3VkcIIjKA +- enr:-KG4QF4B5WrlFcRhUU6dZETwY5ZzAXnA0vGC__L1Kdw602nDZwXSTs5RFXFIFUnbQJmhNGVU6OIX7KVrCSTODsz1tK4DhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQExNYEiXNlY3AyNTZrMaECQmM9vp7KhaXhI-nqL_R0ovULLCFSFTa9CPPSdb1zPX6DdGNwgiMog3VkcIIjKA # Prysm team (Prysmatic Labs) - enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg - enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA 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 0bbf873a3..98984f3b7 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 @@ -86,3 +86,17 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 +GOSSIP_MAX_SIZE: 10485760 +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +MAX_CHUNK_SIZE: 10485760 +TTFB_TIMEOUT: 5 +RESP_TIMEOUT: 10 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +ATTESTATION_SUBNET_PREFIX_BITS: 6 diff --git a/common/eth2_network_config/built_in_network_configs/prater/config.yaml b/common/eth2_network_config/built_in_network_configs/prater/config.yaml index 69d65ca8f..a0dd85fec 100644 --- a/common/eth2_network_config/built_in_network_configs/prater/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/prater/config.yaml @@ -86,3 +86,17 @@ DEPOSIT_CHAIN_ID: 5 DEPOSIT_NETWORK_ID: 5 # Prater test deposit contract on Goerli Testnet DEPOSIT_CONTRACT_ADDRESS: 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 +GOSSIP_MAX_SIZE: 10485760 +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +MAX_CHUNK_SIZE: 10485760 +TTFB_TIMEOUT: 5 +RESP_TIMEOUT: 10 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +ATTESTATION_SUBNET_PREFIX_BITS: 6 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 294657289..e3674cf7d 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 @@ -74,3 +74,17 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 11155111 DEPOSIT_NETWORK_ID: 11155111 DEPOSIT_CONTRACT_ADDRESS: 0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 +GOSSIP_MAX_SIZE: 10485760 +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +MAX_CHUNK_SIZE: 10485760 +TTFB_TIMEOUT: 5 +RESP_TIMEOUT: 10 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +ATTESTATION_SUBNET_PREFIX_BITS: 6 diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index 7274bbf02..99093cf3b 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -11,12 +11,23 @@ //! To add a new built-in testnet, add it to the `define_hardcoded_nets` invocation in the `eth2_config` //! crate. +use bytes::Bytes; use discv5::enr::{CombinedKey, Enr}; use eth2_config::{instantiate_hardcoded_nets, HardcodedNet}; +use pretty_reqwest_error::PrettyReqwestError; +use reqwest::{Client, Error}; +use sensitive_url::SensitiveUrl; +use sha2::{Digest, Sha256}; +use slog::{info, warn, Logger}; use std::fs::{create_dir_all, File}; use std::io::{Read, Write}; use std::path::PathBuf; -use types::{BeaconState, ChainSpec, Config, EthSpec, EthSpecId}; +use std::str::FromStr; +use std::time::Duration; +use types::{BeaconState, ChainSpec, Config, EthSpec, EthSpecId, Hash256}; +use url::Url; + +pub use eth2_config::GenesisStateSource; pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt"; pub const BOOT_ENR_FILE: &str = "boot_enr.yaml"; @@ -32,6 +43,35 @@ instantiate_hardcoded_nets!(eth2_config); pub const DEFAULT_HARDCODED_NETWORK: &str = "mainnet"; +/// A simple slice-or-vec enum to avoid cloning the beacon state bytes in the +/// binary whilst also supporting loading them from a file at runtime. +#[derive(Clone, PartialEq, Debug)] +pub enum GenesisStateBytes { + Slice(&'static [u8]), + Vec(Vec<u8>), +} + +impl AsRef<[u8]> for GenesisStateBytes { + fn as_ref(&self) -> &[u8] { + match self { + GenesisStateBytes::Slice(slice) => slice, + GenesisStateBytes::Vec(vec) => vec.as_ref(), + } + } +} + +impl From<&'static [u8]> for GenesisStateBytes { + fn from(slice: &'static [u8]) -> Self { + GenesisStateBytes::Slice(slice) + } +} + +impl From<Vec<u8>> for GenesisStateBytes { + fn from(vec: Vec<u8>) -> Self { + GenesisStateBytes::Vec(vec) + } +} + /// Specifies an Eth2 network. /// /// See the crate-level documentation for more details. @@ -41,7 +81,8 @@ pub struct Eth2NetworkConfig { /// value to be the block number where the first deposit occurs. pub deposit_contract_deploy_block: u64, pub boot_enr: Option<Vec<Enr<CombinedKey>>>, - pub genesis_state_bytes: Option<Vec<u8>>, + pub genesis_state_source: GenesisStateSource, + pub genesis_state_bytes: Option<GenesisStateBytes>, pub config: Config, } @@ -65,8 +106,10 @@ impl Eth2NetworkConfig { serde_yaml::from_reader(net.boot_enr) .map_err(|e| format!("Unable to parse boot enr: {:?}", e))?, ), - genesis_state_bytes: Some(net.genesis_state_bytes.to_vec()) - .filter(|bytes| !bytes.is_empty()), + genesis_state_source: net.genesis_state_source, + genesis_state_bytes: Some(net.genesis_state_bytes) + .filter(|bytes| !bytes.is_empty()) + .map(Into::into), config: serde_yaml::from_reader(net.config) .map_err(|e| format!("Unable to parse yaml config: {:?}", e))?, }) @@ -81,8 +124,29 @@ impl Eth2NetworkConfig { } /// Returns `true` if this configuration contains a `BeaconState`. - pub fn beacon_state_is_known(&self) -> bool { - self.genesis_state_bytes.is_some() + pub fn genesis_state_is_known(&self) -> bool { + self.genesis_state_source != GenesisStateSource::Unknown + } + + /// The `genesis_validators_root` of the genesis state. + pub fn genesis_validators_root<E: EthSpec>(&self) -> Result<Option<Hash256>, String> { + if let GenesisStateSource::Url { + genesis_validators_root, + .. + } = self.genesis_state_source + { + Hash256::from_str(genesis_validators_root) + .map(Option::Some) + .map_err(|e| { + format!( + "Unable to parse genesis state genesis_validators_root: {:?}", + e + ) + }) + } else { + self.get_genesis_state_from_bytes::<E>() + .map(|state| Some(state.genesis_validators_root())) + } } /// Construct a consolidated `ChainSpec` from the YAML config. @@ -96,15 +160,68 @@ impl Eth2NetworkConfig { } /// Attempts to deserialize `self.beacon_state`, returning an error if it's missing or invalid. - pub fn beacon_state<E: EthSpec>(&self) -> Result<BeaconState<E>, String> { + /// + /// If the genesis state is configured to be downloaded from a URL, then the + /// `genesis_state_url` will override the built-in list of download URLs. + pub async fn genesis_state<E: EthSpec>( + &self, + genesis_state_url: Option<&str>, + timeout: Duration, + log: &Logger, + ) -> Result<Option<BeaconState<E>>, String> { let spec = self.chain_spec::<E>()?; - let genesis_state_bytes = self - .genesis_state_bytes - .as_ref() - .ok_or("Genesis state is unknown")?; + match &self.genesis_state_source { + GenesisStateSource::Unknown => Ok(None), + GenesisStateSource::IncludedBytes => { + let state = self.get_genesis_state_from_bytes()?; + Ok(Some(state)) + } + GenesisStateSource::Url { + urls: built_in_urls, + checksum, + genesis_validators_root, + } => { + let checksum = Hash256::from_str(checksum).map_err(|e| { + format!("Unable to parse genesis state bytes checksum: {:?}", e) + })?; + let bytes = if let Some(specified_url) = genesis_state_url { + download_genesis_state(&[specified_url], timeout, checksum, log).await + } else { + download_genesis_state(built_in_urls, timeout, checksum, log).await + }?; + let state = BeaconState::from_ssz_bytes(bytes.as_ref(), &spec).map_err(|e| { + format!("Downloaded genesis state SSZ bytes are invalid: {:?}", e) + })?; - BeaconState::from_ssz_bytes(genesis_state_bytes, &spec) - .map_err(|e| format!("Genesis state SSZ bytes are invalid: {:?}", e)) + let genesis_validators_root = + Hash256::from_str(genesis_validators_root).map_err(|e| { + format!( + "Unable to parse genesis state genesis_validators_root: {:?}", + e + ) + })?; + if state.genesis_validators_root() != genesis_validators_root { + return Err(format!( + "Downloaded genesis validators root {:?} does not match expected {:?}", + state.genesis_validators_root(), + genesis_validators_root + )); + } + + Ok(Some(state)) + } + } + } + + fn get_genesis_state_from_bytes<E: EthSpec>(&self) -> Result<BeaconState<E>, String> { + let spec = self.chain_spec::<E>()?; + self.genesis_state_bytes + .as_ref() + .map(|bytes| { + BeaconState::from_ssz_bytes(bytes.as_ref(), &spec) + .map_err(|e| format!("Built-in genesis state SSZ bytes are invalid: {:?}", e)) + }) + .ok_or("Genesis state bytes missing from Eth2NetworkConfig")? } /// Write the files to the directory. @@ -162,7 +279,7 @@ impl Eth2NetworkConfig { File::create(&file) .map_err(|e| format!("Unable to create {:?}: {:?}", file, e)) .and_then(|mut file| { - file.write_all(genesis_state_bytes) + file.write_all(genesis_state_bytes.as_ref()) .map_err(|e| format!("Unable to write {:?}: {:?}", file, e)) })?; } @@ -198,7 +315,7 @@ impl Eth2NetworkConfig { // The genesis state is a special case because it uses SSZ, not YAML. let genesis_file_path = base_dir.join(GENESIS_STATE_FILE); - let genesis_state_bytes = if genesis_file_path.exists() { + let (genesis_state_bytes, genesis_state_source) = if genesis_file_path.exists() { let mut bytes = vec![]; File::open(&genesis_file_path) .map_err(|e| format!("Unable to open {:?}: {:?}", genesis_file_path, e)) @@ -207,20 +324,112 @@ impl Eth2NetworkConfig { .map_err(|e| format!("Unable to read {:?}: {:?}", file, e)) })?; - Some(bytes).filter(|bytes| !bytes.is_empty()) + let state = Some(bytes).filter(|bytes| !bytes.is_empty()); + let genesis_state_source = if state.is_some() { + GenesisStateSource::IncludedBytes + } else { + GenesisStateSource::Unknown + }; + (state, genesis_state_source) } else { - None + (None, GenesisStateSource::Unknown) }; Ok(Self { deposit_contract_deploy_block, boot_enr, - genesis_state_bytes, + genesis_state_source, + genesis_state_bytes: genesis_state_bytes.map(Into::into), config, }) } } +/// Try to download a genesis state from each of the `urls` in the order they +/// are defined. Return `Ok` if any url returns a response that matches the +/// given `checksum`. +async fn download_genesis_state( + urls: &[&str], + timeout: Duration, + checksum: Hash256, + log: &Logger, +) -> Result<Vec<u8>, String> { + if urls.is_empty() { + return Err( + "The genesis state is not present in the binary and there are no known download URLs. \ + Please use --checkpoint-sync-url or --genesis-state-url." + .to_string(), + ); + } + + let mut errors = vec![]; + for url in urls { + // URLs are always expected to be the base URL of a server that supports + // the beacon-API. + let url = parse_state_download_url(url)?; + let redacted_url = SensitiveUrl::new(url.clone()) + .map(|url| url.to_string()) + .unwrap_or_else(|_| "<REDACTED>".to_string()); + + info!( + log, + "Downloading genesis state"; + "server" => &redacted_url, + "timeout" => ?timeout, + "info" => "this may take some time on testnets with large validator counts" + ); + + let client = Client::new(); + let response = get_state_bytes(timeout, url, client).await; + + match response { + Ok(bytes) => { + // Check the server response against our local checksum. + if Sha256::digest(bytes.as_ref())[..] == checksum[..] { + return Ok(bytes.into()); + } else { + warn!( + log, + "Genesis state download failed"; + "server" => &redacted_url, + "timeout" => ?timeout, + ); + errors.push(format!( + "Response from {} did not match local checksum", + redacted_url + )) + } + } + Err(e) => errors.push(PrettyReqwestError::from(e).to_string()), + } + } + Err(format!( + "Unable to download a genesis state from {} source(s): {}", + errors.len(), + errors.join(",") + )) +} + +async fn get_state_bytes(timeout: Duration, url: Url, client: Client) -> Result<Bytes, Error> { + client + .get(url) + .header("Accept", "application/octet-stream") + .timeout(timeout) + .send() + .await? + .error_for_status()? + .bytes() + .await +} + +/// Parses the `url` and joins the necessary state download path. +fn parse_state_download_url(url: &str) -> Result<Url, String> { + Url::parse(url) + .map_err(|e| format!("Invalid genesis state URL: {:?}", e))? + .join("eth/v2/debug/beacon/states/genesis") + .map_err(|e| format!("Failed to append genesis state path to URL: {:?}", e)) +} + #[cfg(test)] mod tests { use super::*; @@ -257,10 +466,13 @@ mod tests { assert_eq!(spec, config.chain_spec::<GnosisEthSpec>().unwrap()); } - #[test] - fn mainnet_genesis_state() { + #[tokio::test] + async fn mainnet_genesis_state() { let config = Eth2NetworkConfig::from_hardcoded_net(&MAINNET).unwrap(); - config.beacon_state::<E>().expect("beacon state can decode"); + config + .genesis_state::<E>(None, Duration::from_secs(1), &logging::test_logger()) + .await + .expect("beacon state can decode"); } #[test] @@ -274,10 +486,10 @@ mod tests { fn hard_coded_nets_work() { for net in HARDCODED_NETS { let config = Eth2NetworkConfig::from_hardcoded_net(net) - .unwrap_or_else(|_| panic!("{:?}", net.name)); + .unwrap_or_else(|e| panic!("{:?}: {:?}", net.name, e)); // Ensure we can parse the YAML config to a chain spec. - if net.name == types::GNOSIS { + if config.config.preset_base == types::GNOSIS { config.chain_spec::<GnosisEthSpec>().unwrap(); } else { config.chain_spec::<MainnetEthSpec>().unwrap(); @@ -285,10 +497,25 @@ mod tests { assert_eq!( config.genesis_state_bytes.is_some(), - net.genesis_is_known, + net.genesis_state_source == GenesisStateSource::IncludedBytes, "{:?}", net.name ); + + if let GenesisStateSource::Url { + urls, + checksum, + genesis_validators_root, + } = net.genesis_state_source + { + Hash256::from_str(checksum).expect("the checksum must be a valid 32-byte value"); + Hash256::from_str(genesis_validators_root) + .expect("the GVR must be a valid 32-byte value"); + for url in urls { + parse_state_download_url(url).expect("url must be valid"); + } + } + assert_eq!(config.config.config_name, Some(net.config_dir.to_string())); } } @@ -324,10 +551,20 @@ mod tests { let base_dir = temp_dir.path().join("my_testnet"); let deposit_contract_deploy_block = 42; + let genesis_state_source = if genesis_state.is_some() { + GenesisStateSource::IncludedBytes + } else { + GenesisStateSource::Unknown + }; + let testnet: Eth2NetworkConfig = Eth2NetworkConfig { deposit_contract_deploy_block, boot_enr, - genesis_state_bytes: genesis_state.as_ref().map(Encode::as_ssz_bytes), + genesis_state_source, + genesis_state_bytes: genesis_state + .as_ref() + .map(Encode::as_ssz_bytes) + .map(Into::into), config, }; diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index 8e6f0c0e5..f47175706 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -2,13 +2,13 @@ name = "eth2_wallet_manager" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eth2_wallet = { path = "../../crypto/eth2_wallet" } -lockfile = { path = "../lockfile" } +eth2_wallet = { workspace = true } +lockfile = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } diff --git a/common/filesystem/Cargo.toml b/common/filesystem/Cargo.toml index 66cbedbb8..fd026bd51 100644 --- a/common/filesystem/Cargo.toml +++ b/common/filesystem/Cargo.toml @@ -2,7 +2,7 @@ name = "filesystem" version = "0.1.0" authors = ["Mark Mackey <mark@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/common/lighthouse_metrics/Cargo.toml b/common/lighthouse_metrics/Cargo.toml index 06e33555d..6d9053440 100644 --- a/common/lighthouse_metrics/Cargo.toml +++ b/common/lighthouse_metrics/Cargo.toml @@ -2,10 +2,10 @@ name = "lighthouse_metrics" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lazy_static = "1.4.0" +lazy_static = { workspace = true } prometheus = "0.13.0" diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 96ac26647..3c4f9fe50 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -2,7 +2,7 @@ name = "lighthouse_version" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,4 +11,4 @@ git-version = "0.3.4" target_info = "0.1.0" [dev-dependencies] -regex = "1.5.5" +regex = { workspace = true } diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 3f2745bf9..c31917e04 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v4.2.0-", - fallback = "Lighthouse/v4.2.0" + prefix = "Lighthouse/v4.5.0-", + fallback = "Lighthouse/v4.5.0" ); /// Returns `VERSION`, but with platform information appended to the end. diff --git a/common/lockfile/Cargo.toml b/common/lockfile/Cargo.toml index b9616e871..d0cca13d4 100644 --- a/common/lockfile/Cargo.toml +++ b/common/lockfile/Cargo.toml @@ -2,10 +2,10 @@ name = "lockfile" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -fs2 = "0.4.3" +fs2 = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index b6179d9e7..9c5321591 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -2,21 +2,21 @@ name = "logging" version = "0.2.0" authors = ["blacktemplar <blacktemplar@a1.net>"] -edition = "2021" +edition = { workspace = true } [features] test_logger = [] # Print log output to stderr when running tests instead of dropping it [dependencies] -slog = "2.5.2" -slog-term = "2.6.0" -tokio = { version = "1.26.0", features = ["sync"] } -lighthouse_metrics = { path = "../lighthouse_metrics" } -lazy_static = "1.4.0" -sloggers = { version = "2.1.1", features = ["json"] } -slog-async = "2.7.0" +slog = { workspace = true } +slog-term = { workspace = true } +tokio = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +sloggers = { workspace = true } +slog-async = { workspace = true } take_mut = "0.2.2" -parking_lot = "0.12.1" -serde = "1.0.153" -serde_json = "1.0.94" -chrono = "0.4.23" +parking_lot = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } diff --git a/common/lru_cache/Cargo.toml b/common/lru_cache/Cargo.toml index 405b7be5d..73c623ed4 100644 --- a/common/lru_cache/Cargo.toml +++ b/common/lru_cache/Cargo.toml @@ -2,7 +2,7 @@ name = "lru_cache" version = "0.1.0" authors = ["Sigma Prime <contact@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -fnv = "1.0.7" +fnv = { workspace = true } diff --git a/common/lru_cache/src/time.rs b/common/lru_cache/src/time.rs index 7b8e9ba9a..966741ca4 100644 --- a/common/lru_cache/src/time.rs +++ b/common/lru_cache/src/time.rs @@ -1,4 +1,4 @@ -///! This implements a time-based LRU cache for fast checking of duplicates +//! This implements a time-based LRU cache for fast checking of duplicates use fnv::FnvHashSet; use std::collections::VecDeque; use std::time::{Duration, Instant}; diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index c88ec0bd5..4a5f39b66 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -2,13 +2,13 @@ name = "malloc_utils" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -lighthouse_metrics = { path = "../lighthouse_metrics" } -lazy_static = "1.4.0" +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } libc = "0.2.79" -parking_lot = "0.12.0" +parking_lot = { workspace = true } jemalloc-ctl = { version = "0.5.0", optional = true } # Jemalloc's background_threads feature requires Linux (pthreads). diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index bfb5e7204..e22f747bb 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -2,22 +2,22 @@ name = "monitoring_api" version = "0.1.0" authors = ["pawan <pawandhananjay@gmail.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.11.0", features = ["json","stream"] } -task_executor = { path = "../task_executor" } -tokio = "1.14.0" -eth2 = {path = "../eth2"} -serde_json = "1.0.58" -serde = "1.0.116" +reqwest = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +eth2 = { workspace = true } +serde_json = { workspace = true } serde_derive = "1.0.116" -lighthouse_version = { path = "../lighthouse_version"} -lighthouse_metrics = { path = "../lighthouse_metrics" } -slog = "2.5.2" -store = { path = "../../beacon_node/store" } -lazy_static = "1.4.0" -regex = "1.5.5" -sensitive_url = { path = "../sensitive_url" } +serde = { workspace = true } +lighthouse_version = { workspace = true } +lighthouse_metrics = { workspace = true } +slog = { workspace = true } +store = { workspace = true } +lazy_static = { workspace = true } +regex = { workspace = true } +sensitive_url = { workspace = true } diff --git a/common/oneshot_broadcast/Cargo.toml b/common/oneshot_broadcast/Cargo.toml index baefe1066..12c9b40bc 100644 --- a/common/oneshot_broadcast/Cargo.toml +++ b/common/oneshot_broadcast/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "oneshot_broadcast" version = "0.1.0" -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -parking_lot = "0.12.0" +parking_lot = { workspace = true } diff --git a/common/pretty_reqwest_error/Cargo.toml b/common/pretty_reqwest_error/Cargo.toml new file mode 100644 index 000000000..dc79832cd --- /dev/null +++ b/common/pretty_reqwest_error/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pretty_reqwest_error" +version = "0.1.0" +edition = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { workspace = true } +sensitive_url = { workspace = true } diff --git a/common/pretty_reqwest_error/src/lib.rs b/common/pretty_reqwest_error/src/lib.rs new file mode 100644 index 000000000..0aaee5965 --- /dev/null +++ b/common/pretty_reqwest_error/src/lib.rs @@ -0,0 +1,68 @@ +use sensitive_url::SensitiveUrl; +use std::error::Error as StdError; +use std::fmt; + +pub struct PrettyReqwestError(reqwest::Error); + +impl PrettyReqwestError { + pub fn inner(&self) -> &reqwest::Error { + &self.0 + } +} + +impl fmt::Debug for PrettyReqwestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(url) = self.0.url() { + if let Ok(url) = SensitiveUrl::new(url.clone()) { + write!(f, "url: {}", url)?; + } else { + write!(f, "url: unable_to_parse")?; + }; + } + + let kind = if self.0.is_builder() { + "builder" + } else if self.0.is_redirect() { + "redirect" + } else if self.0.is_status() { + "status" + } else if self.0.is_timeout() { + "timeout" + } else if self.0.is_request() { + "request" + } else if self.0.is_connect() { + "connect" + } else if self.0.is_body() { + "body" + } else if self.0.is_decode() { + "decode" + } else { + "unknown" + }; + write!(f, ", kind: {}", kind)?; + + if let Some(status) = self.0.status() { + write!(f, ", status_code: {}", status)?; + } + + if let Some(ref source) = self.0.source() { + write!(f, ", detail: {}", source)?; + } else { + write!(f, ", source: unknown")?; + } + + Ok(()) + } +} + +impl fmt::Display for PrettyReqwestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<reqwest::Error> for PrettyReqwestError { + fn from(inner: reqwest::Error) -> Self { + Self(inner) + } +} diff --git a/common/sensitive_url/Cargo.toml b/common/sensitive_url/Cargo.toml index 6de591efc..d218c8d93 100644 --- a/common/sensitive_url/Cargo.toml +++ b/common/sensitive_url/Cargo.toml @@ -2,10 +2,10 @@ name = "sensitive_url" version = "0.1.0" authors = ["Mac L <mjladson@pm.me>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -url = "2.2.1" -serde = "1.0.116" +url = { workspace = true } +serde = { workspace = true } diff --git a/common/sensitive_url/src/lib.rs b/common/sensitive_url/src/lib.rs index b6705eb60..b6068a2dc 100644 --- a/common/sensitive_url/src/lib.rs +++ b/common/sensitive_url/src/lib.rs @@ -75,7 +75,7 @@ impl SensitiveUrl { SensitiveUrl::new(surl) } - fn new(full: Url) -> Result<Self, SensitiveError> { + pub fn new(full: Url) -> Result<Self, SensitiveError> { let mut redacted = full.clone(); redacted .path_segments_mut() diff --git a/common/slot_clock/Cargo.toml b/common/slot_clock/Cargo.toml index d9fdd7312..31e53779a 100644 --- a/common/slot_clock/Cargo.toml +++ b/common/slot_clock/Cargo.toml @@ -2,10 +2,10 @@ name = "slot_clock" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -types = { path = "../../consensus/types" } -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../lighthouse_metrics" } -parking_lot = "0.12.0" +types = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +parking_lot = { workspace = true } diff --git a/common/system_health/Cargo.toml b/common/system_health/Cargo.toml index 0956710b8..c02380c9d 100644 --- a/common/system_health/Cargo.toml +++ b/common/system_health/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "system_health" version = "0.1.0" -edition = "2021" +edition = { workspace = true } [dependencies] -lighthouse_network = { path = "../../beacon_node/lighthouse_network" } -types = { path = "../../consensus/types" } -sysinfo = "0.26.5" -serde = "1.0.116" +lighthouse_network = { workspace = true } +types = { workspace = true } +sysinfo = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -serde_json = "1.0.58" -parking_lot = "0.12.0" +serde_json = { workspace = true } +parking_lot = { workspace = true } diff --git a/common/target_check/Cargo.toml b/common/target_check/Cargo.toml index 2cf2cacc6..206a636be 100644 --- a/common/target_check/Cargo.toml +++ b/common/target_check/Cargo.toml @@ -2,7 +2,7 @@ name = "target_check" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] static_assertions = "1.1.0" diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index 08bb56587..38f4eca36 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -2,13 +2,13 @@ name = "task_executor" version = "0.1.0" authors = ["Sigma Prime <contact@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -tokio = { version = "1.14.0", features = ["rt-multi-thread", "macros"] } -slog = "2.5.2" -futures = "0.3.7" -exit-future = "0.2.0" -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../lighthouse_metrics" } -sloggers = { version = "2.1.1", features = ["json"] } +tokio = { workspace = true } +slog = { workspace = true } +futures = { workspace = true } +exit-future = { workspace = true } +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +sloggers = { workspace = true } diff --git a/common/test_random_derive/Cargo.toml b/common/test_random_derive/Cargo.toml index 8794eeea2..79308797a 100644 --- a/common/test_random_derive/Cargo.toml +++ b/common/test_random_derive/Cargo.toml @@ -2,12 +2,12 @@ name = "test_random_derive" version = "0.2.0" authors = ["thojest <thojest@gmail.com>"] -edition = "2021" +edition = { workspace = true } description = "Procedural derive macros for implementation of TestRandom trait" [lib] proc-macro = true [dependencies] -syn = "1.0.42" -quote = "1.0.7" +syn = { workspace = true } +quote = { workspace = true } diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml index 2dd041ff0..3d70cad27 100644 --- a/common/unused_port/Cargo.toml +++ b/common/unused_port/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "unused_port" version = "0.1.0" -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lru_cache = { path = "../lru_cache" } -lazy_static = "1.4.0" -parking_lot = "0.12.0" +lru_cache = { workspace = true } +lazy_static = { workspace = true } +parking_lot = { workspace = true } diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 39a14e283..ae8742fe0 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -2,7 +2,7 @@ name = "validator_dir" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [features] insecure_keys = [] @@ -10,16 +10,17 @@ insecure_keys = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bls = { path = "../../crypto/bls" } -eth2_keystore = { path = "../../crypto/eth2_keystore" } -filesystem = { path = "../filesystem" } -types = { path = "../../consensus/types" } -rand = "0.8.5" -deposit_contract = { path = "../deposit_contract" } -tree_hash = "0.5.0" -hex = "0.4.2" -derivative = "2.1.1" -lockfile = { path = "../lockfile" } +bls = { workspace = true } +eth2_keystore = { workspace = true } +filesystem = { workspace = true } +types = { workspace = true } +rand = { workspace = true } +deposit_contract = { workspace = true } +tree_hash = { workspace = true } +hex = { workspace = true } +derivative = { workspace = true } +lockfile = { workspace = true } +directory = { workspace = true } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 2b3f670c7..bccf9086a 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -1,6 +1,7 @@ use crate::{Error as DirError, ValidatorDir}; use bls::get_withdrawal_credentials; use deposit_contract::{encode_eth1_tx_data, Error as DepositError}; +use directory::ensure_dir_exists; use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText}; use filesystem::create_with_600_perms; use rand::{distributions::Alphanumeric, Rng}; @@ -41,6 +42,7 @@ pub enum Error { #[cfg(feature = "insecure_keys")] InsecureKeysError(String), MissingPasswordDir, + UnableToCreatePasswordDir(String), } impl From<KeystoreError> for Error { @@ -78,6 +80,13 @@ impl<'a> Builder<'a> { self } + /// Optionally supply a directory in which to store the passwords for the validator keystores. + /// If `None` is provided, do not store the password. + pub fn password_dir_opt(mut self, password_dir_opt: Option<PathBuf>) -> Self { + self.password_dir = password_dir_opt; + self + } + /// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`. /// /// The builder will not necessarily check that `password` can unlock `keystore`. @@ -153,6 +162,10 @@ impl<'a> Builder<'a> { create_dir_all(&dir).map_err(Error::UnableToCreateDir)?; } + if let Some(password_dir) = &self.password_dir { + ensure_dir_exists(password_dir).map_err(Error::UnableToCreatePasswordDir)?; + } + // The withdrawal keystore must be initialized in order to store it or create an eth1 // deposit. if (self.store_withdrawal_keystore || self.deposit_info.is_some()) @@ -234,7 +247,7 @@ impl<'a> Builder<'a> { if self.store_withdrawal_keystore { // Write the withdrawal password to file. write_password_to_file( - password_dir.join(withdrawal_keypair.pk.as_hex_string()), + keystore_password_path(password_dir, &withdrawal_keystore), withdrawal_password.as_bytes(), )?; @@ -250,7 +263,7 @@ impl<'a> Builder<'a> { if let Some(password_dir) = self.password_dir.as_ref() { // Write the voting password to file. write_password_to_file( - password_dir.join(format!("0x{}", voting_keystore.pubkey())), + keystore_password_path(password_dir, &voting_keystore), voting_password.as_bytes(), )?; } @@ -262,6 +275,12 @@ impl<'a> Builder<'a> { } } +pub fn keystore_password_path<P: AsRef<Path>>(password_dir: P, keystore: &Keystore) -> PathBuf { + password_dir + .as_ref() + .join(format!("0x{}", keystore.pubkey())) +} + /// Writes a JSON keystore to file. fn write_keystore_to_file(path: PathBuf, keystore: &Keystore) -> Result<(), Error> { if path.exists() { diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index a39d32283..4aa0d590a 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -15,6 +15,6 @@ pub use crate::validator_dir::{ ETH1_DEPOSIT_TX_HASH_FILE, }; pub use builder::{ - Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, - WITHDRAWAL_KEYSTORE_FILE, + keystore_password_path, Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, + VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index cb1ddde24..24b317dcf 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -1,5 +1,5 @@ use crate::builder::{ - ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, + keystore_password_path, ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; use deposit_contract::decode_eth1_tx_data; @@ -219,9 +219,7 @@ pub fn unlock_keypair<P: AsRef<Path>>( ) .map_err(Error::UnableToReadKeystore)?; - let password_path = password_dir - .as_ref() - .join(format!("0x{}", keystore.pubkey())); + let password_path = keystore_password_path(password_dir, &keystore); let password: PlainText = read(&password_path) .map_err(|_| Error::UnableToReadPassword(password_path))? .into(); diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index e66aeddfb..85c1901ba 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -2,20 +2,20 @@ name = "warp_utils" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = "0.3.2" -eth2 = { path = "../eth2" } -types = { path = "../../consensus/types" } -beacon_chain = { path = "../../beacon_node/beacon_chain" } -state_processing = { path = "../../consensus/state_processing" } -safe_arith = { path = "../../consensus/safe_arith" } -serde = { version = "1.0.116", features = ["derive"] } -tokio = { version = "1.14.0", features = ["sync"] } +warp = { workspace = true } +eth2 = { workspace = true } +types = { workspace = true } +beacon_chain = { workspace = true } +state_processing = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } headers = "0.3.2" -lighthouse_metrics = { path = "../lighthouse_metrics" } -lazy_static = "1.4.0" +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } serde_array_query = "0.1.0" diff --git a/common/warp_utils/src/cors.rs b/common/warp_utils/src/cors.rs index 314ea9c8f..55043dfd7 100644 --- a/common/warp_utils/src/cors.rs +++ b/common/warp_utils/src/cors.rs @@ -10,10 +10,14 @@ pub fn set_builder_origins( default_origin: (IpAddr, u16), ) -> Result<Builder, String> { if let Some(allow_origin) = allow_origin { - let origins = allow_origin - .split(',') - .map(|s| verify_cors_origin_str(s).map(|_| s)) - .collect::<Result<Vec<_>, _>>()?; + let mut origins = vec![]; + for origin in allow_origin.split(',') { + verify_cors_origin_str(origin)?; + if origin == "*" { + return Ok(builder.allow_any_origin()); + } + origins.push(origin) + } Ok(builder.allow_origins(origins)) } else { let origin = match default_origin.0 { diff --git a/common/warp_utils/src/metrics.rs b/common/warp_utils/src/metrics.rs index 1b9d89db9..d93b74ca9 100644 --- a/common/warp_utils/src/metrics.rs +++ b/common/warp_utils/src/metrics.rs @@ -87,7 +87,7 @@ pub fn scrape_process_health_metrics() { // This will silently fail if we are unable to observe the health. This is desired behaviour // since we don't support `Health` for all platforms. if let Ok(health) = ProcessHealth::observe() { - set_gauge(&PROCESS_NUM_THREADS, health.pid_num_threads as i64); + set_gauge(&PROCESS_NUM_THREADS, health.pid_num_threads); set_gauge(&PROCESS_RES_MEM, health.pid_mem_resident_set_size as i64); set_gauge(&PROCESS_VIRT_MEM, health.pid_mem_virtual_memory_size as i64); set_gauge(&PROCESS_SECONDS, health.pid_process_seconds_total as i64); diff --git a/consensus/cached_tree_hash/Cargo.toml b/consensus/cached_tree_hash/Cargo.toml index c2856003b..05edc3485 100644 --- a/consensus/cached_tree_hash/Cargo.toml +++ b/consensus/cached_tree_hash/Cargo.toml @@ -2,20 +2,20 @@ name = "cached_tree_hash" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -ethereum-types = "0.14.1" -ssz_types = "0.5.0" -ethereum_hashing = "1.0.0-beta.2" -ethereum_ssz_derive = "0.5.0" -ethereum_ssz = "0.5.0" -tree_hash = "0.5.0" -smallvec = "1.6.1" +ethereum-types = { workspace = true } +ssz_types = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum_ssz_derive = { workspace = true } +ethereum_ssz = { workspace = true } +tree_hash = { workspace = true } +smallvec = { workspace = true } [dev-dependencies] -quickcheck = "0.9.2" -quickcheck_macros = "0.9.1" +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index 3864d52d4..7a06d7352 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -2,19 +2,19 @@ name = "fork_choice" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -types = { path = "../types" } -state_processing = { path = "../state_processing" } -proto_array = { path = "../proto_array" } -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } +types = { workspace = true } +state_processing = { workspace = true } +proto_array = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +slog = { workspace = true } [dev-dependencies] -beacon_chain = { path = "../../beacon_node/beacon_chain" } -store = { path = "../../beacon_node/store" } -tokio = { version = "1.14.0", features = ["rt-multi-thread"] } +beacon_chain = { workspace = true } +store = { workspace = true } +tokio = { workspace = true } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index e6c46e83e..ea3a58127 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1,10 +1,15 @@ use crate::{ForkChoiceStore, InvalidationOperation}; +use per_epoch_processing::altair::participation_cache::Error as ParticipationCacheError; use proto_array::{ Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, }; -use slog::{crit, debug, warn, Logger}; +use slog::{crit, debug, error, warn, Logger}; use ssz_derive::{Decode, Encode}; +use state_processing::per_epoch_processing::altair::ParticipationCache; +use state_processing::per_epoch_processing::{ + weigh_justification_and_finalization, JustificationAndFinalizationState, +}; use state_processing::{ per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing, }; @@ -18,6 +23,7 @@ use types::{ EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestation, RelativeEpoch, SignedBeaconBlock, Slot, }; +use types::{ProgressiveBalancesCache, ProgressiveBalancesMode}; #[derive(Debug)] pub enum Error<T> { @@ -72,7 +78,9 @@ pub enum Error<T> { }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), ParticipationCacheBuild(BeaconStateError), + ParticipationCacheError(ParticipationCacheError), ValidatorStatuses(BeaconStateError), + ProgressiveBalancesCacheCheckFailed(String), } impl<T> From<InvalidAttestation> for Error<T> { @@ -93,6 +101,18 @@ impl<T> From<state_processing::EpochProcessingError> for Error<T> { } } +impl<T> From<BeaconStateError> for Error<T> { + fn from(e: BeaconStateError) -> Self { + Error::BeaconStateError(e) + } +} + +impl<T> From<ParticipationCacheError> for Error<T> { + fn from(e: ParticipationCacheError) -> Self { + Error::ParticipationCacheError(e) + } +} + #[derive(Debug, Clone, Copy)] /// Controls how fork choice should behave when restoring from a persisted fork choice. pub enum ResetPayloadStatuses { @@ -174,21 +194,6 @@ impl<T> From<proto_array::Error> for Error<T> { } } -/// Indicates whether the unrealized justification of a block should be calculated and tracked. -/// If a block has been finalized, this can be set to false. This is useful when syncing finalized -/// portions of the chain. Otherwise this should always be set to true. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CountUnrealized { - True, - False, -} - -impl CountUnrealized { - pub fn is_true(&self) -> bool { - matches!(self, CountUnrealized::True) - } -} - /// Indicates if a block has been verified by an execution payload. /// /// There is no variant for "invalid", since such a block should never be added to fork choice. @@ -284,9 +289,10 @@ pub enum AttestationFromBlock { False, } -/// Parameters which are cached between calls to `Self::get_head`. +/// Parameters which are cached between calls to `ForkChoice::get_head`. #[derive(Clone, Copy)] pub struct ForkchoiceUpdateParameters { + /// The most recent result of running `ForkChoice::get_head`. pub head_root: Hash256, pub head_hash: Option<ExecutionBlockHash>, pub justified_hash: Option<ExecutionBlockHash>, @@ -319,8 +325,6 @@ pub struct ForkChoice<T, E> { queued_attestations: Vec<QueuedAttestation>, /// Stores a cache of the values required to be sent to the execution layer. forkchoice_update_parameters: ForkchoiceUpdateParameters, - /// The most recent result of running `Self::get_head`. - head_block_root: Hash256, _phantom: PhantomData<E>, } @@ -351,7 +355,7 @@ where spec: &ChainSpec, ) -> Result<Self, Error<T::Error>> { // Sanity check: the anchor must lie on an epoch boundary. - if anchor_block.slot() % E::slots_per_epoch() != 0 { + if anchor_state.slot() % E::slots_per_epoch() != 0 { return Err(Error::InvalidAnchor { block_slot: anchor_block.slot(), state_slot: anchor_state.slot(), @@ -387,6 +391,7 @@ where let current_slot = current_slot.unwrap_or_else(|| fc_store.get_current_slot()); let proto_array = ProtoArrayForkChoice::new::<E>( + current_slot, finalized_block_slot, finalized_block_state_root, *fc_store.justified_checkpoint(), @@ -405,14 +410,13 @@ where head_hash: None, justified_hash: None, finalized_hash: None, + // This will be updated during the next call to `Self::get_head`. head_root: Hash256::zero(), }, - // This will be updated during the next call to `Self::get_head`. - head_block_root: Hash256::zero(), _phantom: PhantomData, }; - // Ensure that `fork_choice.head_block_root` is updated. + // Ensure that `fork_choice.forkchoice_update_parameters.head_root` is updated. fork_choice.get_head(current_slot, spec)?; Ok(fork_choice) @@ -461,13 +465,10 @@ where // for lower slots to account for skip slots. .find(|(_, slot)| *slot <= ancestor_slot) .map(|(root, _)| root)), - Ordering::Less => Ok(Some(block_root)), - Ordering::Equal => // Root is older than queried slot, thus a skip slot. Return most recent root prior // to slot. - { - Ok(Some(block_root)) - } + Ordering::Less => Ok(Some(block_root)), + Ordering::Equal => Ok(Some(block_root)), } } @@ -500,8 +501,6 @@ where spec, )?; - self.head_block_root = head_root; - // Cache some values for the next forkchoiceUpdate call to the execution layer. let head_hash = self .get_block(&head_root) @@ -605,7 +604,7 @@ where /// have *differing* finalized and justified information. pub fn cached_fork_choice_view(&self) -> ForkChoiceView { ForkChoiceView { - head_block_root: self.head_block_root, + head_block_root: self.forkchoice_update_parameters.head_root, justified_checkpoint: self.justified_checkpoint(), finalized_checkpoint: self.finalized_checkpoint(), } @@ -658,9 +657,17 @@ where block_delay: Duration, state: &BeaconState<E>, payload_verification_status: PayloadVerificationStatus, + progressive_balances_mode: ProgressiveBalancesMode, spec: &ChainSpec, - count_unrealized: CountUnrealized, + log: &Logger, ) -> Result<(), Error<T::Error>> { + // If this block has already been processed we do not need to reprocess it. + // We check this immediately in case re-processing the block mutates some property of the + // global fork choice store, e.g. the justified checkpoints or the proposer boost root. + if self.proto_array.contains_block(&block_root) { + return Ok(()); + } + // Provide the slot (as per the system clock) to the `fc_store` and then return its view of // the current slot. The `fc_store` will ensure that the `current_slot` is never // decreasing, a property which we must maintain. @@ -726,97 +733,126 @@ where )?; // Update unrealized justified/finalized checkpoints. - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if count_unrealized - .is_true() + let block_epoch = block.slot().epoch(E::slots_per_epoch()); + + // If the parent checkpoints are already at the same epoch as the block being imported, + // it's impossible for the unrealized checkpoints to differ from the parent's. This + // holds true because: + // + // 1. A child block cannot have lower FFG checkpoints than its parent. + // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. + // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. + // + // This is an optimization. It should reduce the amount of times we run + // `process_justification_and_finalization` by approximately 1/3rd when the chain is + // performing optimally. + let parent_checkpoints = parent_block + .unrealized_justified_checkpoint + .zip(parent_block.unrealized_finalized_checkpoint) + .filter(|(parent_justified, parent_finalized)| { + parent_justified.epoch == block_epoch && parent_finalized.epoch + 1 == block_epoch + }); + + let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = if let Some(( + parent_justified, + parent_finalized, + )) = + parent_checkpoints { - let block_epoch = block.slot().epoch(E::slots_per_epoch()); + (parent_justified, parent_finalized) + } else { + let justification_and_finalization_state = match block { + BeaconBlockRef::Capella(_) + | BeaconBlockRef::Merge(_) + | BeaconBlockRef::Altair(_) => match progressive_balances_mode { + ProgressiveBalancesMode::Disabled => { + let participation_cache = ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild)?; + per_epoch_processing::altair::process_justification_and_finalization( + state, + &participation_cache, + )? + } + ProgressiveBalancesMode::Fast + | ProgressiveBalancesMode::Checked + | ProgressiveBalancesMode::Strict => { + let maybe_participation_cache = progressive_balances_mode + .perform_comparative_checks() + .then(|| { + ParticipationCache::new(state, spec) + .map_err(Error::ParticipationCacheBuild) + }) + .transpose()?; - // If the parent checkpoints are already at the same epoch as the block being imported, - // it's impossible for the unrealized checkpoints to differ from the parent's. This - // holds true because: - // - // 1. A child block cannot have lower FFG checkpoints than its parent. - // 2. A block in epoch `N` cannot contain attestations which would justify an epoch higher than `N`. - // 3. A block in epoch `N` cannot contain attestations which would finalize an epoch higher than `N - 1`. - // - // This is an optimization. It should reduce the amount of times we run - // `process_justification_and_finalization` by approximately 1/3rd when the chain is - // performing optimally. - let parent_checkpoints = parent_block - .unrealized_justified_checkpoint - .zip(parent_block.unrealized_finalized_checkpoint) - .filter(|(parent_justified, parent_finalized)| { - parent_justified.epoch == block_epoch - && parent_finalized.epoch + 1 >= block_epoch - }); - - let (unrealized_justified_checkpoint, unrealized_finalized_checkpoint) = - if let Some((parent_justified, parent_finalized)) = parent_checkpoints { - (parent_justified, parent_finalized) - } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Capella(_) - | BeaconBlockRef::Merge(_) - | BeaconBlockRef::Altair(_) => { - let participation_cache = - per_epoch_processing::altair::ParticipationCache::new(state, spec) - .map_err(Error::ParticipationCacheBuild)?; - per_epoch_processing::altair::process_justification_and_finalization( + process_justification_and_finalization_from_progressive_cache::<E, T>( state, - &participation_cache, - )? - } - BeaconBlockRef::Base(_) => { - let mut validator_statuses = - per_epoch_processing::base::ValidatorStatuses::new(state, spec) - .map_err(Error::ValidatorStatuses)?; - validator_statuses - .process_attestations(state) - .map_err(Error::ValidatorStatuses)?; - per_epoch_processing::base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )? - } - }; - - ( - justification_and_finalization_state.current_justified_checkpoint(), - justification_and_finalization_state.finalized_checkpoint(), - ) - }; - - // Update best known unrealized justified & finalized checkpoints - if unrealized_justified_checkpoint.epoch - > self.fc_store.unrealized_justified_checkpoint().epoch - { - self.fc_store - .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); - } - if unrealized_finalized_checkpoint.epoch - > self.fc_store.unrealized_finalized_checkpoint().epoch - { - self.fc_store - .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); - } - - // If block is from past epochs, try to update store's justified & finalized checkpoints right away - if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { - self.pull_up_store_checkpoints( - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, - )?; - } + maybe_participation_cache.as_ref(), + ) + .or_else(|e| { + if progressive_balances_mode != ProgressiveBalancesMode::Strict { + error!( + log, + "Processing with progressive balances cache failed"; + "info" => "falling back to the non-optimized processing method", + "error" => ?e, + ); + let participation_cache = maybe_participation_cache + .map(Ok) + .unwrap_or_else(|| ParticipationCache::new(state, spec)) + .map_err(Error::ParticipationCacheBuild)?; + per_epoch_processing::altair::process_justification_and_finalization( + state, + &participation_cache, + ).map_err(Error::from) + } else { + Err(e) + } + })? + } + }, + BeaconBlockRef::Base(_) => { + let mut validator_statuses = + per_epoch_processing::base::ValidatorStatuses::new(state, spec) + .map_err(Error::ValidatorStatuses)?; + validator_statuses + .process_attestations(state) + .map_err(Error::ValidatorStatuses)?; + per_epoch_processing::base::process_justification_and_finalization( + state, + &validator_statuses.total_balances, + spec, + )? + } + }; ( - Some(unrealized_justified_checkpoint), - Some(unrealized_finalized_checkpoint), + justification_and_finalization_state.current_justified_checkpoint(), + justification_and_finalization_state.finalized_checkpoint(), ) - } else { - (None, None) }; + // Update best known unrealized justified & finalized checkpoints + if unrealized_justified_checkpoint.epoch + > self.fc_store.unrealized_justified_checkpoint().epoch + { + self.fc_store + .set_unrealized_justified_checkpoint(unrealized_justified_checkpoint); + } + if unrealized_finalized_checkpoint.epoch + > self.fc_store.unrealized_finalized_checkpoint().epoch + { + self.fc_store + .set_unrealized_finalized_checkpoint(unrealized_finalized_checkpoint); + } + + // If block is from past epochs, try to update store's justified & finalized checkpoints right away + if block.slot().epoch(E::slots_per_epoch()) < current_slot.epoch(E::slots_per_epoch()) { + self.pull_up_store_checkpoints( + unrealized_justified_checkpoint, + unrealized_finalized_checkpoint, + )?; + } + let target_slot = block .slot() .epoch(E::slots_per_epoch()) @@ -886,8 +922,8 @@ where justified_checkpoint: state.current_justified_checkpoint(), finalized_checkpoint: state.finalized_checkpoint(), execution_status, - unrealized_justified_checkpoint, - unrealized_finalized_checkpoint, + unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint), + unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint), }, current_slot, )?; @@ -1479,10 +1515,9 @@ where head_hash: None, justified_hash: None, finalized_hash: None, + // Will be updated in the following call to `Self::get_head`. head_root: Hash256::zero(), }, - // Will be updated in the following call to `Self::get_head`. - head_block_root: Hash256::zero(), _phantom: PhantomData, }; @@ -1520,6 +1555,92 @@ where } } +/// Process justification and finalization using progressive cache. Also performs a comparative +/// check against the `ParticipationCache` if it is supplied. +/// +/// Returns an error if the cache is not initialized or if there is a mismatch on the comparative check. +fn process_justification_and_finalization_from_progressive_cache<E, T>( + state: &BeaconState<E>, + maybe_participation_cache: Option<&ParticipationCache>, +) -> Result<JustificationAndFinalizationState<E>, Error<T::Error>> +where + E: EthSpec, + T: ForkChoiceStore<E>, +{ + let justification_and_finalization_state = JustificationAndFinalizationState::new(state); + if state.current_epoch() <= E::genesis_epoch() + 1 { + return Ok(justification_and_finalization_state); + } + + // Load cached balances + let progressive_balances_cache: &ProgressiveBalancesCache = state.progressive_balances_cache(); + let previous_target_balance = + progressive_balances_cache.previous_epoch_target_attesting_balance()?; + let current_target_balance = + progressive_balances_cache.current_epoch_target_attesting_balance()?; + let total_active_balance = state.get_total_active_balance()?; + + if let Some(participation_cache) = maybe_participation_cache { + check_progressive_balances::<E, T>( + state, + participation_cache, + previous_target_balance, + current_target_balance, + total_active_balance, + )?; + } + + weigh_justification_and_finalization( + justification_and_finalization_state, + total_active_balance, + previous_target_balance, + current_target_balance, + ) + .map_err(Error::from) +} + +/// Perform comparative checks against `ParticipationCache`, will return error if there's a mismatch. +fn check_progressive_balances<E, T>( + state: &BeaconState<E>, + participation_cache: &ParticipationCache, + cached_previous_target_balance: u64, + cached_current_target_balance: u64, + cached_total_active_balance: u64, +) -> Result<(), Error<T::Error>> +where + E: EthSpec, + T: ForkChoiceStore<E>, +{ + let slot = state.slot(); + let epoch = state.current_epoch(); + + // Check previous epoch target balances + let previous_target_balance = participation_cache.previous_epoch_target_attesting_balance()?; + if previous_target_balance != cached_previous_target_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Previous epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, previous_target_balance, cached_previous_target_balance) + )); + } + + // Check current epoch target balances + let current_target_balance = participation_cache.current_epoch_target_attesting_balance()?; + if current_target_balance != cached_current_target_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Current epoch target attesting balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, current_target_balance, cached_current_target_balance) + )); + } + + // Check current epoch total balances + let total_active_balance = participation_cache.current_epoch_total_active_balance(); + if total_active_balance != cached_total_active_balance { + return Err(Error::ProgressiveBalancesCacheCheckFailed( + format!("Current epoch total active balance mismatch, slot: {}, epoch: {}, actual: {}, cached: {}", slot, epoch, total_active_balance, cached_total_active_balance) + )); + } + + Ok(()) +} + /// 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. diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 397a2ff89..e7ca84efb 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -2,9 +2,9 @@ mod fork_choice; mod fork_choice_store; pub use crate::fork_choice::{ - AttestationFromBlock, CountUnrealized, Error, ForkChoice, ForkChoiceView, - ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - PersistedForkChoice, QueuedAttestation, ResetPayloadStatuses, + AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, + InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{Block as ProtoBlock, ExecutionStatus, InvalidationOperation}; diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 82bf642f1..d28210aa1 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -12,18 +12,18 @@ use beacon_chain::{ StateSkipConfig, WhenSlotSkipped, }; use fork_choice::{ - CountUnrealized, ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, - QueuedAttestation, + ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation, }; use store::MemoryStore; use types::{ test_utils::generate_deterministic_keypair, BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, - Epoch, EthSpec, Hash256, IndexedAttestation, MainnetEthSpec, SignedBeaconBlock, Slot, SubnetId, + Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, MainnetEthSpec, ProgressiveBalancesMode, + RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, }; pub type E = MainnetEthSpec; -pub const VALIDATOR_COUNT: usize = 32; +pub const VALIDATOR_COUNT: usize = 64; /// Defines some delay between when an attestation is created and when it is mutated. pub enum MutationDelay { @@ -69,6 +69,24 @@ impl ForkChoiceTest { Self { harness } } + /// Creates a new tester with the specified `ProgressiveBalancesMode` and genesis from latest fork. + fn new_with_progressive_balances_mode(mode: ProgressiveBalancesMode) -> ForkChoiceTest { + // genesis with latest fork (at least altair required to test the cache) + let spec = ForkName::latest().make_genesis_spec(ChainSpec::default()); + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .chain_config(ChainConfig { + progressive_balances_mode: mode, + ..ChainConfig::default() + }) + .deterministic_keypairs(VALIDATOR_COUNT) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + Self { harness } + } + /// Get a value from the `ForkChoice` instantiation. fn get<T, U>(&self, func: T) -> U where @@ -213,6 +231,39 @@ impl ForkChoiceTest { self } + /// Slash a validator from the previous epoch committee. + pub async fn add_previous_epoch_attester_slashing(self) -> Self { + let state = self.harness.get_current_state(); + let previous_epoch_shuffling = state.get_shuffling(RelativeEpoch::Previous).unwrap(); + let validator_indices = previous_epoch_shuffling + .iter() + .map(|idx| *idx as u64) + .take(1) + .collect(); + + self.harness + .add_attester_slashing(validator_indices) + .unwrap(); + + self + } + + /// Slash the proposer of a block in the previous epoch. + pub async fn add_previous_epoch_proposer_slashing(self, slots_per_epoch: u64) -> Self { + let previous_epoch_slot = self.harness.get_current_slot() - slots_per_epoch; + let previous_epoch_block = self + .harness + .chain + .block_at_slot(previous_epoch_slot, WhenSlotSkipped::None) + .unwrap() + .unwrap(); + let proposer_index: u64 = previous_epoch_block.message().proposer_index(); + + self.harness.add_proposer_slashing(proposer_index).unwrap(); + + self + } + /// Apply `count` blocks to the chain (without attestations). pub async fn apply_blocks_without_new_attestations(self, count: usize) -> Self { self.harness.advance_slot(); @@ -287,8 +338,9 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, + self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - CountUnrealized::True, + self.harness.logger(), ) .unwrap(); self @@ -330,8 +382,9 @@ impl ForkChoiceTest { Duration::from_secs(0), &state, PayloadVerificationStatus::Verified, + self.harness.chain.config.progressive_balances_mode, &self.harness.chain.spec, - CountUnrealized::True, + self.harness.logger(), ) .err() .expect("on_block did not return an error"); @@ -1290,3 +1343,49 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() { .assert_finalized_epoch_is_less_than(checkpoint.epoch) .assert_shutdown_signal_sent(); } + +/// Checks that `ProgressiveBalancesCache` is updated correctly after an attester slashing event, +/// where the slashed validator is a target attester in previous / current epoch. +#[tokio::test] +async fn progressive_balances_cache_attester_slashing() { + ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .add_previous_epoch_attester_slashing() + .await + // expect fork choice to import blocks successfully after a previous epoch attester is + // slashed, i.e. the slashed attester's balance is correctly excluded from + // the previous epoch total balance in `ProgressiveBalancesCache`. + .apply_blocks(1) + .await + // expect fork choice to import another epoch of blocks successfully - the slashed + // attester's balance should be excluded from the current epoch total balance in + // `ProgressiveBalancesCache` as well. + .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .await; +} + +/// Checks that `ProgressiveBalancesCache` is updated correctly after a proposer slashing event, +/// where the slashed validator is a target attester in previous / current epoch. +#[tokio::test] +async fn progressive_balances_cache_proposer_slashing() { + ForkChoiceTest::new_with_progressive_balances_mode(ProgressiveBalancesMode::Strict) + // first two epochs + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .add_previous_epoch_proposer_slashing(MainnetEthSpec::slots_per_epoch()) + .await + // expect fork choice to import blocks successfully after a previous epoch proposer is + // slashed, i.e. the slashed proposer's balance is correctly excluded from + // the previous epoch total balance in `ProgressiveBalancesCache`. + .apply_blocks(1) + .await + // expect fork choice to import another epoch of blocks successfully - the slashed + // proposer's balance should be excluded from the current epoch total balance in + // `ProgressiveBalancesCache` as well. + .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .await; +} diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index 73dfec40f..03bec9d38 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -2,11 +2,11 @@ name = "int_to_bytes" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -bytes = "1.0.1" +bytes = { workspace = true } [dev-dependencies] yaml-rust = "0.4.4" -hex = "0.4.2" +hex = { workspace = true } diff --git a/consensus/merkle_proof/Cargo.toml b/consensus/merkle_proof/Cargo.toml index 2b883f864..3bee25eaa 100644 --- a/consensus/merkle_proof/Cargo.toml +++ b/consensus/merkle_proof/Cargo.toml @@ -2,17 +2,17 @@ name = "merkle_proof" version = "0.2.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -ethereum-types = "0.14.1" -ethereum_hashing = "1.0.0-beta.2" -lazy_static = "1.4.0" -safe_arith = { path = "../safe_arith" } +ethereum-types = { workspace = true } +ethereum_hashing = { workspace = true } +lazy_static = { workspace = true } +safe_arith = { workspace = true } [dev-dependencies] -quickcheck = "0.9.2" -quickcheck_macros = "0.9.1" +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index 81a535e34..b30173eb7 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -2,18 +2,18 @@ name = "proto_array" version = "0.2.0" authors = ["Paul Hauner <paul@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [[bin]] name = "proto_array" path = "src/bin.rs" [dependencies] -types = { path = "../types" } -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -serde = "1.0.116" +types = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -serde_yaml = "0.8.13" -safe_arith = { path = "../safe_arith" } -superstruct = "0.5.0" \ No newline at end of file +serde_yaml = { workspace = true } +safe_arith = { workspace = true } +superstruct = { workspace = true } \ No newline at end of file diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 157f072ad..98d43e485 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -80,6 +80,7 @@ impl ForkChoiceTestDefinition { let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); let mut fork_choice = ProtoArrayForkChoice::new::<MainnetEthSpec>( + self.finalized_block_slot, self.finalized_block_slot, Hash256::zero(), self.justified_checkpoint, diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 88111b461..7b6afb94f 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -884,7 +884,7 @@ impl ProtoArray { } } else { // Choose the winner by weight. - if child.weight >= best_child.weight { + if child.weight > best_child.weight { change_to_child } else { no_change @@ -910,7 +910,7 @@ impl ProtoArray { Ok(()) } - /// Indicates if the node itself is viable for the head, or if it's best descendant is viable + /// Indicates if the node itself is viable for the head, or if its best descendant is viable /// for the head. fn node_leads_to_viable_head<E: EthSpec>( &self, diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index fe831b3c3..5911e50fc 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -345,6 +345,7 @@ pub struct ProtoArrayForkChoice { impl ProtoArrayForkChoice { #[allow(clippy::too_many_arguments)] pub fn new<E: EthSpec>( + current_slot: Slot, finalized_block_slot: Slot, finalized_block_state_root: Hash256, justified_checkpoint: Checkpoint, @@ -380,7 +381,7 @@ impl ProtoArrayForkChoice { }; proto_array - .on_block::<E>(block, finalized_block_slot) + .on_block::<E>(block, current_slot) .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; Ok(Self { @@ -983,6 +984,7 @@ mod test_compute_deltas { }; let mut fc = ProtoArrayForkChoice::new::<MainnetEthSpec>( + genesis_slot, genesis_slot, state_root, genesis_checkpoint, @@ -1108,6 +1110,7 @@ mod test_compute_deltas { }; let mut fc = ProtoArrayForkChoice::new::<MainnetEthSpec>( + genesis_slot, genesis_slot, junk_state_root, genesis_checkpoint, diff --git a/consensus/safe_arith/Cargo.toml b/consensus/safe_arith/Cargo.toml index d212f9884..6f2e4b811 100644 --- a/consensus/safe_arith/Cargo.toml +++ b/consensus/safe_arith/Cargo.toml @@ -2,7 +2,7 @@ name = "safe_arith" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/consensus/safe_arith/src/iter.rs b/consensus/safe_arith/src/iter.rs index 1fc3d3a1a..d5ee51b58 100644 --- a/consensus/safe_arith/src/iter.rs +++ b/consensus/safe_arith/src/iter.rs @@ -28,10 +28,10 @@ mod test { #[test] fn unsigned_sum_small() { - let v = vec![400u64, 401, 402, 403, 404, 405, 406]; + let arr = [400u64, 401, 402, 403, 404, 405, 406]; assert_eq!( - v.iter().copied().safe_sum().unwrap(), - v.iter().copied().sum() + arr.iter().copied().safe_sum().unwrap(), + arr.iter().copied().sum() ); } @@ -61,10 +61,10 @@ mod test { #[test] fn signed_sum_almost_overflow() { - let v = vec![i64::MIN, 1, -1i64, i64::MAX, i64::MAX, 1]; + let arr = [i64::MIN, 1, -1i64, i64::MAX, i64::MAX, 1]; assert_eq!( - v.iter().copied().safe_sum().unwrap(), - v.iter().copied().sum() + arr.iter().copied().safe_sum().unwrap(), + arr.iter().copied().sum() ); } } diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index c16742782..e4dfb45d5 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -2,42 +2,39 @@ name = "state_processing" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>", "Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dev-dependencies] -env_logger = "0.9.0" -beacon_chain = { path = "../../beacon_node/beacon_chain" } -tokio = { version = "1.14.0", features = ["rt-multi-thread"] } +env_logger = { workspace = true } +beacon_chain = { workspace = true } +tokio = { workspace = true } [dependencies] -bls = { path = "../../crypto/bls" } +bls = { workspace = true } integer-sqrt = "0.1.5" -itertools = "0.10.0" -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -ssz_types = "0.5.0" -merkle_proof = { path = "../merkle_proof" } -safe_arith = { path = "../safe_arith" } -tree_hash = "0.5.0" -types = { path = "../types", default-features = false } -rayon = "1.4.1" -ethereum_hashing = "1.0.0-beta.2" -int_to_bytes = { path = "../int_to_bytes" } -smallvec = "1.6.1" -arbitrary = { version = "1.0", features = ["derive"], optional = true } -lighthouse_metrics = { path = "../../common/lighthouse_metrics", optional = true } -lazy_static = { version = "1.4.0", optional = true } -derivative = "2.1.1" +itertools = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +ssz_types = { workspace = true } +merkle_proof = { workspace = true } +safe_arith = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +rayon = { workspace = true } +ethereum_hashing = { workspace = true } +int_to_bytes = { workspace = true } +smallvec = { workspace = true } +arbitrary = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +derivative = { workspace = true } [features] -default = ["legacy-arith", "metrics"] +default = ["legacy-arith"] fake_crypto = ["bls/fake_crypto"] legacy-arith = ["types/legacy-arith"] -metrics = ["lighthouse_metrics", "lazy_static"] arbitrary-fuzz = [ - "arbitrary", "types/arbitrary-fuzz", - "bls/arbitrary", "merkle_proof/arbitrary", "ethereum_ssz/arbitrary", "ssz_types/arbitrary", diff --git a/consensus/state_processing/src/common/base.rs b/consensus/state_processing/src/common/base.rs index b5cb38272..a8d04ad6c 100644 --- a/consensus/state_processing/src/common/base.rs +++ b/consensus/state_processing/src/common/base.rs @@ -17,3 +17,15 @@ pub fn get_base_reward<T: EthSpec>( .safe_div(spec.base_rewards_per_epoch) .map_err(Into::into) } + +pub fn get_base_reward_from_effective_balance<T: EthSpec>( + effective_balance: u64, + total_active_balance: u64, + spec: &ChainSpec, +) -> Result<u64, BeaconStateError> { + effective_balance + .safe_mul(spec.base_reward_factor)? + .safe_div(total_active_balance.integer_sqrt())? + .safe_div(spec.base_rewards_per_epoch) + .map_err(Into::into) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 8a2e2439b..ffe8be3a0 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -7,6 +7,7 @@ mod slash_validator; pub mod altair; pub mod base; +pub mod update_progressive_balances_cache; pub use deposit_data_tree::DepositDataTree; pub use get_attestation_participation::get_attestation_participation_flag_indices; diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index d4675f5ef..d54da43a0 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -1,3 +1,4 @@ +use crate::common::update_progressive_balances_cache::update_progressive_balances_on_slashing; use crate::{ common::{decrease_balance, increase_balance, initiate_validator_exit}, per_block_processing::errors::BlockProcessingError, @@ -43,6 +44,8 @@ pub fn slash_validator<T: EthSpec>( .safe_div(spec.min_slashing_penalty_quotient_for_state(state))?, )?; + update_progressive_balances_on_slashing(state, slashed_index)?; + // Apply proposer and whistleblower rewards let proposer_index = ctxt.get_proposer_index(state, spec)? as usize; let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs new file mode 100644 index 000000000..45b5d657a --- /dev/null +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -0,0 +1,142 @@ +/// A collection of all functions that mutates the `ProgressiveBalancesCache`. +use crate::metrics::{ + PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, +}; +use crate::per_epoch_processing::altair::ParticipationCache; +use crate::{BlockProcessingError, EpochProcessingError}; +use lighthouse_metrics::set_gauge; +use ssz_types::VariableList; +use std::borrow::Cow; +use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; +use types::{ + is_progressive_balances_enabled, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, + ParticipationFlags, ProgressiveBalancesCache, +}; + +/// Initializes the `ProgressiveBalancesCache` cache using balance values from the +/// `ParticipationCache`. If the optional `&ParticipationCache` is not supplied, it will be computed +/// from the `BeaconState`. +pub fn initialize_progressive_balances_cache<E: EthSpec>( + state: &mut BeaconState<E>, + maybe_participation_cache: Option<&ParticipationCache>, + spec: &ChainSpec, +) -> Result<(), BeaconStateError> { + if !is_progressive_balances_enabled(state) + || state.progressive_balances_cache().is_initialized() + { + return Ok(()); + } + + let participation_cache = match maybe_participation_cache { + Some(cache) => Cow::Borrowed(cache), + None => Cow::Owned(ParticipationCache::new(state, spec)?), + }; + + let previous_epoch_target_attesting_balance = participation_cache + .previous_epoch_target_attesting_balance_raw() + .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; + + let current_epoch_target_attesting_balance = participation_cache + .current_epoch_target_attesting_balance_raw() + .map_err(|e| BeaconStateError::ParticipationCacheError(format!("{e:?}")))?; + + let current_epoch = state.current_epoch(); + state.progressive_balances_cache_mut().initialize( + current_epoch, + previous_epoch_target_attesting_balance, + current_epoch_target_attesting_balance, + ); + + update_progressive_balances_metrics(state.progressive_balances_cache())?; + + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` when a new target attestation has been processed. +pub fn update_progressive_balances_on_attestation<T: EthSpec>( + state: &mut BeaconState<T>, + epoch: Epoch, + validator_index: usize, +) -> Result<(), BlockProcessingError> { + if is_progressive_balances_enabled(state) { + let validator = state.get_validator(validator_index)?; + if !validator.slashed { + let validator_effective_balance = validator.effective_balance; + state + .progressive_balances_cache_mut() + .on_new_target_attestation(epoch, validator_effective_balance)?; + } + } + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` when a target attester has been slashed. +pub fn update_progressive_balances_on_slashing<T: EthSpec>( + state: &mut BeaconState<T>, + validator_index: usize, +) -> Result<(), BlockProcessingError> { + if is_progressive_balances_enabled(state) { + let previous_epoch_participation = state.previous_epoch_participation()?; + let is_previous_epoch_target_attester = + is_target_attester_in_epoch::<T>(previous_epoch_participation, validator_index)?; + + let current_epoch_participation = state.current_epoch_participation()?; + let is_current_epoch_target_attester = + is_target_attester_in_epoch::<T>(current_epoch_participation, validator_index)?; + + let validator_effective_balance = state.get_effective_balance(validator_index)?; + + state.progressive_balances_cache_mut().on_slashing( + is_previous_epoch_target_attester, + is_current_epoch_target_attester, + validator_effective_balance, + )?; + } + + Ok(()) +} + +/// Updates the `ProgressiveBalancesCache` on epoch transition. +pub fn update_progressive_balances_on_epoch_transition<T: EthSpec>( + state: &mut BeaconState<T>, + spec: &ChainSpec, +) -> Result<(), EpochProcessingError> { + if is_progressive_balances_enabled(state) { + state + .progressive_balances_cache_mut() + .on_epoch_transition(spec)?; + + update_progressive_balances_metrics(state.progressive_balances_cache())?; + } + + Ok(()) +} + +pub fn update_progressive_balances_metrics( + cache: &ProgressiveBalancesCache, +) -> Result<(), BeaconStateError> { + set_gauge( + &PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + cache.previous_epoch_target_attesting_balance()? as i64, + ); + + set_gauge( + &PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + cache.current_epoch_target_attesting_balance()? as i64, + ); + + Ok(()) +} + +fn is_target_attester_in_epoch<T: EthSpec>( + epoch_participation: &VariableList<ParticipationFlags, T::ValidatorRegistryLimit>, + validator_index: usize, +) -> Result<bool, BlockProcessingError> { + let participation_flags = epoch_participation + .get(validator_index) + .ok_or(BeaconStateError::UnknownValidator(validator_index))?; + participation_flags + .has_flag(TIMELY_TARGET_FLAG_INDEX) + .map_err(|e| e.into()) +} diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 68f04b554..ebbc8f9f3 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -92,7 +92,7 @@ pub fn initialize_beacon_state_from_eth1<T: EthSpec>( } // Now that we have our validators, initialize the caches (including the committees) - state.build_all_caches(spec)?; + state.build_caches(spec)?; // Set genesis validators root for domain separation and chain versioning *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache()?; @@ -115,7 +115,7 @@ pub fn process_activations<T: EthSpec>( state: &mut BeaconState<T>, spec: &ChainSpec, ) -> Result<(), Error> { - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter_mut().enumerate() { let balance = balances .get(index) diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 7340206a3..a3ee72540 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -2,7 +2,7 @@ #![cfg_attr( not(test), deny( - clippy::integer_arithmetic, + clippy::arithmetic_side_effects, clippy::disallowed_methods, clippy::indexing_slicing, clippy::unwrap_used, diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index ddfaae564..d8a51135e 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "metrics")] - use lazy_static::lazy_static; pub use lighthouse_metrics::*; @@ -23,4 +21,15 @@ lazy_static! { "beacon_participation_prev_epoch_active_gwei_total", "Total effective balance (gwei) of validators active in the previous epoch" ); + /* + * Participation Metrics (progressive balances) + */ + pub static ref PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Result<IntGauge> = 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" + ); + pub static ref PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL: Result<IntGauge> = 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" + ); } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 124fdf650..b8b76a499 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -41,6 +41,9 @@ mod verify_proposer_slashing; use crate::common::decrease_balance; use crate::StateProcessingStrategy; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_metrics, +}; #[cfg(feature = "arbitrary-fuzz")] use arbitrary::Arbitrary; @@ -114,6 +117,8 @@ pub fn per_block_processing<T: EthSpec, Payload: AbstractExecPayload<T>>( .fork_name(spec) .map_err(BlockProcessingError::InconsistentStateFork)?; + initialize_progressive_balances_cache(state, None, spec)?; + let verify_signatures = match block_signature_strategy { BlockSignatureStrategy::VerifyBulk => { // Verify all signatures in the block at once. @@ -182,6 +187,10 @@ pub fn per_block_processing<T: EthSpec, Payload: AbstractExecPayload<T>>( )?; } + if is_progressive_balances_enabled(state) { + update_progressive_balances_metrics(state.progressive_balances_cache())?; + } + Ok(()) } 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 709302eec..22309334f 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 @@ -1,4 +1,4 @@ -#![allow(clippy::integer_arithmetic)] +#![allow(clippy::arithmetic_side_effects)] use super::signature_sets::{Error as SignatureSetError, *}; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 1aaf298d6..0aba1d83f 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,6 +1,8 @@ use super::signature_sets::Error as SignatureSetError; +use crate::per_epoch_processing::altair::participation_cache; use crate::ContextError; use merkle_proof::MerkleTreeError; +use participation_cache::Error as ParticipationCacheError; use safe_arith::ArithError; use ssz::DecodeError; use types::*; @@ -83,6 +85,7 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + ParticipationCacheError(ParticipationCacheError), } impl From<BeaconStateError> for BlockProcessingError { @@ -140,6 +143,12 @@ impl From<BlockOperationError<HeaderInvalid>> for BlockProcessingError { } } +impl From<ParticipationCacheError> for BlockProcessingError { + fn from(e: ParticipationCacheError) -> Self { + BlockProcessingError::ParticipationCacheError(e) + } +} + /// A conversion that consumes `self` and adds an `index` variable to resulting struct. /// /// Used here to allow converting an error into an upstream error that points to the object that 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 4bee59661..1dbcb7fb8 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -97,6 +97,8 @@ pub mod base { pub mod altair { use super::*; + use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; + use types::consts::altair::TIMELY_TARGET_FLAG_INDEX; pub fn process_attestations<T: EthSpec>( state: &mut BeaconState<T>, @@ -163,6 +165,14 @@ pub mod altair { get_base_reward(state, index, base_reward_per_increment, spec)? .safe_mul(weight)?, )?; + + if flag_index == TIMELY_TARGET_FLAG_INDEX { + update_progressive_balances_on_attestation( + state, + data.target.epoch, + index, + )?; + } } } } @@ -235,6 +245,7 @@ pub fn process_attester_slashings<T: EthSpec>( Ok(()) } + /// Wrapper function to handle calling the correct version of `process_attestations` based on /// the fork. pub fn process_attestations<T: EthSpec, Payload: AbstractExecPayload<T>>( diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index ddb9ca6ad..16fa2462f 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -960,7 +960,7 @@ async fn fork_spanning_exit() { spec.bellatrix_fork_epoch = Some(Epoch::new(4)); spec.shard_committee_period = 0; - let harness = BeaconChainHarness::builder(MainnetEthSpec::default()) + let harness = BeaconChainHarness::builder(MainnetEthSpec) .spec(spec.clone()) .deterministic_keypairs(VALIDATOR_COUNT) .mock_execution_layer() diff --git a/consensus/state_processing/src/per_epoch_processing/altair.rs b/consensus/state_processing/src/per_epoch_processing/altair.rs index d5df2fc97..0abbd16a9 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair.rs @@ -1,4 +1,7 @@ use super::{process_registry_updates, process_slashings, EpochProcessingSummary, Error}; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, +}; use crate::per_epoch_processing::{ effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, @@ -31,6 +34,7 @@ pub fn process_epoch<T: EthSpec>( // Pre-compute participating indices and total balances. let participation_cache = ParticipationCache::new(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); + initialize_progressive_balances_cache::<T>(state, Some(&participation_cache), spec)?; // Justification and finalization. let justification_and_finalization_state = @@ -56,7 +60,7 @@ pub fn process_epoch<T: EthSpec>( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, Some(&participation_cache), spec)?; // Reset slashings process_slashings_reset(state)?; @@ -75,6 +79,8 @@ pub fn process_epoch<T: EthSpec>( // Rotate the epoch caches to suit the epoch transition. state.advance_caches(spec)?; + update_progressive_balances_on_epoch_transition(state, spec)?; + Ok(EpochProcessingSummary::Altair { participation_cache, sync_committee, 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 967f642e8..a895567d1 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 @@ -34,7 +34,7 @@ pub fn process_inactivity_updates<T: EthSpec>( .safe_add_assign(spec.inactivity_score_bias)?; } // Decrease the score of all validators for forgiveness when not during a leak - if !state.is_in_inactivity_leak(previous_epoch, spec) { + if !state.is_in_inactivity_leak(previous_epoch, spec)? { let inactivity_score = state.get_inactivity_score_mut(index)?; inactivity_score .safe_sub_assign(min(spec.inactivity_score_recovery_rate, *inactivity_score))?; diff --git a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs index 004726923..d67e7874c 100644 --- a/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs +++ b/consensus/state_processing/src/per_epoch_processing/altair/participation_cache.rs @@ -11,49 +11,23 @@ //! Additionally, this cache is returned from the `altair::process_epoch` function and can be used //! to get useful summaries about the validator participation in an epoch. -use safe_arith::{ArithError, SafeArith}; use types::{ consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }, - BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, RelativeEpoch, + Balance, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ParticipationFlags, + RelativeEpoch, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Error { InvalidFlagIndex(usize), InvalidValidatorIndex(usize), } -/// 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)] -struct Balance { - raw: u64, - minimum: u64, -} - -impl Balance { - /// Initialize the balance to `0`, or the given `minimum`. - pub fn zero(minimum: u64) -> Self { - Self { raw: 0, minimum } - } - - /// Returns the balance with respect to the initialization `minimum`. - pub fn get(&self) -> u64 { - std::cmp::max(self.raw, self.minimum) - } - - /// Add-assign to the balance. - pub fn safe_add_assign(&mut self, other: u64) -> Result<(), ArithError> { - self.raw.safe_add_assign(other) - } -} - /// Caches the participation values for one epoch (either the previous or current). -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] struct SingleEpochParticipationCache { /// Maps an active validator index to their participation flags. /// @@ -95,6 +69,14 @@ impl SingleEpochParticipationCache { .ok_or(Error::InvalidFlagIndex(flag_index)) } + /// Returns the raw total balance of attesters who have `flag_index` set. + fn total_flag_balance_raw(&self, flag_index: usize) -> Result<Balance, Error> { + self.total_flag_balances + .get(flag_index) + .copied() + .ok_or(Error::InvalidFlagIndex(flag_index)) + } + /// Returns `true` if `val_index` is active, unslashed and has `flag_index` set. /// /// ## Errors @@ -129,8 +111,8 @@ impl SingleEpochParticipationCache { current_epoch: Epoch, relative_epoch: RelativeEpoch, ) -> Result<(), BeaconStateError> { - let val_balance = state.get_effective_balance(val_index)?; let validator = state.get_validator(val_index)?; + let val_balance = validator.effective_balance; // Sanity check to ensure the validator is active. let epoch = relative_epoch.into_epoch(current_epoch); @@ -173,7 +155,7 @@ impl SingleEpochParticipationCache { } /// Maintains a cache to be used during `altair::process_epoch`. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct ParticipationCache { current_epoch: Epoch, /// Caches information about active validators pertaining to `self.current_epoch`. @@ -291,6 +273,11 @@ impl ParticipationCache { .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) } + pub fn current_epoch_target_attesting_balance_raw(&self) -> Result<Balance, Error> { + self.current_epoch_participation + .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) + } + pub fn previous_epoch_total_active_balance(&self) -> u64 { self.previous_epoch_participation.total_active_balance.get() } @@ -300,6 +287,11 @@ impl ParticipationCache { .total_flag_balance(TIMELY_TARGET_FLAG_INDEX) } + pub fn previous_epoch_target_attesting_balance_raw(&self) -> Result<Balance, Error> { + self.previous_epoch_participation + .total_flag_balance_raw(TIMELY_TARGET_FLAG_INDEX) + } + pub fn previous_epoch_source_attesting_balance(&self) -> Result<u64, Error> { self.previous_epoch_participation .total_flag_balance(TIMELY_SOURCE_FLAG_INDEX) 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 e2aa67a61..19d57130c 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 @@ -77,7 +77,7 @@ pub fn get_flag_index_deltas<T: EthSpec>( let mut delta = Delta::default(); if unslashed_participating_indices.contains(index)? { - if !state.is_in_inactivity_leak(previous_epoch, spec) { + if !state.is_in_inactivity_leak(previous_epoch, spec)? { let reward_numerator = base_reward .safe_mul(weight)? .safe_mul(unslashed_participating_increments)?; diff --git a/consensus/state_processing/src/per_epoch_processing/base.rs b/consensus/state_processing/src/per_epoch_processing/base.rs index cb7e7d4b3..c5864bd1e 100644 --- a/consensus/state_processing/src/per_epoch_processing/base.rs +++ b/consensus/state_processing/src/per_epoch_processing/base.rs @@ -36,7 +36,7 @@ pub fn process_epoch<T: EthSpec>( justification_and_finalization_state.apply_changes_to_state(state); // Rewards and Penalties. - process_rewards_and_penalties(state, &mut validator_statuses, spec)?; + process_rewards_and_penalties(state, &validator_statuses, spec)?; // Registry Updates. process_registry_updates(state, spec)?; @@ -52,7 +52,7 @@ pub fn process_epoch<T: EthSpec>( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, None, spec)?; // Reset slashings process_slashings_reset(state)?; 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 e7a4d9c4d..74c96d8ae 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 @@ -45,7 +45,7 @@ impl AttestationDelta { /// Apply attester and proposer rewards. pub fn process_rewards_and_penalties<T: EthSpec>( state: &mut BeaconState<T>, - validator_statuses: &mut ValidatorStatuses, + validator_statuses: &ValidatorStatuses, spec: &ChainSpec, ) -> Result<(), Error> { if state.current_epoch() == T::genesis_epoch() { @@ -59,7 +59,7 @@ pub fn process_rewards_and_penalties<T: EthSpec>( return Err(Error::ValidatorStatusesInconsistent); } - let deltas = get_attestation_deltas(state, validator_statuses, spec)?; + let deltas = get_attestation_deltas_all(state, validator_statuses, spec)?; // Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0 // instead). @@ -73,10 +73,41 @@ pub fn process_rewards_and_penalties<T: EthSpec>( } /// Apply rewards for participation in attestations during the previous epoch. -pub fn get_attestation_deltas<T: EthSpec>( +pub fn get_attestation_deltas_all<T: EthSpec>( state: &BeaconState<T>, validator_statuses: &ValidatorStatuses, spec: &ChainSpec, +) -> Result<Vec<AttestationDelta>, Error> { + get_attestation_deltas(state, validator_statuses, None, spec) +} + +/// Apply rewards for participation in attestations during the previous epoch, and only compute +/// rewards for a subset of validators. +pub fn get_attestation_deltas_subset<T: EthSpec>( + state: &BeaconState<T>, + validator_statuses: &ValidatorStatuses, + validators_subset: &Vec<usize>, + spec: &ChainSpec, +) -> Result<Vec<(usize, AttestationDelta)>, Error> { + get_attestation_deltas(state, validator_statuses, Some(validators_subset), spec).map(|deltas| { + deltas + .into_iter() + .enumerate() + .filter(|(index, _)| validators_subset.contains(index)) + .collect() + }) +} + +/// Apply rewards for participation in attestations during the previous epoch. +/// If `maybe_validators_subset` specified, only the deltas for the specified validator subset is +/// returned, otherwise deltas for all validators are returned. +/// +/// Returns a vec of validator indices to `AttestationDelta`. +fn get_attestation_deltas<T: EthSpec>( + state: &BeaconState<T>, + validator_statuses: &ValidatorStatuses, + maybe_validators_subset: Option<&Vec<usize>>, + spec: &ChainSpec, ) -> Result<Vec<AttestationDelta>, Error> { let previous_epoch = state.previous_epoch(); let finality_delay = state @@ -88,6 +119,13 @@ pub fn get_attestation_deltas<T: EthSpec>( let total_balances = &validator_statuses.total_balances; + // Ignore validator if a subset is specified and validator is not in the subset + let include_validator_delta = |idx| match maybe_validators_subset.as_ref() { + None => true, + Some(validators_subset) if validators_subset.contains(&idx) => true, + Some(_) => false, + }; + for (index, validator) in validator_statuses.statuses.iter().enumerate() { // Ignore ineligible validators. All sub-functions of the spec do this except for // `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in @@ -99,41 +137,46 @@ pub fn get_attestation_deltas<T: EthSpec>( let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?; - let source_delta = - get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?; - let target_delta = - get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?; - let head_delta = - get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?; let (inclusion_delay_delta, proposer_delta) = get_inclusion_delay_delta(validator, base_reward, spec)?; - let inactivity_penalty_delta = - get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?; - let delta = deltas - .get_mut(index) - .ok_or(Error::DeltaOutOfBounds(index))?; - delta.source_delta.combine(source_delta)?; - delta.target_delta.combine(target_delta)?; - delta.head_delta.combine(head_delta)?; - delta.inclusion_delay_delta.combine(inclusion_delay_delta)?; - delta - .inactivity_penalty_delta - .combine(inactivity_penalty_delta)?; + if include_validator_delta(index) { + let source_delta = + get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?; + let target_delta = + get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?; + let head_delta = + get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?; + let inactivity_penalty_delta = + get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?; + + let delta = deltas + .get_mut(index) + .ok_or(Error::DeltaOutOfBounds(index))?; + delta.source_delta.combine(source_delta)?; + delta.target_delta.combine(target_delta)?; + delta.head_delta.combine(head_delta)?; + delta.inclusion_delay_delta.combine(inclusion_delay_delta)?; + delta + .inactivity_penalty_delta + .combine(inactivity_penalty_delta)?; + } if let Some((proposer_index, proposer_delta)) = proposer_delta { - deltas - .get_mut(proposer_index) - .ok_or(Error::ValidatorStatusesInconsistent)? - .inclusion_delay_delta - .combine(proposer_delta)?; + if include_validator_delta(proposer_index) { + deltas + .get_mut(proposer_index) + .ok_or(Error::ValidatorStatusesInconsistent)? + .inclusion_delay_delta + .combine(proposer_delta)?; + } } } Ok(deltas) } -fn get_attestation_component_delta( +pub fn get_attestation_component_delta( index_in_unslashed_attesting_indices: bool, attesting_balance: u64, total_balances: &TotalBalances, @@ -216,7 +259,7 @@ fn get_head_delta( ) } -fn get_inclusion_delay_delta( +pub fn get_inclusion_delay_delta( validator: &ValidatorStatus, base_reward: u64, spec: &ChainSpec, @@ -242,7 +285,7 @@ fn get_inclusion_delay_delta( } } -fn get_inactivity_penalty_delta( +pub fn get_inactivity_penalty_delta( validator: &ValidatorStatus, base_reward: u64, finality_delay: u64, diff --git a/consensus/state_processing/src/per_epoch_processing/capella.rs b/consensus/state_processing/src/per_epoch_processing/capella.rs index aaf301f29..911510ed0 100644 --- a/consensus/state_processing/src/per_epoch_processing/capella.rs +++ b/consensus/state_processing/src/per_epoch_processing/capella.rs @@ -11,6 +11,9 @@ use crate::per_epoch_processing::{ }; use types::{BeaconState, ChainSpec, EthSpec, RelativeEpoch}; +use crate::common::update_progressive_balances_cache::{ + initialize_progressive_balances_cache, update_progressive_balances_on_epoch_transition, +}; pub use historical_summaries_update::process_historical_summaries_update; mod historical_summaries_update; @@ -27,6 +30,7 @@ pub fn process_epoch<T: EthSpec>( // Pre-compute participating indices and total balances. let participation_cache = ParticipationCache::new(state, spec)?; let sync_committee = state.current_sync_committee()?.clone(); + initialize_progressive_balances_cache(state, Some(&participation_cache), spec)?; // Justification and finalization. let justification_and_finalization_state = @@ -52,7 +56,7 @@ pub fn process_epoch<T: EthSpec>( process_eth1_data_reset(state)?; // Update effective balances with hysteresis (lag). - process_effective_balance_updates(state, spec)?; + process_effective_balance_updates(state, Some(&participation_cache), spec)?; // Reset slashings process_slashings_reset(state)?; @@ -71,6 +75,8 @@ pub fn process_epoch<T: EthSpec>( // Rotate the epoch caches to suit the epoch transition. state.advance_caches(spec)?; + update_progressive_balances_on_epoch_transition(state, spec)?; + Ok(EpochProcessingSummary::Altair { participation_cache, sync_committee, 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 c166667b5..1759f7e14 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,11 +1,13 @@ use super::errors::EpochProcessingError; +use crate::per_epoch_processing::altair::ParticipationCache; use safe_arith::SafeArith; use types::beacon_state::BeaconState; use types::chain_spec::ChainSpec; -use types::{BeaconStateError, EthSpec}; +use types::{BeaconStateError, EthSpec, ProgressiveBalancesCache}; pub fn process_effective_balance_updates<T: EthSpec>( state: &mut BeaconState<T>, + maybe_participation_cache: Option<&ParticipationCache>, spec: &ChainSpec, ) -> Result<(), EpochProcessingError> { let hysteresis_increment = spec @@ -13,7 +15,8 @@ pub fn process_effective_balance_updates<T: EthSpec>( .safe_div(spec.hysteresis_quotient)?; let downward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_downward_multiplier)?; let upward_threshold = hysteresis_increment.safe_mul(spec.hysteresis_upward_multiplier)?; - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, progressive_balances_cache) = + state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter_mut().enumerate() { let balance = balances .get(index) @@ -23,11 +26,43 @@ pub fn process_effective_balance_updates<T: EthSpec>( if balance.safe_add(downward_threshold)? < validator.effective_balance || validator.effective_balance.safe_add(upward_threshold)? < balance { - validator.effective_balance = std::cmp::min( + let old_effective_balance = validator.effective_balance; + let new_effective_balance = std::cmp::min( balance.safe_sub(balance.safe_rem(spec.effective_balance_increment)?)?, spec.max_effective_balance, ); + + if let Some(participation_cache) = maybe_participation_cache { + update_progressive_balances( + participation_cache, + progressive_balances_cache, + index, + old_effective_balance, + new_effective_balance, + )?; + } + + validator.effective_balance = new_effective_balance; } } Ok(()) } + +fn update_progressive_balances( + participation_cache: &ParticipationCache, + progressive_balances_cache: &mut ProgressiveBalancesCache, + index: usize, + old_effective_balance: u64, + new_effective_balance: u64, +) -> Result<(), EpochProcessingError> { + if old_effective_balance != new_effective_balance { + let is_current_epoch_target_attester = + participation_cache.is_current_epoch_timely_target_attester(index)?; + progressive_balances_cache.on_effective_balance_change( + is_current_epoch_target_attester, + old_effective_balance, + new_effective_balance, + )?; + } + Ok(()) +} 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 6eb2f9776..89bc4ab5a 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 @@ -21,7 +21,6 @@ pub enum EpochProcessingSummary<T: EthSpec> { impl<T: EthSpec> EpochProcessingSummary<T> { /// Updates some Prometheus metrics with some values in `self`. - #[cfg(feature = "metrics")] pub fn observe_metrics(&self) -> Result<(), ParticipationCacheError> { metrics::set_gauge( &metrics::PARTICIPATION_PREV_EPOCH_HEAD_ATTESTING_GWEI_TOTAL, diff --git a/consensus/state_processing/src/per_epoch_processing/slashings.rs b/consensus/state_processing/src/per_epoch_processing/slashings.rs index 6d5342cd3..2d595491c 100644 --- a/consensus/state_processing/src/per_epoch_processing/slashings.rs +++ b/consensus/state_processing/src/per_epoch_processing/slashings.rs @@ -16,7 +16,7 @@ pub fn process_slashings<T: EthSpec>( total_balance, ); - let (validators, balances) = state.validators_and_balances_mut(); + let (validators, balances, _) = state.validators_and_balances_and_progressive_balances_mut(); for (index, validator) in validators.iter().enumerate() { if validator.slashed && epoch.safe_add(T::EpochsPerSlashingsVector::to_u64().safe_div(2)?)? diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index ead06edbf..e16fb4a7b 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -21,7 +21,7 @@ impl From<ArithError> 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 magniude faster. +/// Providing the `state_root` makes this function several orders of magnitude faster. pub fn per_slot_processing<T: EthSpec>( state: &mut BeaconState<T>, state_root: Option<Hash256>, diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 176f1af15..26b1192bc 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -1,3 +1,4 @@ +use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use crate::common::{get_attestation_participation_flag_indices, get_attesting_indices}; use std::mem; use std::sync::Arc; @@ -101,6 +102,7 @@ pub fn upgrade_to_altair<E: EthSpec>( next_sync_committee: temp_sync_committee, // not read // 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), @@ -110,6 +112,8 @@ pub fn upgrade_to_altair<E: EthSpec>( // Fill in previous epoch participation from the pre state's pending attestations. translate_participation(&mut post, &pre.previous_epoch_attestations, spec)?; + initialize_progressive_balances_cache(&mut post, None, spec)?; + // Fill in sync committees // Note: A duplicate committee is assigned for the current and next committee at the fork // boundary diff --git a/consensus/state_processing/src/upgrade/capella.rs b/consensus/state_processing/src/upgrade/capella.rs index 3b933fac3..5153e35f4 100644 --- a/consensus/state_processing/src/upgrade/capella.rs +++ b/consensus/state_processing/src/upgrade/capella.rs @@ -62,6 +62,7 @@ pub fn upgrade_to_capella<E: EthSpec>( historical_summaries: VariableList::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), diff --git a/consensus/state_processing/src/upgrade/merge.rs b/consensus/state_processing/src/upgrade/merge.rs index c17246624..eb7445010 100644 --- a/consensus/state_processing/src/upgrade/merge.rs +++ b/consensus/state_processing/src/upgrade/merge.rs @@ -60,6 +60,7 @@ pub fn upgrade_to_bellatrix<E: EthSpec>( latest_execution_payload_header: <ExecutionPayloadHeaderMerge<E>>::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), diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index 864844080..b3924cd97 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -138,7 +138,7 @@ impl<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit { Ok(SigVerifiedOp::new(self, state)) } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { smallvec![self.message.epoch] } @@ -156,7 +156,7 @@ impl<E: EthSpec> VerifyOperation<E> for AttesterSlashing<E> { Ok(SigVerifiedOp::new(self, state)) } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { smallvec![ self.attestation_1.data.target.epoch, @@ -177,7 +177,7 @@ impl<E: EthSpec> VerifyOperation<E> for ProposerSlashing { Ok(SigVerifiedOp::new(self, state)) } - #[allow(clippy::integer_arithmetic)] + #[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 @@ -200,7 +200,7 @@ impl<E: EthSpec> VerifyOperation<E> for SignedBlsToExecutionChange { Ok(SigVerifiedOp::new(self, state)) } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { smallvec![] } diff --git a/consensus/swap_or_not_shuffle/Cargo.toml b/consensus/swap_or_not_shuffle/Cargo.toml index 303e5cfba..ea9b603c5 100644 --- a/consensus/swap_or_not_shuffle/Cargo.toml +++ b/consensus/swap_or_not_shuffle/Cargo.toml @@ -2,18 +2,18 @@ name = "swap_or_not_shuffle" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [[bench]] name = "benches" harness = false [dev-dependencies] -criterion = "0.3.3" +criterion = { workspace = true } [dependencies] -ethereum_hashing = "1.0.0-beta.2" -ethereum-types = "0.14.1" +ethereum_hashing = { workspace = true } +ethereum-types = { workspace = true } [features] arbitrary = ["ethereum-types/arbitrary"] diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 91ad3089f..d705dfdd5 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -2,69 +2,70 @@ name = "types" version = "0.2.1" authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>"] -edition = "2021" +edition = { workspace = true } [[bench]] name = "benches" harness = false [dependencies] -merkle_proof = { path = "../../consensus/merkle_proof" } -bls = { path = "../../crypto/bls", features = ["arbitrary"] } -compare_fields = { path = "../../common/compare_fields" } -compare_fields_derive = { path = "../../common/compare_fields_derive" } +merkle_proof = { workspace = true } +bls = { workspace = true, features = ["arbitrary"] } +compare_fields = { workspace = true } +compare_fields_derive = { workspace = true } eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } -ethereum-types = { version = "0.14.1", features = ["arbitrary"] } -ethereum_hashing = "1.0.0-beta.2" -hex = "0.4.2" -int_to_bytes = { path = "../int_to_bytes" } -log = "0.4.11" -rayon = "1.4.1" -rand = "0.8.5" -safe_arith = { path = "../safe_arith" } -serde = {version = "1.0.116" , features = ["rc"] } +ethereum-types = { workspace = true, features = ["arbitrary"] } +ethereum_hashing = { workspace = true } +hex = { workspace = true } +int_to_bytes = { workspace = true } +log = { workspace = true } +rayon = { workspace = true } +rand = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true, features = ["rc"] } serde_derive = "1.0.116" -slog = "2.5.2" -ethereum_ssz = { version = "0.5.0", features = ["arbitrary"] } -ethereum_ssz_derive = "0.5.0" -ssz_types = { version = "0.5.0", features = ["arbitrary"] } -swap_or_not_shuffle = { path = "../swap_or_not_shuffle", features = ["arbitrary"] } +slog = { workspace = true } +ethereum_ssz = { workspace = true, features = ["arbitrary"] } +ethereum_ssz_derive = { workspace = true } +ssz_types = { workspace = true, features = ["arbitrary"] } +swap_or_not_shuffle = { workspace = true, features = ["arbitrary"] } test_random_derive = { path = "../../common/test_random_derive" } -tree_hash = { version = "0.5.0", features = ["arbitrary"] } -tree_hash_derive = "0.5.0" +tree_hash = { workspace = true, features = ["arbitrary"] } +tree_hash_derive = { workspace = true } rand_xorshift = "0.3.0" -cached_tree_hash = { path = "../cached_tree_hash" } -serde_yaml = "0.8.13" -tempfile = "3.1.0" -derivative = "2.1.1" -rusqlite = { version = "0.28.0", features = ["bundled"], optional = true } +cached_tree_hash = { workspace = true } +serde_yaml = { workspace = true } +tempfile = { workspace = true } +derivative = { workspace = true } +rusqlite = { workspace = true } # The arbitrary dependency is enabled by default since Capella to avoid complexity introduced by # `AbstractExecPayload` -arbitrary = { version = "1.0", features = ["derive"] } -ethereum_serde_utils = "0.5.0" -regex = "1.5.5" -lazy_static = "1.4.0" -parking_lot = "0.12.0" -itertools = "0.10.0" -superstruct = "0.6.0" +arbitrary = { workspace = true, features = ["derive"] } +ethereum_serde_utils = { workspace = true } +regex = { workspace = true } +lazy_static = { workspace = true } +parking_lot = { workspace = true } +itertools = { workspace = true } +superstruct = { workspace = true } metastruct = "0.1.0" -serde_json = "1.0.74" -smallvec = "1.8.0" +serde_json = { workspace = true } +smallvec = { workspace = true } serde_with = "1.13.0" -maplit = "1.0.2" +maplit = { workspace = true } +strum = { workspace = true } [dev-dependencies] -criterion = "0.3.3" -beacon_chain = { path = "../../beacon_node/beacon_chain" } -eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } -state_processing = { path = "../state_processing" } -tokio = "1.14.0" +criterion = { workspace = true } +beacon_chain = { workspace = true } +state_processing = { workspace = true } +tokio = { workspace = true } +paste = { workspace = true } [features] default = ["sqlite", "legacy-arith"] # Allow saturating arithmetic on slots and epochs. Enabled by default, but deprecated. legacy-arith = [] -sqlite = ["rusqlite"] +sqlite = [] # 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 = [] diff --git a/consensus/types/benches/benches.rs b/consensus/types/benches/benches.rs index 28f57e708..bb2b52710 100644 --- a/consensus/types/benches/benches.rs +++ b/consensus/types/benches/benches.rs @@ -51,7 +51,7 @@ fn all_benches(c: &mut Criterion) { let spec = Arc::new(MainnetEthSpec::default_spec()); let mut state = get_state::<MainnetEthSpec>(validator_count); - state.build_all_caches(&spec).expect("should build caches"); + state.build_caches(&spec).expect("should build caches"); let state_bytes = state.as_ssz_bytes(); let inner_state = state.clone(); diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index c0ba86941..dce1be742 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -89,7 +89,7 @@ impl<'a, T: EthSpec, Payload: AbstractExecPayload<T>> BeaconBlockBodyRef<'a, T, } } -impl<'a, T: EthSpec> BeaconBlockBodyRef<'a, T> { +impl<'a, T: EthSpec, Payload: AbstractExecPayload<T>> BeaconBlockBodyRef<'a, T, Payload> { /// Get the fork_name of this object pub fn fork_name(self) -> ForkName { match self { diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 4a9da3640..6a205e307 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -26,6 +26,8 @@ pub use self::committee_cache::{ compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count, CommitteeCache, }; +pub use crate::beacon_state::balance::Balance; +pub use crate::beacon_state::progressive_balances_cache::*; use crate::historical_summary::HistoricalSummary; pub use clone_config::CloneConfig; pub use eth_spec::*; @@ -34,9 +36,11 @@ pub use tree_hash_cache::BeaconTreeHashCache; #[macro_use] mod committee_cache; +mod balance; mod clone_config; mod exit_cache; mod iter; +mod progressive_balances_cache; mod pubkey_cache; mod tests; mod tree_hash_cache; @@ -101,6 +105,9 @@ pub enum Error { SszTypesError(ssz_types::Error), TreeHashCacheNotInitialized, NonLinearTreeHashCacheHistory, + ParticipationCacheError(String), + ProgressiveBalancesCacheNotInitialized, + ProgressiveBalancesCacheInconsistent, TreeHashCacheSkippedSlot { cache: Slot, state: Slot, @@ -317,6 +324,12 @@ where #[tree_hash(skip_hashing)] #[test_random(default)] #[derivative(Clone(clone_with = "clone_default"))] + pub progressive_balances_cache: ProgressiveBalancesCache, + #[serde(skip_serializing, skip_deserializing)] + #[ssz(skip_serializing, skip_deserializing)] + #[tree_hash(skip_hashing)] + #[test_random(default)] + #[derivative(Clone(clone_with = "clone_default"))] pub committee_caches: [CommitteeCache; CACHED_EPOCHS], #[serde(skip_serializing, skip_deserializing)] #[ssz(skip_serializing, skip_deserializing)] @@ -393,6 +406,7 @@ impl<T: EthSpec> BeaconState<T> { // Caching (not in spec) total_active_balance: None, + progressive_balances_cache: <_>::default(), committee_caches: [ CommitteeCache::default(), CommitteeCache::default(), @@ -428,7 +442,7 @@ impl<T: EthSpec> BeaconState<T> { } /// Specialised deserialisation method that uses the `ChainSpec` as context. - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result<Self, ssz::DecodeError> { // Slot is after genesis_time (u64) and genesis_validators_root (Hash256). let slot_start = <u64 as Decode>::ssz_fixed_len() + <Hash256 as Decode>::ssz_fixed_len(); @@ -757,7 +771,7 @@ impl<T: EthSpec> BeaconState<T> { Ok(signature_hash_int.safe_rem(modulo)? == 0) } - /// Returns the beacon proposer index for the `slot` in the given `relative_epoch`. + /// 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<usize, Error> { @@ -1150,12 +1164,30 @@ impl<T: EthSpec> BeaconState<T> { } /// Convenience accessor for validators and balances simultaneously. - pub fn validators_and_balances_mut(&mut self) -> (&mut [Validator], &mut [u64]) { + pub fn validators_and_balances_and_progressive_balances_mut( + &mut self, + ) -> (&mut [Validator], &mut [u64], &mut ProgressiveBalancesCache) { match self { - BeaconState::Base(state) => (&mut state.validators, &mut state.balances), - BeaconState::Altair(state) => (&mut state.validators, &mut state.balances), - BeaconState::Merge(state) => (&mut state.validators, &mut state.balances), - BeaconState::Capella(state) => (&mut state.validators, &mut state.balances), + BeaconState::Base(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Altair(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Merge(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), + BeaconState::Capella(state) => ( + &mut state.validators, + &mut state.balances, + &mut state.progressive_balances_cache, + ), } } @@ -1380,7 +1412,7 @@ impl<T: EthSpec> BeaconState<T> { } /// Build all caches (except the tree hash cache), if they need to be built. - pub fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { + pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { self.build_all_committee_caches(spec)?; self.update_pubkey_cache()?; self.build_exit_cache(spec)?; @@ -1412,6 +1444,7 @@ impl<T: EthSpec> BeaconState<T> { self.drop_committee_cache(RelativeEpoch::Next)?; self.drop_pubkey_cache(); self.drop_tree_hash_cache(); + self.drop_progressive_balances_cache(); *self.exit_cache_mut() = ExitCache::default(); Ok(()) } @@ -1608,6 +1641,11 @@ impl<T: EthSpec> BeaconState<T> { *self.pubkey_cache_mut() = PubkeyCache::default() } + /// Completely drops the `progressive_balances_cache` cache, replacing it with a new, empty cache. + fn drop_progressive_balances_cache(&mut self) { + *self.progressive_balances_cache_mut() = ProgressiveBalancesCache::default(); + } + /// Initialize but don't fill the tree hash cache, if it isn't already initialized. pub fn initialize_tree_hash_cache(&mut self) { if !self.tree_hash_cache().is_initialized() { @@ -1679,6 +1717,9 @@ impl<T: EthSpec> BeaconState<T> { if config.tree_hash_cache { *res.tree_hash_cache_mut() = self.tree_hash_cache().clone(); } + if config.progressive_balances_cache { + *res.progressive_balances_cache_mut() = self.progressive_balances_cache().clone(); + } res } @@ -1693,16 +1734,22 @@ impl<T: EthSpec> BeaconState<T> { previous_epoch: Epoch, val_index: usize, ) -> Result<bool, Error> { - self.get_validator(val_index).map(|val| { - val.is_active_at(previous_epoch) - || (val.slashed && previous_epoch + Epoch::new(1) < val.withdrawable_epoch) - }) + let val = self.get_validator(val_index)?; + Ok(val.is_active_at(previous_epoch) + || (val.slashed && previous_epoch.safe_add(Epoch::new(1))? < val.withdrawable_epoch)) } /// Passing `previous_epoch` to this function rather than computing it internally provides /// a tangible speed improvement in state processing. - pub fn is_in_inactivity_leak(&self, previous_epoch: Epoch, spec: &ChainSpec) -> bool { - (previous_epoch - self.finalized_checkpoint().epoch) > spec.min_epochs_to_inactivity_penalty + pub fn is_in_inactivity_leak( + &self, + previous_epoch: Epoch, + spec: &ChainSpec, + ) -> Result<bool, safe_arith::ArithError> { + Ok( + (previous_epoch.safe_sub(self.finalized_checkpoint().epoch)?) + > spec.min_epochs_to_inactivity_penalty, + ) } /// Get the `SyncCommittee` associated with the next slot. Useful because sync committees diff --git a/consensus/types/src/beacon_state/balance.rs b/consensus/types/src/beacon_state/balance.rs new file mode 100644 index 000000000..e537a5b98 --- /dev/null +++ b/consensus/types/src/beacon_state/balance.rs @@ -0,0 +1,33 @@ +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)] +pub struct Balance { + raw: u64, + minimum: u64, +} + +impl Balance { + /// Initialize the balance to `0`, or the given `minimum`. + pub fn zero(minimum: u64) -> Self { + Self { raw: 0, minimum } + } + + /// Returns the balance with respect to the initialization `minimum`. + pub fn get(&self) -> u64 { + std::cmp::max(self.raw, self.minimum) + } + + /// Add-assign to the balance. + pub fn safe_add_assign(&mut self, other: u64) -> Result<(), ArithError> { + self.raw.safe_add_assign(other) + } + + /// Sub-assign to the balance. + pub fn safe_sub_assign(&mut self, other: u64) -> Result<(), ArithError> { + self.raw.safe_sub_assign(other) + } +} diff --git a/consensus/types/src/beacon_state/clone_config.rs b/consensus/types/src/beacon_state/clone_config.rs index e5f050aee..c6e7f4742 100644 --- a/consensus/types/src/beacon_state/clone_config.rs +++ b/consensus/types/src/beacon_state/clone_config.rs @@ -5,6 +5,7 @@ pub struct CloneConfig { pub pubkey_cache: bool, pub exit_cache: bool, pub tree_hash_cache: bool, + pub progressive_balances_cache: bool, } impl CloneConfig { @@ -14,6 +15,7 @@ impl CloneConfig { pubkey_cache: true, exit_cache: true, tree_hash_cache: true, + progressive_balances_cache: true, } } diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index 8afef1183..64bf686f3 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -1,4 +1,4 @@ -#![allow(clippy::integer_arithmetic)] +#![allow(clippy::arithmetic_side_effects)] use super::BeaconState; use crate::*; @@ -56,6 +56,11 @@ impl CommitteeCache { return Err(Error::ZeroSlotsPerEpoch); } + // The use of `NonZeroUsize` reduces the maximum number of possible validators by one. + if state.validators().len() == usize::max_value() { + return Err(Error::TooManyValidators); + } + let active_validator_indices = get_active_validator_indices(state.validators(), epoch); if active_validator_indices.is_empty() { @@ -75,11 +80,6 @@ impl CommitteeCache { ) .ok_or(Error::UnableToShuffle)?; - // The use of `NonZeroUsize` reduces the maximum number of possible validators by one. - if state.validators().len() == usize::max_value() { - return Err(Error::TooManyValidators); - } - let mut shuffling_positions = vec![<_>::default(); state.validators().len()]; for (i, &v) in shuffling.iter().enumerate() { *shuffling_positions @@ -174,7 +174,7 @@ impl CommitteeCache { .ok_or(Error::CommitteeCacheUninitialized(None))?; initialized_epoch.slot_iter(self.slots_per_epoch).try_fold( - Vec::with_capacity(self.slots_per_epoch as usize), + Vec::with_capacity(self.epoch_committee_count()), |mut vec, slot| { vec.append(&mut self.get_beacon_committees_at_slot(slot)?); Ok(vec) diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs new file mode 100644 index 000000000..9f5c223d5 --- /dev/null +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -0,0 +1,184 @@ +use crate::beacon_state::balance::Balance; +use crate::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec}; +use arbitrary::Arbitrary; +use safe_arith::SafeArith; +use serde_derive::{Deserialize, Serialize}; +use strum::{Display, EnumString, EnumVariantNames}; + +/// 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)] +pub struct ProgressiveBalancesCache { + inner: Option<Inner>, +} + +#[derive(Debug, PartialEq, Arbitrary, Clone)] +struct Inner { + pub current_epoch: Epoch, + pub previous_epoch_target_attesting_balance: Balance, + pub current_epoch_target_attesting_balance: Balance, +} + +impl ProgressiveBalancesCache { + pub fn initialize( + &mut self, + current_epoch: Epoch, + previous_epoch_target_attesting_balance: Balance, + current_epoch_target_attesting_balance: Balance, + ) { + self.inner = Some(Inner { + current_epoch, + previous_epoch_target_attesting_balance, + current_epoch_target_attesting_balance, + }); + } + + pub fn is_initialized(&self) -> bool { + self.inner.is_some() + } + + /// When a new target attestation has been processed, we update the cached + /// `current_epoch_target_attesting_balance` to include the validator effective balance. + /// If the epoch is neither the current epoch nor the previous epoch, an error is returned. + pub fn on_new_target_attestation( + &mut self, + epoch: Epoch, + validator_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + + if epoch == cache.current_epoch { + cache + .current_epoch_target_attesting_balance + .safe_add_assign(validator_effective_balance)?; + } else if epoch.safe_add(1)? == cache.current_epoch { + cache + .previous_epoch_target_attesting_balance + .safe_add_assign(validator_effective_balance)?; + } else { + return Err(BeaconStateError::ProgressiveBalancesCacheInconsistent); + } + + Ok(()) + } + + /// When a validator is slashed, we reduce the `current_epoch_target_attesting_balance` by the + /// validator's effective balance to exclude the validator weight. + pub fn on_slashing( + &mut self, + is_previous_epoch_target_attester: bool, + is_current_epoch_target_attester: bool, + effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + if is_previous_epoch_target_attester { + cache + .previous_epoch_target_attesting_balance + .safe_sub_assign(effective_balance)?; + } + if is_current_epoch_target_attester { + cache + .current_epoch_target_attesting_balance + .safe_sub_assign(effective_balance)?; + } + Ok(()) + } + + /// When a current epoch target attester has its effective balance changed, we adjust the + /// its share of the target attesting balance in the cache. + pub fn on_effective_balance_change( + &mut self, + is_current_epoch_target_attester: bool, + old_effective_balance: u64, + new_effective_balance: u64, + ) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + if is_current_epoch_target_attester { + if new_effective_balance > old_effective_balance { + cache + .current_epoch_target_attesting_balance + .safe_add_assign(new_effective_balance.safe_sub(old_effective_balance)?)?; + } else { + cache + .current_epoch_target_attesting_balance + .safe_sub_assign(old_effective_balance.safe_sub(new_effective_balance)?)?; + } + } + Ok(()) + } + + /// On epoch transition, the balance from current epoch is shifted to previous epoch, and the + /// current epoch balance is reset to 0. + pub fn on_epoch_transition(&mut self, spec: &ChainSpec) -> Result<(), BeaconStateError> { + let cache = self.get_inner_mut()?; + cache.current_epoch.safe_add_assign(1)?; + cache.previous_epoch_target_attesting_balance = + cache.current_epoch_target_attesting_balance; + cache.current_epoch_target_attesting_balance = + Balance::zero(spec.effective_balance_increment); + Ok(()) + } + + pub fn previous_epoch_target_attesting_balance(&self) -> Result<u64, BeaconStateError> { + Ok(self + .get_inner()? + .previous_epoch_target_attesting_balance + .get()) + } + + pub fn current_epoch_target_attesting_balance(&self) -> Result<u64, BeaconStateError> { + Ok(self + .get_inner()? + .current_epoch_target_attesting_balance + .get()) + } + + fn get_inner_mut(&mut self) -> Result<&mut Inner, BeaconStateError> { + self.inner + .as_mut() + .ok_or(BeaconStateError::ProgressiveBalancesCacheNotInitialized) + } + + fn get_inner(&self) -> Result<&Inner, BeaconStateError> { + self.inner + .as_ref() + .ok_or(BeaconStateError::ProgressiveBalancesCacheNotInitialized) + } +} + +#[derive( + Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize, Display, EnumString, EnumVariantNames, +)] +#[strum(serialize_all = "lowercase")] +pub enum ProgressiveBalancesMode { + /// Disable the usage of progressive cache, and use the existing `ParticipationCache` calculation. + Disabled, + /// Enable the usage of progressive cache, with checks against the `ParticipationCache` and falls + /// back to the existing calculation if there is a balance mismatch. + Checked, + /// Enable the usage of progressive cache, with checks against the `ParticipationCache`. Errors + /// if there is a balance mismatch. Used in testing only. + Strict, + /// Enable the usage of progressive cache, with no comparative checks against the + /// `ParticipationCache`. This is fast but an experimental mode, use with caution. + Fast, +} + +impl ProgressiveBalancesMode { + pub fn perform_comparative_checks(&self) -> bool { + match self { + Self::Disabled | Self::Fast => false, + Self::Checked | Self::Strict => true, + } + } +} + +/// `ProgressiveBalancesCache` is only enabled from `Altair` as it requires `ParticipationCache`. +pub fn is_progressive_balances_enabled<E: EthSpec>(state: &BeaconState<E>) -> bool { + match state { + BeaconState::Base(_) => false, + BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => true, + } +} diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index d63eaafc4..6cd9c1dbf 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -219,17 +219,18 @@ async fn clone_config() { let mut state = build_state::<MinimalEthSpec>(16).await; - state.build_all_caches(&spec).unwrap(); + state.build_caches(&spec).unwrap(); state .update_tree_hash_cache() .expect("should update tree hash cache"); - let num_caches = 4; + let num_caches = 5; let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { committee_caches: (i & 1) != 0, pubkey_cache: ((i >> 1) & 1) != 0, exit_cache: ((i >> 2) & 1) != 0, tree_hash_cache: ((i >> 3) & 1) != 0, + progressive_balances_cache: ((i >> 4) & 1) != 0, }); for config in all_configs { diff --git a/consensus/types/src/beacon_state/tree_hash_cache.rs b/consensus/types/src/beacon_state/tree_hash_cache.rs index d1d63e3c8..69cd6fbb8 100644 --- a/consensus/types/src/beacon_state/tree_hash_cache.rs +++ b/consensus/types/src/beacon_state/tree_hash_cache.rs @@ -1,4 +1,4 @@ -#![allow(clippy::integer_arithmetic)] +#![allow(clippy::arithmetic_side_effects)] #![allow(clippy::disallowed_methods)] #![allow(clippy::indexing_slicing)] diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 2b25cc1d5..a13d3116d 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -6,6 +6,7 @@ use serde_derive::Deserialize; use serde_utils::quoted_u64::MaybeQuoted; use std::fs::File; use std::path::Path; +use std::time::Duration; use tree_hash::TreeHash; /// Each of the BLS signature domains. @@ -168,11 +169,17 @@ pub struct ChainSpec { pub maximum_gossip_clock_disparity_millis: u64, pub target_aggregators_per_committee: u64, pub attestation_subnet_count: u64, - pub random_subnets_per_validator: u64, - pub epochs_per_random_subnet_subscription: u64, pub subnets_per_node: u8, pub epochs_per_subnet_subscription: u64, - attestation_subnet_extra_bits: u8, + pub gossip_max_size: u64, + pub min_epochs_for_block_requests: u64, + pub max_chunk_size: u64, + pub ttfb_timeout: u64, + pub resp_timeout: u64, + pub message_domain_invalid_snappy: [u8; 4], + pub message_domain_valid_snappy: [u8; 4], + pub attestation_subnet_extra_bits: u8, + pub attestation_subnet_prefix_bits: u8, /* * Application params @@ -453,20 +460,16 @@ impl ChainSpec { Hash256::from(domain) } - #[allow(clippy::integer_arithmetic)] - pub const fn attestation_subnet_prefix_bits(&self) -> u32 { - // maybe use log2 when stable https://github.com/rust-lang/rust/issues/70887 + pub fn maximum_gossip_clock_disparity(&self) -> Duration { + Duration::from_millis(self.maximum_gossip_clock_disparity_millis) + } - // NOTE: this line is here simply to guarantee that if self.attestation_subnet_count type - // is changed, a compiler warning will be raised. This code depends on the type being u64. - let attestation_subnet_count: u64 = self.attestation_subnet_count; - let attestation_subnet_count_bits = if attestation_subnet_count == 0 { - 0 - } else { - 63 - attestation_subnet_count.leading_zeros() - }; + pub fn ttfb_timeout(&self) -> Duration { + Duration::from_secs(self.ttfb_timeout) + } - self.attestation_subnet_extra_bits as u32 + attestation_subnet_count_bits + pub fn resp_timeout(&self) -> Duration { + Duration::from_secs(self.resp_timeout) } /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. @@ -625,14 +628,19 @@ impl ChainSpec { network_id: 1, // mainnet network id attestation_propagation_slot_range: 32, attestation_subnet_count: 64, - random_subnets_per_validator: 1, - subnets_per_node: 1, + subnets_per_node: 2, maximum_gossip_clock_disparity_millis: 500, target_aggregators_per_committee: 16, - epochs_per_random_subnet_subscription: 256, epochs_per_subnet_subscription: 256, - attestation_subnet_extra_bits: 6, - + gossip_max_size: default_gossip_max_size(), + min_epochs_for_block_requests: default_min_epochs_for_block_requests(), + max_chunk_size: default_max_chunk_size(), + ttfb_timeout: default_ttfb_timeout(), + resp_timeout: default_resp_timeout(), + message_domain_invalid_snappy: default_message_domain_invalid_snappy(), + message_domain_valid_snappy: default_message_domain_valid_snappy(), + attestation_subnet_extra_bits: default_attestation_subnet_extra_bits(), + attestation_subnet_prefix_bits: default_attestation_subnet_prefix_bits(), /* * Application specific */ @@ -842,7 +850,7 @@ impl ChainSpec { * Capella hard fork params */ capella_fork_version: [0x03, 0x00, 0x00, 0x64], - capella_fork_epoch: None, + capella_fork_epoch: Some(Epoch::new(648704)), max_validators_per_withdrawals_sweep: 8192, /* @@ -852,13 +860,19 @@ impl ChainSpec { network_id: 100, // Gnosis Chain network id attestation_propagation_slot_range: 32, attestation_subnet_count: 64, - random_subnets_per_validator: 1, - subnets_per_node: 1, + subnets_per_node: 4, // Make this larger than usual to avoid network damage maximum_gossip_clock_disparity_millis: 500, target_aggregators_per_committee: 16, - epochs_per_random_subnet_subscription: 256, epochs_per_subnet_subscription: 256, - attestation_subnet_extra_bits: 6, + gossip_max_size: default_gossip_max_size(), + min_epochs_for_block_requests: default_min_epochs_for_block_requests(), + max_chunk_size: default_max_chunk_size(), + ttfb_timeout: default_ttfb_timeout(), + resp_timeout: default_resp_timeout(), + message_domain_invalid_snappy: default_message_domain_invalid_snappy(), + message_domain_valid_snappy: default_message_domain_valid_snappy(), + attestation_subnet_extra_bits: default_attestation_subnet_extra_bits(), + attestation_subnet_prefix_bits: default_attestation_subnet_prefix_bits(), /* * Application specific @@ -946,6 +960,9 @@ pub struct Config { shard_committee_period: u64, #[serde(with = "serde_utils::quoted_u64")] eth1_follow_distance: u64, + #[serde(default = "default_subnets_per_node")] + #[serde(with = "serde_utils::quoted_u8")] + subnets_per_node: u8, #[serde(with = "serde_utils::quoted_u64")] inactivity_score_bias: u64, @@ -966,6 +983,34 @@ pub struct Config { #[serde(with = "serde_utils::quoted_u64")] deposit_network_id: u64, deposit_contract_address: Address, + + #[serde(default = "default_gossip_max_size")] + #[serde(with = "serde_utils::quoted_u64")] + gossip_max_size: u64, + #[serde(default = "default_min_epochs_for_block_requests")] + #[serde(with = "serde_utils::quoted_u64")] + min_epochs_for_block_requests: u64, + #[serde(default = "default_max_chunk_size")] + #[serde(with = "serde_utils::quoted_u64")] + max_chunk_size: u64, + #[serde(default = "default_ttfb_timeout")] + #[serde(with = "serde_utils::quoted_u64")] + ttfb_timeout: u64, + #[serde(default = "default_resp_timeout")] + #[serde(with = "serde_utils::quoted_u64")] + resp_timeout: u64, + #[serde(default = "default_message_domain_invalid_snappy")] + #[serde(with = "serde_utils::bytes_4_hex")] + message_domain_invalid_snappy: [u8; 4], + #[serde(default = "default_message_domain_valid_snappy")] + #[serde(with = "serde_utils::bytes_4_hex")] + message_domain_valid_snappy: [u8; 4], + #[serde(default = "default_attestation_subnet_extra_bits")] + #[serde(with = "serde_utils::quoted_u8")] + attestation_subnet_extra_bits: u8, + #[serde(default = "default_attestation_subnet_prefix_bits")] + #[serde(with = "serde_utils::quoted_u8")] + attestation_subnet_prefix_bits: u8, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1002,6 +1047,46 @@ fn default_safe_slots_to_import_optimistically() -> u64 { 128u64 } +fn default_subnets_per_node() -> u8 { + 2u8 +} + +const fn default_gossip_max_size() -> u64 { + 10485760 +} + +const fn default_min_epochs_for_block_requests() -> u64 { + 33024 +} + +const fn default_max_chunk_size() -> u64 { + 10485760 +} + +const fn default_ttfb_timeout() -> u64 { + 5 +} + +const fn default_resp_timeout() -> u64 { + 10 +} + +const fn default_message_domain_invalid_snappy() -> [u8; 4] { + [0, 0, 0, 0] +} + +const fn default_message_domain_valid_snappy() -> [u8; 4] { + [1, 0, 0, 0] +} + +const fn default_attestation_subnet_extra_bits() -> u8 { + 0 +} + +const fn default_attestation_subnet_prefix_bits() -> u8 { + 6 +} + impl Default for Config { fn default() -> Self { let chain_spec = MainnetEthSpec::default_spec(); @@ -1084,6 +1169,7 @@ impl Config { min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, shard_committee_period: spec.shard_committee_period, eth1_follow_distance: spec.eth1_follow_distance, + subnets_per_node: spec.subnets_per_node, inactivity_score_bias: spec.inactivity_score_bias, inactivity_score_recovery_rate: spec.inactivity_score_recovery_rate, @@ -1096,6 +1182,16 @@ impl Config { deposit_chain_id: spec.deposit_chain_id, deposit_network_id: spec.deposit_network_id, deposit_contract_address: spec.deposit_contract_address, + + gossip_max_size: spec.gossip_max_size, + min_epochs_for_block_requests: spec.min_epochs_for_block_requests, + max_chunk_size: spec.max_chunk_size, + ttfb_timeout: spec.ttfb_timeout, + resp_timeout: spec.resp_timeout, + message_domain_invalid_snappy: spec.message_domain_invalid_snappy, + message_domain_valid_snappy: spec.message_domain_valid_snappy, + attestation_subnet_extra_bits: spec.attestation_subnet_extra_bits, + attestation_subnet_prefix_bits: spec.attestation_subnet_prefix_bits, } } @@ -1130,6 +1226,7 @@ impl Config { min_validator_withdrawability_delay, shard_committee_period, eth1_follow_distance, + subnets_per_node, inactivity_score_bias, inactivity_score_recovery_rate, ejection_balance, @@ -1139,6 +1236,15 @@ impl Config { deposit_chain_id, deposit_network_id, deposit_contract_address, + gossip_max_size, + min_epochs_for_block_requests, + max_chunk_size, + ttfb_timeout, + resp_timeout, + message_domain_invalid_snappy, + message_domain_valid_snappy, + attestation_subnet_extra_bits, + attestation_subnet_prefix_bits, } = self; if preset_base != T::spec_name().to_string().as_str() { @@ -1162,6 +1268,7 @@ impl Config { min_validator_withdrawability_delay, shard_committee_period, eth1_follow_distance, + subnets_per_node, inactivity_score_bias, inactivity_score_recovery_rate, ejection_balance, @@ -1175,6 +1282,15 @@ impl Config { terminal_block_hash, terminal_block_hash_activation_epoch, safe_slots_to_import_optimistically, + gossip_max_size, + min_epochs_for_block_requests, + max_chunk_size, + ttfb_timeout, + resp_timeout, + message_domain_invalid_snappy, + message_domain_valid_snappy, + attestation_subnet_extra_bits, + attestation_subnet_prefix_bits, ..chain_spec.clone() }) } @@ -1312,6 +1428,7 @@ mod tests { #[cfg(test)] mod yaml_tests { use super::*; + use paste::paste; use tempfile::NamedTempFile; #[test] @@ -1416,29 +1533,35 @@ mod yaml_tests { "#; let chain_spec: Config = serde_yaml::from_str(spec).unwrap(); - assert_eq!( - chain_spec.terminal_total_difficulty, - default_terminal_total_difficulty() - ); - assert_eq!( - chain_spec.terminal_block_hash, - default_terminal_block_hash() - ); - assert_eq!( - chain_spec.terminal_block_hash_activation_epoch, - default_terminal_block_hash_activation_epoch() - ); - assert_eq!( - chain_spec.safe_slots_to_import_optimistically, - default_safe_slots_to_import_optimistically() - ); + + // Asserts that `chain_spec.$name` and `default_$name()` are equal. + macro_rules! check_default { + ($name: ident) => { + paste! { + assert_eq!( + chain_spec.$name, + [<default_ $name>](), + "{} does not match default", stringify!($name)); + } + }; + } + + check_default!(terminal_total_difficulty); + check_default!(terminal_block_hash); + check_default!(terminal_block_hash_activation_epoch); + check_default!(safe_slots_to_import_optimistically); + check_default!(bellatrix_fork_version); + check_default!(gossip_max_size); + check_default!(min_epochs_for_block_requests); + check_default!(max_chunk_size); + check_default!(ttfb_timeout); + check_default!(resp_timeout); + check_default!(message_domain_invalid_snappy); + check_default!(message_domain_valid_snappy); + check_default!(attestation_subnet_extra_bits); + check_default!(attestation_subnet_prefix_bits); assert_eq!(chain_spec.bellatrix_fork_epoch, None); - - assert_eq!( - chain_spec.bellatrix_fork_version, - default_bellatrix_fork_version() - ); } #[test] diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index b10ad7557..01f86d348 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -86,10 +86,6 @@ pub fn get_extra_fields(spec: &ChainSpec) -> HashMap<String, Value> { "domain_application_mask".to_uppercase()=> u32_hex(spec.domain_application_mask), "target_aggregators_per_committee".to_uppercase() => spec.target_aggregators_per_committee.to_string().into(), - "random_subnets_per_validator".to_uppercase() => - spec.random_subnets_per_validator.to_string().into(), - "epochs_per_random_subnet_subscription".to_uppercase() => - spec.epochs_per_random_subnet_subscription.to_string().into(), "domain_contribution_and_proof".to_uppercase() => u32_hex(spec.domain_contribution_and_proof), "domain_sync_committee".to_uppercase() => u32_hex(spec.domain_sync_committee), diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit_tree_snapshot.rs index aea4677f2..12e81d002 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit_tree_snapshot.rs @@ -30,8 +30,10 @@ impl From<&DepositTreeSnapshot> for FinalizedExecutionBlock { pub struct DepositTreeSnapshot { pub finalized: Vec<Hash256>, pub deposit_root: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub deposit_count: u64, pub execution_block_hash: Hash256, + #[serde(with = "serde_utils::quoted_u64")] pub execution_block_height: u64, } diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 77ef6407e..690138da6 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -106,7 +106,7 @@ impl<T: EthSpec> ExecutionPayload<T> { } } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_merge_size() -> usize { // Fixed part @@ -117,7 +117,7 @@ impl<T: EthSpec> ExecutionPayload<T> { + (T::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + T::max_bytes_per_transaction())) } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_capella_size() -> usize { // Fixed part diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index 1fb29db9d..77bea03db 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -28,7 +28,10 @@ use BeaconStateError; serde(bound = "T: EthSpec", deny_unknown_fields), arbitrary(bound = "T: EthSpec") ), - ref_attributes(derive(PartialEq, TreeHash), tree_hash(enum_behaviour = "transparent")), + 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") )] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index aefb45490..85ce35176 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr( not(test), deny( - clippy::integer_arithmetic, + clippy::arithmetic_side_effects, clippy::disallowed_methods, clippy::indexing_slicing ) @@ -71,6 +71,7 @@ pub mod sync_duty; pub mod validator; pub mod validator_subscription; pub mod voluntary_exit; +pub mod withdrawal_credentials; #[macro_use] pub mod slot_epoch_macros; pub mod config_and_preset; @@ -189,6 +190,7 @@ 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 type CommitteeIndex = u64; pub type Hash256 = H256; diff --git a/consensus/types/src/participation_list.rs b/consensus/types/src/participation_list.rs index 89a56cb87..be119fbef 100644 --- a/consensus/types/src/participation_list.rs +++ b/consensus/types/src/participation_list.rs @@ -1,4 +1,4 @@ -#![allow(clippy::integer_arithmetic)] +#![allow(clippy::arithmetic_side_effects)] use crate::{Hash256, ParticipationFlags, Unsigned, VariableList}; use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, Error, TreeHashCache}; diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index b885f89f7..415d6a140 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -72,7 +72,7 @@ impl SubnetId { .into()) } - #[allow(clippy::integer_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] /// Computes the set of subnets the node should be subscribed to during the current epoch, /// along with the first epoch in which these subscriptions are no longer valid. pub fn compute_subnets_for_epoch<T: EthSpec>( @@ -80,15 +80,26 @@ impl SubnetId { epoch: Epoch, spec: &ChainSpec, ) -> Result<(impl Iterator<Item = SubnetId>, Epoch), &'static str> { - let node_id_prefix = - (node_id >> (256 - spec.attestation_subnet_prefix_bits() as usize)).as_usize(); + // Simplify the variable name + let subscription_duration = spec.epochs_per_subnet_subscription; - let subscription_event_idx = epoch.as_u64() / spec.epochs_per_subnet_subscription; + let node_id_prefix = + (node_id >> (256 - spec.attestation_subnet_prefix_bits as usize)).as_usize(); + + // NOTE: The as_u64() panics if the number is larger than u64::max_value(). This cannot be + // true as spec.epochs_per_subnet_subscription is a u64. + let node_offset = (node_id % ethereum_types::U256::from(subscription_duration)).as_u64(); + + // Calculate at which epoch this node needs to re-evaluate + let valid_until_epoch = epoch.as_u64() + + subscription_duration + .saturating_sub((epoch.as_u64() + node_offset) % subscription_duration); + + let subscription_event_idx = (epoch.as_u64() + node_offset) / subscription_duration; let permutation_seed = ethereum_hashing::hash(&int_to_bytes::int_to_bytes8(subscription_event_idx)); - let num_subnets = 1 << spec.attestation_subnet_prefix_bits(); - + let num_subnets = 1 << spec.attestation_subnet_prefix_bits; let permutated_prefix = compute_shuffled_index( node_id_prefix, num_subnets, @@ -107,7 +118,6 @@ impl SubnetId { let subnet_set_generator = (0..subnets_per_node).map(move |idx| { SubnetId::new((permutated_prefix + idx as u64) % attestation_subnet_count) }); - let valid_until_epoch = (subscription_event_idx + 1) * spec.epochs_per_subnet_subscription; Ok((subnet_set_generator, valid_until_epoch.into())) } } @@ -149,3 +159,80 @@ impl AsRef<str> for SubnetId { subnet_id_to_string(self.0) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A set of tests compared to the python specification + #[test] + fn compute_subnets_for_epoch_unit_test() { + // Randomized variables used generated with the python specification + let node_ids = [ + "0", + "88752428858350697756262172400162263450541348766581994718383409852729519486397", + "18732750322395381632951253735273868184515463718109267674920115648614659369468", + "27726842142488109545414954493849224833670205008410190955613662332153332462900", + "39755236029158558527862903296867805548949739810920318269566095185775868999998", + "31899136003441886988955119620035330314647133604576220223892254902004850516297", + "58579998103852084482416614330746509727562027284701078483890722833654510444626", + "28248042035542126088870192155378394518950310811868093527036637864276176517397", + "60930578857433095740782970114409273483106482059893286066493409689627770333527", + "103822458477361691467064888613019442068586830412598673713899771287914656699997", + ] + .into_iter() + .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) + .collect::<Vec<_>>(); + + let epochs = [ + 54321u64, 1017090249, 1827566880, 846255942, 766597383, 1204990115, 1616209495, + 1774367616, 1484598751, 3525502229, + ] + .into_iter() + .map(Epoch::from) + .collect::<Vec<_>>(); + + // Test mainnet + let spec = ChainSpec::mainnet(); + + // Calculated by hand + let expected_valid_time: Vec<u64> = [ + 54528, 1017090371, 1827567108, 846256076, 766597570, 1204990135, 1616209582, + 1774367723, 1484598953, 3525502371, + ] + .into(); + + // Calculated from pyspec + let expected_subnets = vec![ + vec![4u64, 5u64], + vec![61, 62], + vec![23, 24], + vec![38, 39], + vec![53, 54], + vec![39, 40], + vec![48, 49], + vec![39, 40], + vec![34, 35], + vec![37, 38], + ]; + + for x in 0..node_ids.len() { + println!("Test: {}", x); + println!( + "NodeId: {}\n Epoch: {}\n, expected_update_time: {}\n, expected_subnets: {:?}", + node_ids[x], epochs[x], expected_valid_time[x], expected_subnets[x] + ); + + let (computed_subnets, valid_time) = SubnetId::compute_subnets_for_epoch::< + crate::MainnetEthSpec, + >(node_ids[x], epochs[x], &spec) + .unwrap(); + + assert_eq!(Epoch::from(expected_valid_time[x]), valid_time); + assert_eq!( + expected_subnets[x], + computed_subnets.map(SubnetId::into).collect::<Vec<u64>>() + ); + } + } +} diff --git a/consensus/types/src/test_utils/mod.rs b/consensus/types/src/test_utils/mod.rs index c0333bcfd..d172342ee 100644 --- a/consensus/types/src/test_utils/mod.rs +++ b/consensus/types/src/test_utils/mod.rs @@ -1,4 +1,4 @@ -#![allow(clippy::integer_arithmetic)] +#![allow(clippy::arithmetic_side_effects)] use std::fmt::Debug; diff --git a/consensus/types/src/test_utils/test_random.rs b/consensus/types/src/test_utils/test_random.rs index 43396dedc..51b79d8d5 100644 --- a/consensus/types/src/test_utils/test_random.rs +++ b/consensus/types/src/test_utils/test_random.rs @@ -28,7 +28,7 @@ pub trait TestRandom { impl<T> TestRandom for PhantomData<T> { fn random_for_test(_rng: &mut impl RngCore) -> Self { - PhantomData::default() + PhantomData } } diff --git a/consensus/types/src/withdrawal_credentials.rs b/consensus/types/src/withdrawal_credentials.rs new file mode 100644 index 000000000..8d42d4eaf --- /dev/null +++ b/consensus/types/src/withdrawal_credentials.rs @@ -0,0 +1,57 @@ +use crate::*; +use bls::get_withdrawal_credentials; + +pub struct WithdrawalCredentials(Hash256); + +impl WithdrawalCredentials { + pub fn bls(withdrawal_public_key: &PublicKey, spec: &ChainSpec) -> Self { + let withdrawal_credentials = + get_withdrawal_credentials(withdrawal_public_key, spec.bls_withdrawal_prefix_byte); + Self(Hash256::from_slice(&withdrawal_credentials)) + } + + pub fn eth1(withdrawal_address: Address, spec: &ChainSpec) -> Self { + let mut withdrawal_credentials = [0; 32]; + withdrawal_credentials[0] = spec.eth1_address_withdrawal_prefix_byte; + withdrawal_credentials[12..].copy_from_slice(withdrawal_address.as_bytes()); + Self(Hash256::from_slice(&withdrawal_credentials)) + } +} + +impl From<WithdrawalCredentials> for Hash256 { + fn from(withdrawal_credentials: WithdrawalCredentials) -> Self { + withdrawal_credentials.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::generate_deterministic_keypair; + use std::str::FromStr; + + #[test] + fn bls_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let keypair = generate_deterministic_keypair(0); + let credentials = WithdrawalCredentials::bls(&keypair.pk, spec); + let manually_generated_credentials = + get_withdrawal_credentials(&keypair.pk, spec.bls_withdrawal_prefix_byte); + let hash: Hash256 = credentials.into(); + assert_eq!(hash[0], spec.bls_withdrawal_prefix_byte); + assert_eq!(hash.as_bytes(), &manually_generated_credentials); + } + + #[test] + fn eth1_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let address = Address::from_str("0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b").unwrap(); + let credentials = WithdrawalCredentials::eth1(address, spec); + let hash: Hash256 = credentials.into(); + assert_eq!( + hash, + Hash256::from_str("0x01000000000000000000000025c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b") + .unwrap() + ) + } +} diff --git a/crypto/bls/Cargo.toml b/crypto/bls/Cargo.toml index a610f257c..4340fb3e8 100644 --- a/crypto/bls/Cargo.toml +++ b/crypto/bls/Cargo.toml @@ -2,24 +2,25 @@ name = "bls" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -ethereum_ssz = "0.5.0" -tree_hash = "0.5.0" -milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v1.4.2", optional = true } -rand = "0.7.3" -serde = "1.0.116" +ethereum_ssz = { workspace = true } +tree_hash = { workspace = true } +milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v1.5.1", optional = true } +rand = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -ethereum_serde_utils = "0.5.0" -hex = "0.4.2" -ethereum_hashing = "1.0.0-beta.2" -ethereum-types = "0.14.1" -arbitrary = { version = "1.0", features = ["derive"], optional = true } -zeroize = { version = "1.4.2", features = ["zeroize_derive"] } +ethereum_serde_utils = { workspace = true } +hex = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum-types = { workspace = true } +arbitrary = { workspace = true } +zeroize = { workspace = true } blst = { version = "0.3.3", optional = true } [features] +arbitrary = [] default = ["supranational"] fake_crypto = [] milagro = ["milagro_bls"] diff --git a/crypto/bls/src/generic_public_key_bytes.rs b/crypto/bls/src/generic_public_key_bytes.rs index 59b0ffc43..240568b4f 100644 --- a/crypto/bls/src/generic_public_key_bytes.rs +++ b/crypto/bls/src/generic_public_key_bytes.rs @@ -27,10 +27,7 @@ impl<Pub> Copy for GenericPublicKeyBytes<Pub> {} impl<Pub> Clone for GenericPublicKeyBytes<Pub> { fn clone(&self) -> Self { - Self { - bytes: self.bytes, - _phantom: PhantomData, - } + *self } } diff --git a/crypto/eth2_key_derivation/Cargo.toml b/crypto/eth2_key_derivation/Cargo.toml index 3f174a02d..a0237ba7e 100644 --- a/crypto/eth2_key_derivation/Cargo.toml +++ b/crypto/eth2_key_derivation/Cargo.toml @@ -2,16 +2,16 @@ name = "eth2_key_derivation" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sha2 = "0.10" -zeroize = { version = "1.4.2", features = ["zeroize_derive"] } -num-bigint-dig = { version = "0.6.0", features = ["zeroize"] } -ring = "0.16.19" -bls = { path = "../bls" } +sha2 = { workspace = true } +zeroize = { workspace = true } +num-bigint-dig = { version = "0.8.4", features = ["zeroize"] } +ring = { workspace = true } +bls = { workspace = true } [dev-dependencies] -hex = "0.4.2" +hex = { workspace = true } diff --git a/crypto/eth2_keystore/Cargo.toml b/crypto/eth2_keystore/Cargo.toml index d83a60da2..bb6222807 100644 --- a/crypto/eth2_keystore/Cargo.toml +++ b/crypto/eth2_keystore/Cargo.toml @@ -2,26 +2,26 @@ name = "eth2_keystore" version = "0.1.0" authors = ["Pawan Dhananjay <pawan@sigmaprime.io", "Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = "0.8.5" +rand = { workspace = true } hmac = "0.11.0" pbkdf2 = { version = "0.8.0", default-features = false } scrypt = { version = "0.7.0", default-features = false } -sha2 = "0.9.2" -uuid = { version = "0.8.1", features = ["serde", "v4"] } -zeroize = { version = "1.4.2", features = ["zeroize_derive"] } -serde = "1.0.116" -serde_repr = "0.1.6" -hex = "0.4.2" -bls = { path = "../bls" } -serde_json = "1.0.58" -eth2_key_derivation = { path = "../eth2_key_derivation" } +sha2 = { workspace = true } +uuid = { workspace = true } +zeroize = { workspace = true } +serde = { workspace = true } +serde_repr = { workspace = true } +hex = { workspace = true } +bls = { workspace = true } +serde_json = { workspace = true } +eth2_key_derivation = { workspace = true } unicode-normalization = "0.1.16" aes = { version = "0.7", features = ["ctr"] } [dev-dependencies] -tempfile = "3.1.0" +tempfile = { workspace = true } diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index 71f66ff93..f3af6aab5 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -2,20 +2,20 @@ name = "eth2_wallet" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1.0.116" -serde_json = "1.0.58" -serde_repr = "0.1.6" -uuid = { version = "0.8.1", features = ["serde", "v4"] } -rand = "0.8.5" -eth2_keystore = { path = "../eth2_keystore" } -eth2_key_derivation = { path = "../eth2_key_derivation" } -tiny-bip39 = "0.8.1" +serde = { workspace = true } +serde_json = { workspace = true } +serde_repr = { workspace = true } +uuid = { workspace = true } +rand = { workspace = true } +eth2_keystore = { workspace = true } +eth2_key_derivation = { workspace = true } +tiny-bip39 = "1" [dev-dependencies] -hex = "0.4.2" -tempfile = "3.1.0" +hex = { workspace = true } +tempfile = { workspace = true } diff --git a/database_manager/Cargo.toml b/database_manager/Cargo.toml index f71552813..1570c171c 100644 --- a/database_manager/Cargo.toml +++ b/database_manager/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "database_manager" version = "0.1.0" -edition = "2021" +edition = { workspace = true } [dependencies] -beacon_chain = { path = "../beacon_node/beacon_chain" } -beacon_node = { path = "../beacon_node" } -clap = "2.33.3" -clap_utils = { path = "../common/clap_utils" } -environment = { path = "../lighthouse/environment" } -logging = { path = "../common/logging" } -sloggers = "2.0.2" -store = { path = "../beacon_node/store" } -tempfile = "3.1.0" -types = { path = "../consensus/types" } -slog = "2.5.2" -strum = { version = "0.24.0", features = ["derive"] } +beacon_chain = { workspace = true } +beacon_node = { workspace = true } +clap = { workspace = true } +clap_utils = { workspace = true } +environment = { workspace = true } +logging = { workspace = true } +sloggers = { workspace = true } +store = { workspace = true } +tempfile = { workspace = true } +types = { workspace = true } +slog = { workspace = true } +strum = { workspace = true } diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 33accfc05..ce0b094b7 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -6,6 +6,9 @@ use beacon_node::{get_data_dir, get_slots_per_restore_point, ClientConfig}; use clap::{App, Arg, ArgMatches}; use environment::{Environment, RuntimeContext}; use slog::{info, Logger}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; use store::{ errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, @@ -57,6 +60,13 @@ pub fn inspect_cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("sizes") .possible_values(InspectTarget::VARIANTS), ) + .arg( + Arg::with_name("output-dir") + .long("output-dir") + .value_name("DIR") + .help("Base directory for the output files. Defaults to the current directory") + .takes_value(true), + ) } pub fn prune_payloads_app<'a, 'b>() -> App<'a, 'b> { @@ -154,18 +164,27 @@ pub enum InspectTarget { ValueSizes, #[strum(serialize = "total")] ValueTotal, + #[strum(serialize = "values")] + Values, } pub struct InspectConfig { column: DBColumn, target: InspectTarget, + /// Configures where the inspect output should be stored. + output_dir: PathBuf, } fn parse_inspect_config(cli_args: &ArgMatches) -> Result<InspectConfig, String> { let column = clap_utils::parse_required(cli_args, "column")?; let target = clap_utils::parse_required(cli_args, "output")?; - - Ok(InspectConfig { column, target }) + let output_dir: PathBuf = + clap_utils::parse_optional(cli_args, "output-dir")?.unwrap_or_else(PathBuf::new); + Ok(InspectConfig { + column, + target, + output_dir, + }) } pub fn inspect_db<E: EthSpec>( @@ -173,7 +192,7 @@ pub fn inspect_db<E: EthSpec>( client_config: ClientConfig, runtime_context: &RuntimeContext<E>, log: Logger, -) -> Result<(), Error> { +) -> Result<(), String> { let spec = runtime_context.eth2_config.spec.clone(); let hot_path = client_config.get_db_path(); let cold_path = client_config.get_freezer_db_path(); @@ -185,12 +204,19 @@ pub fn inspect_db<E: EthSpec>( client_config.store, spec, log, - )?; + ) + .map_err(|e| format!("{:?}", e))?; let mut total = 0; + let base_path = &inspect_config.output_dir; + + if let InspectTarget::Values = inspect_config.target { + fs::create_dir_all(base_path) + .map_err(|e| format!("Unable to create import directory: {:?}", e))?; + } for res in db.hot_db.iter_column(inspect_config.column) { - let (key, value) = res?; + let (key, value) = res.map_err(|e| format!("{:?}", e))?; match inspect_config.target { InspectTarget::ValueSizes => { @@ -200,11 +226,32 @@ pub fn inspect_db<E: EthSpec>( InspectTarget::ValueTotal => { total += value.len(); } + InspectTarget::Values => { + let file_path = + base_path.join(format!("{}_{}.ssz", inspect_config.column.as_str(), key)); + + let write_result = fs::OpenOptions::new() + .create(true) + .write(true) + .open(&file_path) + .map_err(|e| format!("Failed to open file: {:?}", e)) + .map(|mut file| { + file.write_all(&value) + .map_err(|e| format!("Failed to write file: {:?}", e)) + }); + if let Err(e) = write_result { + println!("Error writing values to file {:?}: {:?}", file_path, e); + } else { + println!("Successfully saved values to file: {:?}", file_path); + } + + total += value.len(); + } } } match inspect_config.target { - InspectTarget::ValueSizes | InspectTarget::ValueTotal => { + InspectTarget::ValueSizes | InspectTarget::ValueTotal | InspectTarget::Values => { println!("Total: {} bytes", total); } } @@ -292,21 +339,23 @@ pub fn run<T: EthSpec>(cli_args: &ArgMatches<'_>, env: Environment<T>) -> Result let client_config = parse_client_config(cli_args, &env)?; let context = env.core_context(); let log = context.log().clone(); + let format_err = |e| format!("Fatal error: {:?}", e); match cli_args.subcommand() { - ("version", Some(_)) => display_db_version(client_config, &context, log), + ("version", Some(_)) => { + display_db_version(client_config, &context, log).map_err(format_err) + } ("migrate", Some(cli_args)) => { let migrate_config = parse_migrate_config(cli_args)?; - migrate_db(migrate_config, client_config, &context, log) + migrate_db(migrate_config, client_config, &context, log).map_err(format_err) } ("inspect", Some(cli_args)) => { let inspect_config = parse_inspect_config(cli_args)?; inspect_db(inspect_config, client_config, &context, log) } - ("prune_payloads", Some(_)) => prune_payloads(client_config, &context, log), - _ => { - return Err("Unknown subcommand, for help `lighthouse database_manager --help`".into()) + ("prune_payloads", Some(_)) => { + prune_payloads(client_config, &context, log).map_err(format_err) } + _ => Err("Unknown subcommand, for help `lighthouse database_manager --help`".into()), } - .map_err(|e| format!("Fatal error: {:?}", e)) } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index b4d1baba4..854f718c5 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "4.2.0" +version = "4.5.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [features] portable = ["bls/supranational-portable"] @@ -11,38 +11,38 @@ fake_crypto = ['bls/fake_crypto'] jemalloc = ["malloc_utils/jemalloc"] [dependencies] -bls = { path = "../crypto/bls" } -clap = "2.33.3" -log = "0.4.11" -serde = "1.0.116" -serde_yaml = "0.8.13" -serde_json = "1.0.66" -env_logger = "0.9.0" -types = { path = "../consensus/types" } -state_processing = { path = "../consensus/state_processing" } -int_to_bytes = { path = "../consensus/int_to_bytes" } -ethereum_hashing = "1.0.0-beta.2" -ethereum_ssz = "0.5.0" -environment = { path = "../lighthouse/environment" } -eth2_network_config = { path = "../common/eth2_network_config" } -genesis = { path = "../beacon_node/genesis" } -deposit_contract = { path = "../common/deposit_contract" } -tree_hash = "0.5.0" -clap_utils = { path = "../common/clap_utils" } -lighthouse_network = { path = "../beacon_node/lighthouse_network" } -validator_dir = { path = "../common/validator_dir", features = ["insecure_keys"] } -lighthouse_version = { path = "../common/lighthouse_version" } -directory = { path = "../common/directory" } -account_utils = { path = "../common/account_utils" } -eth2_wallet = { path = "../crypto/eth2_wallet" } -eth1_test_rig = { path = "../testing/eth1_test_rig" } -sensitive_url = { path = "../common/sensitive_url" } -eth2 = { path = "../common/eth2" } -snap = "1.0.1" -beacon_chain = { path = "../beacon_node/beacon_chain" } -store = { path = "../beacon_node/store" } -malloc_utils = { path = "../common/malloc_utils" } -rayon = "1.7.0" +bls = { workspace = true } +clap = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +serde_json = { workspace = true } +env_logger = { workspace = true } +types = { workspace = true } +state_processing = { workspace = true } +int_to_bytes = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum_ssz = { workspace = true } +environment = { workspace = true } +eth2_network_config = { workspace = true } +genesis = { workspace = true } +deposit_contract = { workspace = true } +tree_hash = { workspace = true } +clap_utils = { workspace = true } +lighthouse_network = { workspace = true } +validator_dir = { workspace = true, features = ["insecure_keys"] } +lighthouse_version = { workspace = true } +directory = { workspace = true } +account_utils = { workspace = true } +eth2_wallet = { workspace = true } +eth1_test_rig = { workspace = true } +sensitive_url = { workspace = true } +eth2 = { workspace = true } +snap = { workspace = true } +beacon_chain = { workspace = true } +store = { workspace = true } +malloc_utils = { workspace = true } +rayon = { workspace = true } [package.metadata.cargo-udeps.ignore] normal = ["malloc_utils"] diff --git a/lcli/Dockerfile b/lcli/Dockerfile index 485ebfd5b..cd7f45ec7 100644 --- a/lcli/Dockerfile +++ b/lcli/Dockerfile @@ -1,8 +1,8 @@ # `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.68.2-bullseye AS builder -RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev protobuf-compiler +FROM rust:1.69.0-bullseye AS builder +RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev COPY . lighthouse ARG PORTABLE ENV PORTABLE $PORTABLE @@ -10,4 +10,4 @@ RUN if [ "$PORTABLE" = true ] ; then export FEATURES=portable; fi && cd lighthou FROM ubuntu:22.04 RUN apt-get update && apt-get -y upgrade && apt-get clean && rm -rf /var/lib/apt/lists/* -COPY --from=builder /usr/local/cargo/bin/lcli /usr/local/bin/lcli +COPY --from=builder /usr/local/cargo/bin/lcli /usr/local/bin/lcli \ No newline at end of file diff --git a/lcli/src/block_root.rs b/lcli/src/block_root.rs index a47b48a30..a4237d855 100644 --- a/lcli/src/block_root.rs +++ b/lcli/src/block_root.rs @@ -31,14 +31,19 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use eth2_network_config::Eth2NetworkConfig; use std::path::PathBuf; use std::time::{Duration, Instant}; use types::{EthSpec, FullPayload, SignedBeaconBlock}; const HTTP_TIMEOUT: Duration = Duration::from_secs(5); -pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> { - let spec = &T::default_spec(); +pub fn run<T: EthSpec>( + env: Environment<T>, + network_config: Eth2NetworkConfig, + matches: &ArgMatches, +) -> Result<(), String> { + let spec = &network_config.chain_spec::<T>()?; let executor = env.core_context().executor; /* diff --git a/lcli/src/eth1_genesis.rs b/lcli/src/eth1_genesis.rs index 34144cd86..bddd4baad 100644 --- a/lcli/src/eth1_genesis.rs +++ b/lcli/src/eth1_genesis.rs @@ -49,7 +49,7 @@ pub fn run<T: EthSpec>( .wait_for_genesis_state::<T>(ETH1_GENESIS_UPDATE_INTERVAL, spec) .await .map(move |genesis_state| { - eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes()); + eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes().into()); eth2_network_config.force_write_to_file(testnet_dir) }) .map_err(|e| format!("Failed to find genesis: {}", e))?; diff --git a/lcli/src/generate_bootnode_enr.rs b/lcli/src/generate_bootnode_enr.rs index 8662a8047..0584cd654 100644 --- a/lcli/src/generate_bootnode_enr.rs +++ b/lcli/src/generate_bootnode_enr.rs @@ -1,6 +1,7 @@ use clap::ArgMatches; use lighthouse_network::{ - discovery::{build_enr, CombinedKey, CombinedKeyExt, Keypair, ENR_FILENAME}, + discovery::{build_enr, CombinedKey, CombinedKeyExt, ENR_FILENAME}, + libp2p::identity::secp256k1, NetworkConfig, NETWORK_KEY_FILENAME, }; use std::fs::File; @@ -29,8 +30,8 @@ pub fn run<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> { config.enr_udp4_port = Some(udp_port); config.enr_tcp6_port = Some(tcp_port); - let local_keypair = Keypair::generate_secp256k1(); - let enr_key = CombinedKey::from_libp2p(&local_keypair)?; + let secp256k1_keypair = secp256k1::Keypair::generate(); + let enr_key = CombinedKey::from_secp256k1(&secp256k1_keypair); let enr_fork_id = EnrForkId { fork_digest: ChainSpec::compute_fork_digest(genesis_fork_version, Hash256::zero()), next_fork_version: genesis_fork_version, @@ -47,13 +48,10 @@ pub fn run<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> { .write_all(enr.to_base64().as_bytes()) .map_err(|e| format!("Unable to write ENR to {}: {:?}", ENR_FILENAME, e))?; - let secret_bytes = match local_keypair { - Keypair::Secp256k1(key) => key.secret().to_bytes(), - _ => return Err("Key is not a secp256k1 key".into()), - }; - let mut key_file = File::create(output_dir.join(NETWORK_KEY_FILENAME)) .map_err(|e| format!("Unable to create {}: {:?}", NETWORK_KEY_FILENAME, e))?; + + let secret_bytes = secp256k1_keypair.secret().to_bytes(); key_file .write_all(&secret_bytes) .map_err(|e| format!("Unable to write key to {}: {:?}", NETWORK_KEY_FILENAME, e))?; diff --git a/lcli/src/interop_genesis.rs b/lcli/src/interop_genesis.rs index 57a5ba009..1a0b81fcb 100644 --- a/lcli/src/interop_genesis.rs +++ b/lcli/src/interop_genesis.rs @@ -42,7 +42,7 @@ pub fn run<T: EthSpec>(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), &spec, )?; - eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes()); + eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes().into()); eth2_network_config.force_write_to_file(testnet_dir)?; Ok(()) diff --git a/lcli/src/main.rs b/lcli/src/main.rs index d072beaa4..38fec2ebb 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -15,11 +15,13 @@ mod new_testnet; mod parse_ssz; mod replace_state_pubkeys; mod skip_slots; +mod state_root; mod transition_blocks; use clap::{App, Arg, ArgMatches, SubCommand}; -use clap_utils::parse_path_with_default_in_home_dir; +use clap_utils::parse_optional; use environment::{EnvironmentBuilder, LoggerConfig}; +use eth2_network_config::Eth2NetworkConfig; use parse_ssz::run_parse_ssz; use std::path::PathBuf; use std::process; @@ -38,7 +40,6 @@ fn main() { .long("spec") .value_name("STRING") .takes_value(true) - .required(true) .possible_values(&["minimal", "mainnet", "gnosis"]) .default_value("mainnet") .global(true), @@ -50,7 +51,16 @@ fn main() { .value_name("PATH") .takes_value(true) .global(true) - .help("The testnet dir. Defaults to ~/.lighthouse/testnet"), + .help("The testnet dir."), + ) + .arg( + Arg::with_name("network") + .long("network") + .value_name("NAME") + .takes_value(true) + .global(true) + .help("The network to use. Defaults to mainnet.") + .conflicts_with("testnet-dir") ) .subcommand( SubCommand::with_name("skip-slots") @@ -126,7 +136,7 @@ fn main() { .takes_value(true) .conflicts_with("beacon-url") .requires("block-path") - .help("Path to load a BeaconState from file as SSZ."), + .help("Path to load a BeaconState from as SSZ."), ) .arg( Arg::with_name("block-path") @@ -135,7 +145,7 @@ fn main() { .takes_value(true) .conflicts_with("beacon-url") .requires("pre-state-path") - .help("Path to load a SignedBeaconBlock from file as SSZ."), + .help("Path to load a SignedBeaconBlock from as SSZ."), ) .arg( Arg::with_name("post-state-output-path") @@ -361,7 +371,6 @@ fn main() { .index(2) .value_name("BIP39_MNENMONIC") .takes_value(true) - .required(true) .default_value( "replace nephew blur decorate waste convince soup column \ orient excite play baby", @@ -382,7 +391,6 @@ fn main() { .help("The block hash used when generating an execution payload. This \ value is used for `execution_payload_header.block_hash` as well as \ `execution_payload_header.random`") - .required(true) .default_value( "0x0000000000000000000000000000000000000000000000000000000000000000", ), @@ -400,7 +408,6 @@ fn main() { .value_name("INTEGER") .takes_value(true) .help("The base fee per gas field in the execution payload generated.") - .required(true) .default_value("1000000000"), ) .arg( @@ -409,7 +416,6 @@ fn main() { .value_name("INTEGER") .takes_value(true) .help("The gas limit field in the execution payload generated.") - .required(true) .default_value("30000000"), ) .arg( @@ -808,14 +814,14 @@ fn main() { ) .subcommand( SubCommand::with_name("block-root") - .about("Computes the block root of some block") + .about("Computes the block root of some block.") .arg( Arg::with_name("block-path") .long("block-path") .value_name("PATH") .takes_value(true) .conflicts_with("beacon-url") - .help("Path to load a SignedBeaconBlock from file as SSZ."), + .help("Path to load a SignedBeaconBlock from as SSZ."), ) .arg( Arg::with_name("beacon-url") @@ -841,6 +847,41 @@ fn main() { .help("Number of repeat runs, useful for benchmarking."), ) ) + .subcommand( + SubCommand::with_name("state-root") + .about("Computes the state root of some state.") + .arg( + Arg::with_name("state-path") + .long("state-path") + .value_name("PATH") + .takes_value(true) + .conflicts_with("beacon-url") + .help("Path to load a BeaconState from as SSZ."), + ) + .arg( + Arg::with_name("beacon-url") + .long("beacon-url") + .value_name("URL") + .takes_value(true) + .help("URL to a beacon-API provider."), + ) + .arg( + Arg::with_name("state-id") + .long("state-id") + .value_name("BLOCK_ID") + .takes_value(true) + .requires("beacon-url") + .help("Identifier for a state as per beacon-API standards (slot, root, etc.)"), + ) + .arg( + Arg::with_name("runs") + .long("runs") + .value_name("INTEGER") + .takes_value(true) + .default_value("1") + .help("Number of repeat runs, useful for benchmarking."), + ) + ) .get_matches(); let result = matches @@ -887,41 +928,81 @@ fn run<T: EthSpec>( .build() .map_err(|e| format!("should build env: {:?}", e))?; - let testnet_dir = parse_path_with_default_in_home_dir( - matches, - "testnet-dir", - PathBuf::from(directory::DEFAULT_ROOT_DIR).join("testnet"), - )?; + // Determine testnet-dir path or network name depending on CLI flags. + let (testnet_dir, network_name) = + if let Some(testnet_dir) = parse_optional::<PathBuf>(matches, "testnet-dir")? { + (Some(testnet_dir), None) + } else { + let network_name = + parse_optional(matches, "network")?.unwrap_or_else(|| "mainnet".to_string()); + (None, Some(network_name)) + }; + + // Lazily load either the testnet dir or the network config, as required. + // Some subcommands like new-testnet need the testnet dir but not the network config. + let get_testnet_dir = || testnet_dir.clone().ok_or("testnet-dir is required"); + let get_network_config = || { + if let Some(testnet_dir) = &testnet_dir { + Eth2NetworkConfig::load(testnet_dir.clone()).map_err(|e| { + format!( + "Unable to open testnet dir at {}: {}", + testnet_dir.display(), + e + ) + }) + } else { + let network_name = network_name.ok_or("no network name or testnet-dir provided")?; + Eth2NetworkConfig::constant(&network_name)?.ok_or("invalid network name".into()) + } + }; match matches.subcommand() { - ("transition-blocks", Some(matches)) => transition_blocks::run::<T>(env, matches) - .map_err(|e| format!("Failed to transition blocks: {}", e)), + ("transition-blocks", Some(matches)) => { + let network_config = get_network_config()?; + transition_blocks::run::<T>(env, network_config, matches) + .map_err(|e| format!("Failed to transition blocks: {}", e)) + } ("skip-slots", Some(matches)) => { - skip_slots::run::<T>(env, matches).map_err(|e| format!("Failed to skip slots: {}", e)) + let network_config = get_network_config()?; + skip_slots::run::<T>(env, network_config, matches) + .map_err(|e| format!("Failed to skip slots: {}", e)) } ("pretty-ssz", Some(matches)) => { - run_parse_ssz::<T>(matches).map_err(|e| format!("Failed to pretty print hex: {}", e)) + let network_config = get_network_config()?; + run_parse_ssz::<T>(network_config, matches) + .map_err(|e| format!("Failed to pretty print hex: {}", e)) } ("deploy-deposit-contract", Some(matches)) => { deploy_deposit_contract::run::<T>(env, matches) .map_err(|e| format!("Failed to run deploy-deposit-contract command: {}", e)) } - ("eth1-genesis", Some(matches)) => eth1_genesis::run::<T>(env, testnet_dir, matches) - .map_err(|e| format!("Failed to run eth1-genesis command: {}", e)), - ("interop-genesis", Some(matches)) => interop_genesis::run::<T>(testnet_dir, matches) - .map_err(|e| format!("Failed to run interop-genesis command: {}", e)), + ("eth1-genesis", Some(matches)) => { + let testnet_dir = get_testnet_dir()?; + eth1_genesis::run::<T>(env, testnet_dir, matches) + .map_err(|e| format!("Failed to run eth1-genesis command: {}", e)) + } + ("interop-genesis", Some(matches)) => { + let testnet_dir = get_testnet_dir()?; + interop_genesis::run::<T>(testnet_dir, matches) + .map_err(|e| format!("Failed to run interop-genesis command: {}", e)) + } ("change-genesis-time", Some(matches)) => { + let testnet_dir = get_testnet_dir()?; change_genesis_time::run::<T>(testnet_dir, matches) .map_err(|e| format!("Failed to run change-genesis-time command: {}", e)) } ("create-payload-header", Some(matches)) => create_payload_header::run::<T>(matches) .map_err(|e| format!("Failed to run create-payload-header command: {}", e)), ("replace-state-pubkeys", Some(matches)) => { + let testnet_dir = get_testnet_dir()?; replace_state_pubkeys::run::<T>(testnet_dir, matches) .map_err(|e| format!("Failed to run replace-state-pubkeys command: {}", e)) } - ("new-testnet", Some(matches)) => new_testnet::run::<T>(testnet_dir, matches) - .map_err(|e| format!("Failed to run new_testnet command: {}", e)), + ("new-testnet", Some(matches)) => { + let testnet_dir = get_testnet_dir()?; + new_testnet::run::<T>(testnet_dir, matches) + .map_err(|e| format!("Failed to run new_testnet command: {}", e)) + } ("check-deposit-data", Some(matches)) => check_deposit_data::run(matches) .map_err(|e| format!("Failed to run check-deposit-data command: {}", e)), ("generate-bootnode-enr", Some(matches)) => generate_bootnode_enr::run::<T>(matches) @@ -932,8 +1013,16 @@ fn run<T: EthSpec>( .map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)), ("indexed-attestations", Some(matches)) => indexed_attestations::run::<T>(matches) .map_err(|e| format!("Failed to run indexed-attestations command: {}", e)), - ("block-root", Some(matches)) => block_root::run::<T>(env, matches) - .map_err(|e| format!("Failed to run block-root command: {}", e)), + ("block-root", Some(matches)) => { + let network_config = get_network_config()?; + block_root::run::<T>(env, network_config, matches) + .map_err(|e| format!("Failed to run block-root command: {}", e)) + } + ("state-root", Some(matches)) => { + let network_config = get_network_config()?; + state_root::run::<T>(env, network_config, matches) + .map_err(|e| format!("Failed to run state-root command: {}", e)) + } (other, _) => Err(format!("Unknown subcommand {}. See --help.", other)), } } diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs index aa5f52eef..973993f97 100644 --- a/lcli/src/new_testnet.rs +++ b/lcli/src/new_testnet.rs @@ -1,7 +1,7 @@ use account_utils::eth2_keystore::keypair_from_secret; use clap::ArgMatches; use clap_utils::{parse_optional, parse_required, parse_ssz_optional}; -use eth2_network_config::Eth2NetworkConfig; +use eth2_network_config::{Eth2NetworkConfig, GenesisStateSource}; use eth2_wallet::bip39::Seed; use eth2_wallet::bip39::{Language, Mnemonic}; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType}; @@ -190,7 +190,8 @@ pub fn run<T: EthSpec>(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Resul let testnet = Eth2NetworkConfig { deposit_contract_deploy_block, boot_enr: Some(vec![]), - genesis_state_bytes, + genesis_state_bytes: genesis_state_bytes.map(Into::into), + genesis_state_source: GenesisStateSource::IncludedBytes, config: Config::from_chain_spec::<T>(&spec), }; @@ -303,7 +304,7 @@ fn initialize_state_with_validators<T: EthSpec>( } // Now that we have our validators, initialize the caches (including the committees) - state.build_all_caches(spec).unwrap(); + state.build_caches(spec).unwrap(); // Set genesis validators root for domain separation and chain versioning *state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache().unwrap(); diff --git a/lcli/src/parse_ssz.rs b/lcli/src/parse_ssz.rs index 5d988ee18..5c306f4fd 100644 --- a/lcli/src/parse_ssz.rs +++ b/lcli/src/parse_ssz.rs @@ -1,5 +1,6 @@ use clap::ArgMatches; use clap_utils::parse_required; +use eth2_network_config::Eth2NetworkConfig; use serde::Serialize; use snap::raw::Decoder; use ssz::Decode; @@ -26,7 +27,10 @@ impl FromStr for OutputFormat { } } -pub fn run_parse_ssz<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> { +pub fn run_parse_ssz<T: EthSpec>( + network_config: Eth2NetworkConfig, + matches: &ArgMatches, +) -> Result<(), String> { let type_str = matches.value_of("type").ok_or("No type supplied")?; let filename = matches.value_of("ssz-file").ok_or("No file supplied")?; let format = parse_required(matches, "format")?; @@ -44,44 +48,79 @@ pub fn run_parse_ssz<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> { bytes }; - info!("Using {} spec", T::spec_name()); - info!("Type: {:?}", type_str); + let spec = &network_config.chain_spec::<T>()?; + info!( + "Using {} network config ({} preset)", + spec.config_name.as_deref().unwrap_or("unknown"), + T::spec_name() + ); + info!("Type: {type_str}"); + // More fork-specific decoders may need to be added in future, but shouldn't be 100% necessary, + // as the fork-generic decoder will always be available (requires correct --network flag). match type_str { - "signed_block_base" => decode_and_print::<SignedBeaconBlockBase<T>>(&bytes, format)?, - "signed_block_altair" => decode_and_print::<SignedBeaconBlockAltair<T>>(&bytes, format)?, - "signed_block_merge" => decode_and_print::<SignedBeaconBlockMerge<T>>(&bytes, format)?, - "block_base" => decode_and_print::<BeaconBlockBase<T>>(&bytes, format)?, - "block_altair" => decode_and_print::<BeaconBlockAltair<T>>(&bytes, format)?, - "block_merge" => decode_and_print::<BeaconBlockMerge<T>>(&bytes, format)?, - "state_base" => decode_and_print::<BeaconStateBase<T>>(&bytes, format)?, - "state_altair" => decode_and_print::<BeaconStateAltair<T>>(&bytes, format)?, - "state_merge" => decode_and_print::<BeaconStateMerge<T>>(&bytes, format)?, + "SignedBeaconBlock" => decode_and_print::<SignedBeaconBlock<T>>( + &bytes, + |bytes| SignedBeaconBlock::from_ssz_bytes(bytes, spec), + format, + )?, + "SignedBeaconBlockBase" | "SignedBeaconBlockPhase0" => { + decode_and_print(&bytes, SignedBeaconBlockBase::<T>::from_ssz_bytes, format)? + } + "SignedBeaconBlockAltair" => { + decode_and_print(&bytes, SignedBeaconBlockAltair::<T>::from_ssz_bytes, format)? + } + "SignedBeaconBlockMerge" | "SignedBeaconBlockBellatrix" => { + decode_and_print(&bytes, SignedBeaconBlockMerge::<T>::from_ssz_bytes, format)? + } + "SignedBeaconBlockCapella" => decode_and_print( + &bytes, + SignedBeaconBlockCapella::<T>::from_ssz_bytes, + format, + )?, + "BeaconState" => decode_and_print::<BeaconState<T>>( + &bytes, + |bytes| BeaconState::from_ssz_bytes(bytes, spec), + format, + )?, + "BeaconStateBase" | "BeaconStatePhase0" => { + decode_and_print(&bytes, BeaconStateBase::<T>::from_ssz_bytes, format)? + } + "BeaconStateAltair" => { + decode_and_print(&bytes, BeaconStateAltair::<T>::from_ssz_bytes, format)? + } + "BeaconStateMerge" | "BeaconStateBellatrix" => { + decode_and_print(&bytes, BeaconStateMerge::<T>::from_ssz_bytes, format)? + } + "BeaconStateCapella" => { + decode_and_print(&bytes, BeaconStateCapella::<T>::from_ssz_bytes, format)? + } other => return Err(format!("Unknown type: {}", other)), }; Ok(()) } -fn decode_and_print<T: Decode + Serialize>( +fn decode_and_print<T: Serialize>( bytes: &[u8], + decoder: impl FnOnce(&[u8]) -> Result<T, ssz::DecodeError>, output_format: OutputFormat, ) -> Result<(), String> { - let item = T::from_ssz_bytes(bytes).map_err(|e| format!("SSZ decode failed: {:?}", e))?; + let item = decoder(bytes).map_err(|e| format!("SSZ decode failed: {e:?}"))?; match output_format { OutputFormat::Json => { println!( "{}", serde_json::to_string(&item) - .map_err(|e| format!("Unable to write object to JSON: {:?}", e))? + .map_err(|e| format!("Unable to write object to JSON: {e:?}"))? ); } OutputFormat::Yaml => { println!( "{}", serde_yaml::to_string(&item) - .map_err(|e| format!("Unable to write object to YAML: {:?}", e))? + .map_err(|e| format!("Unable to write object to YAML: {e:?}"))? ); } } diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 49d1dd424..31fe9fe64 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -49,6 +49,7 @@ use clap::ArgMatches; use clap_utils::{parse_optional, parse_required}; use environment::Environment; use eth2::{types::StateId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use eth2_network_config::Eth2NetworkConfig; use ssz::Encode; use state_processing::state_advance::{complete_state_advance, partial_state_advance}; use std::fs::File; @@ -59,8 +60,12 @@ use types::{BeaconState, CloneConfig, EthSpec, Hash256}; const HTTP_TIMEOUT: Duration = Duration::from_secs(10); -pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> { - let spec = &T::default_spec(); +pub fn run<T: EthSpec>( + env: Environment<T>, + network_config: Eth2NetworkConfig, + matches: &ArgMatches, +) -> Result<(), String> { + let spec = &network_config.chain_spec::<T>()?; let executor = env.core_context().executor; let output_path: Option<PathBuf> = parse_optional(matches, "output-path")?; @@ -109,7 +114,7 @@ pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), let target_slot = initial_slot + slots; state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = if let Some(root) = cli_state_root.or(state_root) { diff --git a/lcli/src/state_root.rs b/lcli/src/state_root.rs new file mode 100644 index 000000000..efcee2827 --- /dev/null +++ b/lcli/src/state_root.rs @@ -0,0 +1,76 @@ +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_network_config::Eth2NetworkConfig; +use std::path::PathBuf; +use std::time::{Duration, Instant}; +use types::{BeaconState, EthSpec}; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(10); + +pub fn run<T: EthSpec>( + env: Environment<T>, + network_config: Eth2NetworkConfig, + matches: &ArgMatches, +) -> Result<(), String> { + let executor = env.core_context().executor; + + let spec = &network_config.chain_spec::<T>()?; + + let state_path: Option<PathBuf> = parse_optional(matches, "state-path")?; + let beacon_url: Option<SensitiveUrl> = parse_optional(matches, "beacon-url")?; + let runs: usize = parse_required(matches, "runs")?; + + info!( + "Using {} network ({} spec)", + spec.config_name.as_deref().unwrap_or("unknown"), + T::spec_name() + ); + info!("Doing {} runs", runs); + + let state = match (state_path, beacon_url) { + (Some(state_path), None) => { + info!("State path: {:?}", state_path); + load_from_ssz_with(&state_path, spec, BeaconState::from_ssz_bytes)? + } + (None, Some(beacon_url)) => { + let state_id: StateId = parse_required(matches, "state-id")?; + let client = BeaconNodeHttpClient::new(beacon_url, Timeouts::set_all(HTTP_TIMEOUT)); + executor + .handle() + .ok_or("shutdown in progress")? + .block_on(async move { + client + .get_debug_beacon_states::<T>(state_id) + .await + .map_err(|e| format!("Failed to download state: {:?}", e)) + }) + .map_err(|e| format!("Failed to complete task: {:?}", e))? + .ok_or_else(|| format!("Unable to locate state at {:?}", state_id))? + .data + } + _ => return Err("must supply either --state-path or --beacon-url".into()), + }; + + /* + * Perform the core "runs". + */ + let mut state_root = None; + for i in 0..runs { + let mut state = state.clone(); + let timer = Instant::now(); + state_root = Some( + state + .update_tree_hash_cache() + .map_err(|e| format!("error computing state root: {e:?}"))?, + ); + info!("Run {}: {:?}", i, timer.elapsed()); + } + + if let Some(state_root) = state_root { + info!("State root is {:?}", state_root); + } + Ok(()) +} diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index cf971c69f..23b0ae262 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -71,10 +71,12 @@ use eth2::{ types::{BlockId, StateId}, BeaconNodeHttpClient, SensitiveUrl, Timeouts, }; +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, per_slot_processing, - BlockSignatureStrategy, ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, + block_signature_verifier::BlockSignatureVerifier, per_block_processing, BlockSignatureStrategy, + ConsensusContext, StateProcessingStrategy, VerifyBlockRoot, }; use std::borrow::Cow; use std::fs::File; @@ -94,8 +96,12 @@ struct Config { exclude_post_block_thc: bool, } -pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), String> { - let spec = &T::default_spec(); +pub fn run<T: EthSpec>( + env: Environment<T>, + network_config: Eth2NetworkConfig, + matches: &ArgMatches, +) -> Result<(), String> { + let spec = &network_config.chain_spec::<T>()?; let executor = env.core_context().executor; /* @@ -205,7 +211,7 @@ pub fn run<T: EthSpec>(env: Environment<T>, matches: &ArgMatches) -> Result<(), if config.exclude_cache_builds { pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; let state_root = pre_state .update_tree_hash_cache() @@ -303,7 +309,7 @@ fn do_transition<T: EthSpec>( if !config.exclude_cache_builds { let t = Instant::now(); pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build caches: {:?}", t.elapsed()); @@ -327,15 +333,13 @@ fn do_transition<T: EthSpec>( // Transition the parent state to the block slot. let t = Instant::now(); - for i in pre_state.slot().as_u64()..block.slot().as_u64() { - per_slot_processing(&mut pre_state, Some(state_root), spec) - .map_err(|e| format!("Failed to advance slot on iteration {}: {:?}", i, e))?; - } + complete_state_advance(&mut pre_state, Some(state_root), block.slot(), spec) + .map_err(|e| format!("Unable to perform complete advance: {e:?}"))?; debug!("Slot processing: {:?}", t.elapsed()); let t = Instant::now(); pre_state - .build_all_caches(spec) + .build_caches(spec) .map_err(|e| format!("Unable to build caches: {:?}", e))?; debug!("Build all caches (again): {:?}", t.elapsed()); diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 5d2b5e092..48b4eb037 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "lighthouse" -version = "4.2.0" +version = "4.5.0" authors = ["Sigma Prime <contact@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } autotests = false -rust-version = "1.68.2" +rust-version = "1.69.0" [features] -default = ["slasher-mdbx"] +default = ["slasher-lmdb"] # Writes debugging .ssz files to /tmp during block processing. write_ssz_files = ["beacon_node/write_ssz_files"] # Compiles the BLS crypto code so that the binary is portable across machines. @@ -28,42 +28,45 @@ slasher-lmdb = ["slasher/lmdb"] jemalloc = ["malloc_utils/jemalloc"] [dependencies] -beacon_node = { "path" = "../beacon_node" } -slog = { version = "2.5.2", features = ["max_level_trace"] } -sloggers = { version = "2.1.1", features = ["json"] } -types = { "path" = "../consensus/types" } -bls = { path = "../crypto/bls" } -ethereum_hashing = "1.0.0-beta.2" -clap = "2.33.3" -env_logger = "0.9.0" -environment = { path = "./environment" } +beacon_node = { workspace = true } +slog = { workspace = true } +sloggers = { workspace = true } +types = { workspace = true } +bls = { workspace = true } +ethereum_hashing = { workspace = true } +clap = { workspace = true } +env_logger = { workspace = true } +environment = { workspace = true } boot_node = { path = "../boot_node" } -futures = "0.3.7" -validator_client = { "path" = "../validator_client" } +futures = { workspace = true } +validator_client = { workspace = true } account_manager = { "path" = "../account_manager" } -clap_utils = { path = "../common/clap_utils" } -eth2_network_config = { path = "../common/eth2_network_config" } -lighthouse_version = { path = "../common/lighthouse_version" } -account_utils = { path = "../common/account_utils" } -lighthouse_metrics = { path = "../common/lighthouse_metrics" } -lazy_static = "1.4.0" -serde = { version = "1.0.116", features = ["derive"] } -serde_json = "1.0.59" -serde_yaml = "0.8.13" -task_executor = { path = "../common/task_executor" } -malloc_utils = { path = "../common/malloc_utils" } -directory = { path = "../common/directory" } -unused_port = { path = "../common/unused_port" } +clap_utils = { workspace = true } +eth2_network_config = { workspace = true } +lighthouse_version = { workspace = true } +account_utils = { workspace = true } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +task_executor = { workspace = true } +malloc_utils = { workspace = true } +directory = { workspace = true } +unused_port = { workspace = true } database_manager = { path = "../database_manager" } -slasher = { path = "../slasher", default-features = false } +slasher = { workspace = true } +validator_manager = { path = "../validator_manager" } [dev-dependencies] -tempfile = "3.1.0" -validator_dir = { path = "../common/validator_dir" } -slashing_protection = { path = "../validator_client/slashing_protection" } -lighthouse_network = { path = "../beacon_node/lighthouse_network" } -sensitive_url = { path = "../common/sensitive_url" } -eth1 = { path = "../beacon_node/eth1" } +tempfile = { workspace = true } +validator_dir = { workspace = true } +slashing_protection = { workspace = true } +lighthouse_network = { workspace = true } +sensitive_url = { workspace = true } +eth1 = { workspace = true } +eth2 = { workspace = true } +beacon_processor = { workspace = true } [[test]] name = "lighthouse_tests" diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index 1ba0bb267..d2a181a1b 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -2,23 +2,23 @@ name = "environment" version = "0.1.2" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -tokio = { version = "1.14.0", features = ["macros", "rt", "rt-multi-thread", "signal" ] } -slog = { version = "2.5.2", features = ["max_level_trace"] } -sloggers = { version = "2.1.1", features = ["json"] } -types = { path = "../../consensus/types" } -eth2_config = { path = "../../common/eth2_config" } -task_executor = { path = "../../common/task_executor" } -eth2_network_config = { path = "../../common/eth2_network_config" } -logging = { path = "../../common/logging" } -slog-term = "2.6.0" -slog-async = "2.5.0" -futures = "0.3.7" +tokio = { workspace = true } +slog = { workspace = true } +sloggers = { workspace = true } +types = { workspace = true } +eth2_config = { workspace = true } +task_executor = { workspace = true } +eth2_network_config = { workspace = true } +logging = { workspace = true } +slog-term = { workspace = true } +slog-async = { workspace = true } +futures = { workspace = true } slog-json = "2.3.0" -exit-future = "0.2.0" -serde = "1.0.116" +exit-future = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" [target.'cfg(not(target_family = "unix"))'.dependencies] diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index 53915b52d..fc7ab8d52 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -344,18 +344,6 @@ impl<E: EthSpec> EnvironmentBuilder<E> { Ok(self) } - /// Optionally adds a network configuration to the environment. - pub fn optional_eth2_network_config( - self, - optional_config: Option<Eth2NetworkConfig>, - ) -> Result<Self, String> { - if let Some(config) = optional_config { - self.eth2_network_config(config) - } else { - Ok(self) - } - } - /// Consumes the builder, returning an `Environment`. pub fn build(self) -> Result<Environment<E>, String> { let (signal, exit) = exit_future::signal(); diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index 33aa8ad16..b98145163 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -81,3 +81,17 @@ PROPOSER_SCORE_BOOST: 40 DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + +# Network +# --------------------------------------------------------------- +SUBNETS_PER_NODE: 2 +GOSSIP_MAX_SIZE: 10485760 +MIN_EPOCHS_FOR_BLOCK_REQUESTS: 33024 +MAX_CHUNK_SIZE: 10485760 +TTFB_TIMEOUT: 5 +RESP_TIMEOUT: 10 +MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000 +MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000 +ATTESTATION_SUBNET_COUNT: 64 +ATTESTATION_SUBNET_EXTRA_BITS: 0 +ATTESTATION_SUBNET_PREFIX_BITS: 6 diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index b814639ce..f98af9617 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -8,6 +8,7 @@ use env_logger::{Builder, Env}; use environment::{EnvironmentBuilder, LoggerConfig}; use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK, HARDCODED_NET_NAMES}; use ethereum_hashing::have_sha_extensions; +use futures::TryFutureExt; use lighthouse_version::VERSION; use malloc_utils::configure_memory_allocator; use slog::{crit, info, warn}; @@ -324,11 +325,36 @@ fn main() { .takes_value(true) .global(true) ) + .arg( + Arg::with_name("genesis-state-url") + .long("genesis-state-url") + .value_name("URL") + .help( + "A URL of a beacon-API compatible server from which to download the genesis state. \ + Checkpoint sync server URLs can generally be used with this flag. \ + If not supplied, a default URL or the --checkpoint-sync-url may be used. \ + If the genesis state is already included in this binary then this value will be ignored.", + ) + .takes_value(true) + .global(true), + ) + .arg( + Arg::with_name("genesis-state-url-timeout") + .long("genesis-state-url-timeout") + .value_name("SECONDS") + .help( + "The timeout in seconds for the request to --genesis-state-url.", + ) + .takes_value(true) + .default_value("180") + .global(true), + ) .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) .subcommand(validator_client::cli_app()) .subcommand(account_manager::cli_app()) .subcommand(database_manager::cli_app()) + .subcommand(validator_manager::cli_app()) .get_matches(); // Configure the allocator early in the process, before it has the chance to use the default values for @@ -512,7 +538,7 @@ fn run<E: EthSpec>( let mut environment = builder .multi_threaded_tokio_runtime()? - .optional_eth2_network_config(Some(eth2_network_config))? + .eth2_network_config(eth2_network_config)? .build()?; let log = environment.core_context().log().clone(); @@ -558,7 +584,7 @@ fn run<E: EthSpec>( (Some(_), Some(_)) => panic!("CLI prevents both --network and --testnet-dir"), }; - if let Some(sub_matches) = matches.subcommand_matches("account_manager") { + if let Some(sub_matches) = matches.subcommand_matches(account_manager::CMD) { eprintln!("Running account manager for {} network", network_name); // Pass the entire `environment` to the account manager so it can run blocking operations. account_manager::run(sub_matches, environment)?; @@ -567,6 +593,16 @@ fn run<E: EthSpec>( return Ok(()); } + if let Some(sub_matches) = matches.subcommand_matches(validator_manager::CMD) { + eprintln!("Running validator manager for {} network", network_name); + + // Pass the entire `environment` to the account manager so it can run blocking operations. + validator_manager::run::<E>(sub_matches, environment)?; + + // Exit as soon as account manager returns control. + return Ok(()); + } + if let Some(sub_matches) = matches.subcommand_matches(database_manager::CMD) { info!(log, "Running database manager for {} network", network_name); // Pass the entire `environment` to the database manager so it can run blocking operations. @@ -624,8 +660,8 @@ fn run<E: EthSpec>( executor.clone().spawn( async move { if let Err(e) = ProductionValidatorClient::new(context, config) + .and_then(|mut vc| async move { vc.start_service().await }) .await - .and_then(|mut vc| vc.start_service()) { crit!(log, "Failed to start validator client"; "reason" => e); // Ignore the error since it always occurs during normal operation when diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 75bcccc9d..bc6b6284e 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -5,18 +5,23 @@ use beacon_node::beacon_chain::chain_config::{ DisallowedReOrgOffsets, DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_THRESHOLD, }; +use beacon_processor::BeaconProcessorConfig; use eth1::Eth1Endpoint; use lighthouse_network::PeerId; use std::fs::File; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use std::string::ToString; use std::time::Duration; use tempfile::TempDir; -use types::{Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec}; +use types::{ + Address, Checkpoint, Epoch, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, + ProgressiveBalancesMode, +}; use unused_port::{unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port}; const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; @@ -174,7 +179,7 @@ fn checkpoint_sync_url_timeout_default() { CommandLineTest::new() .run_with_zero_port() .with_config(|config| { - assert_eq!(config.chain.checkpoint_sync_url_timeout, 60); + assert_eq!(config.chain.checkpoint_sync_url_timeout, 180); }); } @@ -362,21 +367,6 @@ fn genesis_backfill_with_historic_flag() { .with_config(|config| assert_eq!(config.chain.genesis_backfill, true)); } -#[test] -fn always_prefer_builder_payload_flag() { - CommandLineTest::new() - .flag("always-prefer-builder-payload", None) - .run_with_zero_port() - .with_config(|config| assert!(config.always_prefer_builder_payload)); -} - -#[test] -fn no_flag_sets_always_prefer_builder_payload_to_false() { - CommandLineTest::new() - .run_with_zero_port() - .with_config(|config| assert!(!config.always_prefer_builder_payload)); -} - // Tests for Eth1 flags. #[test] fn dummy_eth1_flag() { @@ -731,6 +721,38 @@ fn builder_fallback_flags() { ); }, ); + run_payload_builder_flag_test_with_config( + "builder", + "http://meow.cats", + Some("always-prefer-builder-payload"), + None, + |config| { + assert_eq!( + config + .execution_layer + .as_ref() + .unwrap() + .always_prefer_builder_payload, + true + ); + }, + ); + run_payload_builder_flag_test_with_config( + "builder", + "http://meow.cats", + None, + None, + |config| { + assert_eq!( + config + .execution_layer + .as_ref() + .unwrap() + .always_prefer_builder_payload, + false + ); + }, + ); } #[test] @@ -988,12 +1010,12 @@ fn network_port_flag_over_ipv4() { .run() .with_config(|config| { assert_eq!( - config - .network - .listen_addrs() - .v4() - .map(|listen_addr| (listen_addr.udp_port, listen_addr.tcp_port)), - Some((port, port)) + config.network.listen_addrs().v4().map(|listen_addr| ( + listen_addr.disc_port, + listen_addr.quic_port, + listen_addr.tcp_port + )), + Some((port, port + 1, port)) ); }); } @@ -1006,22 +1028,22 @@ fn network_port_flag_over_ipv6() { .run() .with_config(|config| { assert_eq!( - config - .network - .listen_addrs() - .v6() - .map(|listen_addr| (listen_addr.udp_port, listen_addr.tcp_port)), - Some((port, port)) + config.network.listen_addrs().v6().map(|listen_addr| ( + listen_addr.disc_port, + listen_addr.quic_port, + listen_addr.tcp_port + )), + Some((port, port + 1, port)) ); }); } #[test] fn network_port_and_discovery_port_flags_over_ipv4() { let tcp4_port = unused_tcp4_port().expect("Unable to find unused port."); - let udp4_port = unused_udp4_port().expect("Unable to find unused port."); + let disc4_port = unused_udp4_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("port", Some(tcp4_port.to_string().as_str())) - .flag("discovery-port", Some(udp4_port.to_string().as_str())) + .flag("discovery-port", Some(disc4_port.to_string().as_str())) .run() .with_config(|config| { assert_eq!( @@ -1029,19 +1051,19 @@ fn network_port_and_discovery_port_flags_over_ipv4() { .network .listen_addrs() .v4() - .map(|listen_addr| (listen_addr.tcp_port, listen_addr.udp_port)), - Some((tcp4_port, udp4_port)) + .map(|listen_addr| (listen_addr.tcp_port, listen_addr.disc_port)), + Some((tcp4_port, disc4_port)) ); }); } #[test] fn network_port_and_discovery_port_flags_over_ipv6() { let tcp6_port = unused_tcp6_port().expect("Unable to find unused port."); - let udp6_port = unused_udp6_port().expect("Unable to find unused port."); + let disc6_port = unused_udp6_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("port", Some(tcp6_port.to_string().as_str())) - .flag("discovery-port", Some(udp6_port.to_string().as_str())) + .flag("discovery-port", Some(disc6_port.to_string().as_str())) .run() .with_config(|config| { assert_eq!( @@ -1049,24 +1071,24 @@ fn network_port_and_discovery_port_flags_over_ipv6() { .network .listen_addrs() .v6() - .map(|listen_addr| (listen_addr.tcp_port, listen_addr.udp_port)), - Some((tcp6_port, udp6_port)) + .map(|listen_addr| (listen_addr.tcp_port, listen_addr.disc_port)), + Some((tcp6_port, disc6_port)) ); }); } #[test] fn network_port_and_discovery_port_flags_over_ipv4_and_ipv6() { let tcp4_port = unused_tcp4_port().expect("Unable to find unused port."); - let udp4_port = unused_udp4_port().expect("Unable to find unused port."); + let disc4_port = unused_udp4_port().expect("Unable to find unused port."); let tcp6_port = unused_tcp6_port().expect("Unable to find unused port."); - let udp6_port = unused_udp6_port().expect("Unable to find unused port."); + let disc6_port = unused_udp6_port().expect("Unable to find unused port."); CommandLineTest::new() .flag("listen-address", Some("::1")) .flag("listen-address", Some("127.0.0.1")) .flag("port", Some(tcp4_port.to_string().as_str())) - .flag("discovery-port", Some(udp4_port.to_string().as_str())) + .flag("discovery-port", Some(disc4_port.to_string().as_str())) .flag("port6", Some(tcp6_port.to_string().as_str())) - .flag("discovery-port6", Some(udp6_port.to_string().as_str())) + .flag("discovery-port6", Some(disc6_port.to_string().as_str())) .run() .with_config(|config| { assert_eq!( @@ -1074,8 +1096,8 @@ fn network_port_and_discovery_port_flags_over_ipv4_and_ipv6() { .network .listen_addrs() .v4() - .map(|listen_addr| (listen_addr.tcp_port, listen_addr.udp_port)), - Some((tcp4_port, udp4_port)) + .map(|listen_addr| (listen_addr.tcp_port, listen_addr.disc_port)), + Some((tcp4_port, disc4_port)) ); assert_eq!( @@ -1083,8 +1105,47 @@ fn network_port_and_discovery_port_flags_over_ipv4_and_ipv6() { .network .listen_addrs() .v6() - .map(|listen_addr| (listen_addr.tcp_port, listen_addr.udp_port)), - Some((tcp6_port, udp6_port)) + .map(|listen_addr| (listen_addr.tcp_port, listen_addr.disc_port)), + Some((tcp6_port, disc6_port)) + ); + }); +} + +#[test] +fn network_port_discovery_quic_port_flags_over_ipv4_and_ipv6() { + let tcp4_port = unused_tcp4_port().expect("Unable to find unused port."); + let disc4_port = unused_udp4_port().expect("Unable to find unused port."); + let quic4_port = unused_udp4_port().expect("Unable to find unused port."); + let tcp6_port = unused_tcp6_port().expect("Unable to find unused port."); + let disc6_port = unused_udp6_port().expect("Unable to find unused port."); + let quic6_port = unused_udp6_port().expect("Unable to find unused port."); + CommandLineTest::new() + .flag("listen-address", Some("::1")) + .flag("listen-address", Some("127.0.0.1")) + .flag("port", Some(tcp4_port.to_string().as_str())) + .flag("discovery-port", Some(disc4_port.to_string().as_str())) + .flag("quic-port", Some(quic4_port.to_string().as_str())) + .flag("port6", Some(tcp6_port.to_string().as_str())) + .flag("discovery-port6", Some(disc6_port.to_string().as_str())) + .flag("quic-port6", Some(quic6_port.to_string().as_str())) + .run() + .with_config(|config| { + assert_eq!( + config.network.listen_addrs().v4().map(|listen_addr| ( + listen_addr.tcp_port, + listen_addr.disc_port, + listen_addr.quic_port + )), + Some((tcp4_port, disc4_port, quic4_port)) + ); + + assert_eq!( + config.network.listen_addrs().v6().map(|listen_addr| ( + listen_addr.tcp_port, + listen_addr.disc_port, + listen_addr.quic_port + )), + Some((tcp6_port, disc6_port, quic6_port)) ); }); } @@ -1096,6 +1157,14 @@ fn disable_discovery_flag() { .run_with_zero_port() .with_config(|config| assert!(config.network.disable_discovery)); } + +#[test] +fn disable_quic_flag() { + CommandLineTest::new() + .flag("disable-quic", None) + .run_with_zero_port() + .with_config(|config| assert!(config.network.disable_quic_support)); +} #[test] fn disable_peer_scoring_flag() { CommandLineTest::new() @@ -1115,58 +1184,23 @@ fn disable_backfill_rate_limiting_flag() { CommandLineTest::new() .flag("disable-backfill-rate-limiting", None) .run_with_zero_port() - .with_config(|config| assert!(!config.chain.enable_backfill_rate_limiting)); + .with_config(|config| assert!(!config.beacon_processor.enable_backfill_rate_limiting)); } #[test] fn default_backfill_rate_limiting_flag() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert!(config.chain.enable_backfill_rate_limiting)); + .with_config(|config| assert!(config.beacon_processor.enable_backfill_rate_limiting)); } #[test] fn default_boot_nodes() { - let mainnet = vec![ - // Lighthouse Team (Sigma Prime) - "enr:-Jq4QItoFUuug_n_qbYbU0OY04-np2wT8rUCauOOXNi0H3BWbDj-zbfZb7otA7jZ6flbBpx1LNZK2TDebZ9dEKx84LYBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISsaa0ZiXNlY3AyNTZrMaEDHAD2JKYevx89W0CcFJFiskdcEzkH_Wdv9iW42qLK79ODdWRwgiMo", - "enr:-Jq4QN_YBsUOqQsty1OGvYv48PMaiEt1AzGD1NkYQHaxZoTyVGqMYXg0K9c0LPNWC9pkXmggApp8nygYLsQwScwAgfgBhGV0aDKQtTA_KgEAAAD__________4JpZIJ2NIJpcISLosQxiXNlY3AyNTZrMaEDBJj7_dLFACaxBfaI8KZTh_SSJUjhyAyfshimvSqo22WDdWRwgiMo", - // EF Team - "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", - "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", - "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", - "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", - // Teku team (Consensys) - "enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA", - "enr:-KG4QDyytgmE4f7AnvW-ZaUOIi9i79qX4JwjRAiXBZCU65wOfBu-3Nb5I7b_Rmg3KCOcZM_C3y5pg7EBU5XGrcLTduQEhGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaEDKnz_-ps3UUOfHWVYaskI5kWYO_vtYMGYCQRAR3gHDouDdGNwgiMog3VkcIIjKA", - // Prysm team (Prysmatic Labs) - "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", - "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", - "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", - // Nimbus team - "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", - "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM" - ]; + let number_of_boot_nodes = 17; CommandLineTest::new() .run_with_zero_port() .with_config(|config| { // Lighthouse Team (Sigma Prime) - assert_eq!(config.network.boot_nodes_enr[0].to_base64(), mainnet[0]); - assert_eq!(config.network.boot_nodes_enr[1].to_base64(), mainnet[1]); - // EF Team - assert_eq!(config.network.boot_nodes_enr[2].to_base64(), mainnet[2]); - assert_eq!(config.network.boot_nodes_enr[3].to_base64(), mainnet[3]); - assert_eq!(config.network.boot_nodes_enr[4].to_base64(), mainnet[4]); - assert_eq!(config.network.boot_nodes_enr[5].to_base64(), mainnet[5]); - // Teku team (Consensys) - assert_eq!(config.network.boot_nodes_enr[6].to_base64(), mainnet[6]); - assert_eq!(config.network.boot_nodes_enr[7].to_base64(), mainnet[7]); - // Prysm team (Prysmatic Labs) - assert_eq!(config.network.boot_nodes_enr[8].to_base64(), mainnet[8]); - assert_eq!(config.network.boot_nodes_enr[9].to_base64(), mainnet[9]); - assert_eq!(config.network.boot_nodes_enr[10].to_base64(), mainnet[10]); - // Nimbus team - assert_eq!(config.network.boot_nodes_enr[11].to_base64(), mainnet[11]); - assert_eq!(config.network.boot_nodes_enr[12].to_base64(), mainnet[12]); + assert_eq!(config.network.boot_nodes_enr.len(), number_of_boot_nodes); }); } #[test] @@ -1237,6 +1271,14 @@ fn enr_udp_port_flag() { .with_config(|config| assert_eq!(config.network.enr_udp4_port, Some(port))); } #[test] +fn enr_quic_port_flag() { + let port = unused_udp4_port().expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-quic-port", Some(port.to_string().as_str())) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.network.enr_quic4_port, Some(port))); +} +#[test] fn enr_tcp_port_flag() { let port = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() @@ -1253,6 +1295,14 @@ fn enr_udp6_port_flag() { .with_config(|config| assert_eq!(config.network.enr_udp6_port, Some(port))); } #[test] +fn enr_quic6_port_flag() { + let port = unused_udp6_port().expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-quic6-port", Some(port.to_string().as_str())) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.network.enr_quic6_port, Some(port))); +} +#[test] fn enr_tcp6_port_flag() { let port = unused_tcp6_port().expect("Unable to find unused port."); CommandLineTest::new() @@ -1275,7 +1325,7 @@ fn enr_match_flag_over_ipv4() { assert_eq!( config.network.listen_addrs().v4().map(|listen_addr| ( listen_addr.addr, - listen_addr.udp_port, + listen_addr.disc_port, listen_addr.tcp_port )), Some((addr, udp4_port, tcp4_port)) @@ -1300,7 +1350,7 @@ fn enr_match_flag_over_ipv6() { assert_eq!( config.network.listen_addrs().v6().map(|listen_addr| ( listen_addr.addr, - listen_addr.udp_port, + listen_addr.disc_port, listen_addr.tcp_port )), Some((addr, udp6_port, tcp6_port)) @@ -1332,7 +1382,7 @@ fn enr_match_flag_over_ipv4_and_ipv6() { assert_eq!( config.network.listen_addrs().v6().map(|listen_addr| ( listen_addr.addr, - listen_addr.udp_port, + listen_addr.disc_port, listen_addr.tcp_port )), Some((ipv6_addr, udp6_port, tcp6_port)) @@ -1340,7 +1390,7 @@ fn enr_match_flag_over_ipv4_and_ipv6() { assert_eq!( config.network.listen_addrs().v4().map(|listen_addr| ( listen_addr.addr, - listen_addr.udp_port, + listen_addr.disc_port, listen_addr.tcp_port )), Some((ipv4_addr, udp4_port, tcp4_port)) @@ -1416,6 +1466,7 @@ fn http_flag() { fn http_address_flag() { let addr = "127.0.0.99".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("http", None) .flag("http-address", Some("127.0.0.99")) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); @@ -1424,6 +1475,7 @@ fn http_address_flag() { fn http_address_ipv6_flag() { let addr = "::1".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("http", None) .flag("http-address", Some("::1")) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); @@ -1433,6 +1485,7 @@ fn http_port_flag() { let port1 = unused_tcp4_port().expect("Unable to find unused port."); let port2 = unused_tcp4_port().expect("Unable to find unused port."); CommandLineTest::new() + .flag("http", None) .flag("http-port", Some(port1.to_string().as_str())) .flag("port", Some(port2.to_string().as_str())) .run() @@ -1451,18 +1504,43 @@ fn empty_self_limiter_flag() { ) }); } + +#[test] +fn empty_inbound_rate_limiter_flag() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.network.inbound_rate_limiter_config, + Some(lighthouse_network::rpc::config::InboundRateLimiterConfig::default()) + ) + }); +} +#[test] +fn disable_inbound_rate_limiter_flag() { + CommandLineTest::new() + .flag("inbound-rate-limiter", Some("disabled")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.network.inbound_rate_limiter_config, None)); +} + #[test] fn http_allow_origin_flag() { CommandLineTest::new() - .flag("http-allow-origin", Some("127.0.0.99")) + .flag("http", None) + .flag("http-allow-origin", Some("http://127.0.0.99")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.http_api.allow_origin, Some("127.0.0.99".to_string())); + assert_eq!( + config.http_api.allow_origin, + Some("http://127.0.0.99".to_string()) + ); }); } #[test] fn http_allow_origin_all_flag() { CommandLineTest::new() + .flag("http", None) .flag("http-allow-origin", Some("*")) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); @@ -1470,23 +1548,37 @@ fn http_allow_origin_all_flag() { #[test] fn http_allow_sync_stalled_flag() { CommandLineTest::new() + .flag("http", None) .flag("http-allow-sync-stalled", None) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.allow_sync_stalled, true)); } #[test] -fn http_tls_flags() { - let dir = TempDir::new().expect("Unable to create temporary directory"); +fn http_enable_beacon_processor() { CommandLineTest::new() + .flag("http", None) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + + CommandLineTest::new() + .flag("http", None) + .flag("http-enable-beacon-processor", Some("true")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + + CommandLineTest::new() + .flag("http", None) + .flag("http-enable-beacon-processor", Some("false")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, false)); +} +#[test] +fn http_tls_flags() { + CommandLineTest::new() + .flag("http", None) .flag("http-enable-tls", None) - .flag( - "http-tls-cert", - dir.path().join("certificate.crt").as_os_str().to_str(), - ) - .flag( - "http-tls-key", - dir.path().join("private.key").as_os_str().to_str(), - ) + .flag("http-tls-cert", Some("tests/tls/cert.pem")) + .flag("http-tls-key", Some("tests/tls/key.rsa")) .run_with_zero_port() .with_config(|config| { let tls_config = config @@ -1494,14 +1586,15 @@ fn http_tls_flags() { .tls_config .as_ref() .expect("tls_config was empty."); - assert_eq!(tls_config.cert, dir.path().join("certificate.crt")); - assert_eq!(tls_config.key, dir.path().join("private.key")); + assert_eq!(tls_config.cert, Path::new("tests/tls/cert.pem")); + assert_eq!(tls_config.key, Path::new("tests/tls/key.rsa")); }); } #[test] fn http_spec_fork_default() { CommandLineTest::new() + .flag("http", None) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.spec_fork_name, None)); } @@ -1509,6 +1602,7 @@ fn http_spec_fork_default() { #[test] fn http_spec_fork_override() { CommandLineTest::new() + .flag("http", None) .flag("http-spec-fork", Some("altair")) .run_with_zero_port() .with_config(|config| assert_eq!(config.http_api.spec_fork_name, Some(ForkName::Altair))); @@ -1745,12 +1839,32 @@ fn no_reconstruct_historic_states_flag() { .run_with_zero_port() .with_config(|config| assert!(!config.chain.reconstruct_historic_states)); } +#[test] +fn epochs_per_migration_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.epochs_per_migration, + beacon_node::beacon_chain::migrate::DEFAULT_EPOCHS_PER_MIGRATION + ) + }); +} +#[test] +fn epochs_per_migration_override() { + CommandLineTest::new() + .flag("epochs-per-migration", Some("128")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.epochs_per_migration, 128)); +} // Tests for Slasher flags. +// Using `--slasher-max-db-size` to work around https://github.com/sigp/lighthouse/issues/2342 #[test] fn slasher_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .run_with_zero_port() .with_config_and_dir(|config, dir| { if let Some(slasher_config) = &config.slasher { @@ -1768,6 +1882,7 @@ fn slasher_dir_flag() { let dir = TempDir::new().expect("Unable to create temporary directory"); CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-dir", dir.path().as_os_str().to_str()) .run_with_zero_port() .with_config(|config| { @@ -1782,6 +1897,7 @@ fn slasher_dir_flag() { fn slasher_update_period_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-update-period", Some("100")) .run_with_zero_port() .with_config(|config| { @@ -1796,6 +1912,7 @@ fn slasher_update_period_flag() { fn slasher_slot_offset_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-slot-offset", Some("11.25")) .run_with_zero_port() .with_config(|config| { @@ -1808,6 +1925,7 @@ fn slasher_slot_offset_flag() { fn slasher_slot_offset_nan_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-slot-offset", Some("NaN")) .run_with_zero_port(); } @@ -1815,6 +1933,7 @@ fn slasher_slot_offset_nan_flag() { fn slasher_history_length_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-history-length", Some("2048")) .run_with_zero_port() .with_config(|config| { @@ -1829,20 +1948,21 @@ fn slasher_history_length_flag() { fn slasher_max_db_size_flag() { CommandLineTest::new() .flag("slasher", None) - .flag("slasher-max-db-size", Some("10")) + .flag("slasher-max-db-size", Some("2")) .run_with_zero_port() .with_config(|config| { let slasher_config = config .slasher .as_ref() .expect("Unable to parse Slasher config"); - assert_eq!(slasher_config.max_db_size_mbs, 10240); + assert_eq!(slasher_config.max_db_size_mbs, 2048); }); } #[test] fn slasher_attestation_cache_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-att-cache-size", Some("10000")) .run_with_zero_port() .with_config(|config| { @@ -1857,6 +1977,7 @@ fn slasher_attestation_cache_size_flag() { fn slasher_chunk_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-chunk-size", Some("32")) .run_with_zero_port() .with_config(|config| { @@ -1871,6 +1992,7 @@ fn slasher_chunk_size_flag() { fn slasher_validator_chunk_size_flag() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-validator-chunk-size", Some("512")) .run_with_zero_port() .with_config(|config| { @@ -1882,9 +2004,10 @@ fn slasher_validator_chunk_size_flag() { }); } #[test] -fn slasher_broadcast_flag() { +fn slasher_broadcast_flag_no_args() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .flag("slasher-broadcast", None) .run_with_zero_port() .with_config(|config| { @@ -1895,29 +2018,62 @@ fn slasher_broadcast_flag() { assert!(slasher_config.broadcast); }); } - #[test] -fn slasher_backend_default() { +fn slasher_broadcast_flag_no_default() { CommandLineTest::new() .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) .run_with_zero_port() .with_config(|config| { - let slasher_config = config.slasher.as_ref().unwrap(); - assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Mdbx); + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(slasher_config.broadcast); + }); +} +#[test] +fn slasher_broadcast_flag_true() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-broadcast", Some("true")) + .run_with_zero_port() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(slasher_config.broadcast); + }); +} +#[test] +fn slasher_broadcast_flag_false() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-broadcast", Some("false")) + .run_with_zero_port() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(!slasher_config.broadcast); }); } - #[test] fn slasher_backend_override_to_default() { // Hard to test this flag because all but one backend is disabled by default and the backend // called "disabled" results in a panic. CommandLineTest::new() .flag("slasher", None) - .flag("slasher-backend", Some("mdbx")) + .flag("slasher-max-db-size", Some("1")) + .flag("slasher-backend", Some("lmdb")) .run_with_zero_port() .with_config(|config| { let slasher_config = config.slasher.as_ref().unwrap(); - assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Mdbx); + assert_eq!(slasher_config.backend, slasher::DatabaseBackend::Lmdb); }); } @@ -2181,6 +2337,18 @@ fn gui_flag() { }); } +#[test] +fn multiple_http_enabled_flags() { + CommandLineTest::new() + .flag("gui", None) + .flag("http", None) + .flag("staking", None) + .run_with_zero_port() + .with_config(|config| { + assert!(config.http_api.enabled); + }); +} + #[test] fn optimistic_finalized_sync_default() { CommandLineTest::new() @@ -2220,3 +2388,126 @@ fn invalid_gossip_verified_blocks_path() { ) }); } + +#[test] +fn progressive_balances_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.progressive_balances_mode, + ProgressiveBalancesMode::Checked + ) + }); +} + +#[test] +fn progressive_balances_fast() { + CommandLineTest::new() + .flag("progressive-balances", Some("fast")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.progressive_balances_mode, + ProgressiveBalancesMode::Fast + ) + }); +} + +#[test] +fn beacon_processor() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert_eq!(config.beacon_processor, <_>::default())); + + CommandLineTest::new() + .flag("beacon-processor-max-workers", Some("1")) + .flag("beacon-processor-work-queue-len", Some("2")) + .flag("beacon-processor-reprocess-queue-len", Some("3")) + .flag("beacon-processor-attestation-batch-size", Some("4")) + .flag("beacon-processor-aggregate-batch-size", Some("5")) + .flag("disable-backfill-rate-limiting", None) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.beacon_processor, + BeaconProcessorConfig { + max_workers: 1, + max_work_event_queue_len: 2, + max_scheduled_work_queue_len: 3, + max_gossip_attestation_batch_size: 4, + max_gossip_aggregate_batch_size: 5, + enable_backfill_rate_limiting: false + } + ) + }); +} + +#[test] +#[should_panic] +fn beacon_processor_zero_workers() { + CommandLineTest::new() + .flag("beacon-processor-max-workers", Some("0")) + .run_with_zero_port(); +} + +#[test] +fn http_sse_capacity_multiplier_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.sse_capacity_multiplier, 1)); +} + +#[test] +fn http_sse_capacity_multiplier_override() { + CommandLineTest::new() + .flag("http", None) + .flag("http-sse-capacity-multiplier", Some("10")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.http_api.sse_capacity_multiplier, 10)); +} + +#[test] +fn http_duplicate_block_status_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.http_api.duplicate_block_status_code.as_u16(), 202) + }); +} + +#[test] +fn http_duplicate_block_status_override() { + CommandLineTest::new() + .flag("http", None) + .flag("http-duplicate-block-status", Some("301")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.http_api.duplicate_block_status_code.as_u16(), 301) + }); +} + +#[test] +fn genesis_state_url_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.genesis_state_url, None); + assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(180)); + }); +} + +#[test] +fn genesis_state_url_value() { + CommandLineTest::new() + .flag("genesis-state-url", Some("http://genesis.com")) + .flag("genesis-state-url-timeout", Some("42")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.genesis_state_url.as_deref(), + Some("http://genesis.com") + ); + assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(42)); + }); +} diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index 4dd5ad95d..659dea468 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -39,7 +39,7 @@ impl CommandLineTest { } fn run_with_ip(&mut self) -> CompletedTest<BootNodeConfigSerialization> { - self.cmd.arg(IP_ADDRESS); + self.cmd.arg("--enr-address").arg(IP_ADDRESS); self.run() } } @@ -67,7 +67,13 @@ fn port_flag() { .flag("port", Some(port.to_string().as_str())) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.port(), port); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .port(), + port + ); }) } @@ -78,7 +84,13 @@ fn listen_address_flag() { .flag("listen-address", Some("127.0.0.2")) .run_with_ip() .with_config(|config| { - assert_eq!(config.listen_socket.ip(), addr); + assert_eq!( + config + .ipv4_listen_socket + .expect("Bootnode should be listening on IPv4") + .ip(), + &addr + ); }); } diff --git a/lighthouse/tests/main.rs b/lighthouse/tests/main.rs index 806524cab..bf587f79d 100644 --- a/lighthouse/tests/main.rs +++ b/lighthouse/tests/main.rs @@ -5,3 +5,4 @@ mod beacon_node; mod boot_node; mod exec; mod validator_client; +mod validator_manager; diff --git a/lighthouse/tests/tls/cert.pem b/lighthouse/tests/tls/cert.pem new file mode 100644 index 000000000..03af12ff8 --- /dev/null +++ b/lighthouse/tests/tls/cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEADCCAmigAwIBAgICAcgwDQYJKoZIhvcNAQELBQAwLDEqMCgGA1UEAwwhcG9u +eXRvd24gUlNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE2MDgxMzE2MDcwNFoX +DTIyMDIwMzE2MDcwNFowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpVhh1/FNP2qvWenbZSghari/UThwe +dynfnHG7gc3JmygkEdErWBO/CHzHgsx7biVE5b8sZYNEDKFojyoPHGWK2bQM/FTy +niJCgNCLdn6hUqqxLAml3cxGW77hAWu94THDGB1qFe+eFiAUnDmob8gNZtAzT6Ky +b/JGJdrEU0wj+Rd7wUb4kpLInNH/Jc+oz2ii2AjNbGOZXnRz7h7Kv3sO9vABByYe +LcCj3qnhejHMqVhbAT1MD6zQ2+YKBjE52MsQKU/xhUpu9KkUyLh0cxkh3zrFiKh4 +Vuvtc+n7aeOv2jJmOl1dr0XLlSHBlmoKqH6dCTSbddQLmlK7dms8vE01AgMBAAGj +gb4wgbswDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCBsAwHQYDVR0OBBYEFMeUzGYV +bXwJNQVbY1+A8YXYZY8pMEIGA1UdIwQ7MDmAFJvEsUi7+D8vp8xcWvnEdVBGkpoW +oR6kHDAaMRgwFgYDVQQDDA9wb255dG93biBSU0EgQ0GCAXswOwYDVR0RBDQwMoIO +dGVzdHNlcnZlci5jb22CFXNlY29uZC50ZXN0c2VydmVyLmNvbYIJbG9jYWxob3N0 +MA0GCSqGSIb3DQEBCwUAA4IBgQBsk5ivAaRAcNgjc7LEiWXFkMg703AqDDNx7kB1 +RDgLalLvrjOfOp2jsDfST7N1tKLBSQ9bMw9X4Jve+j7XXRUthcwuoYTeeo+Cy0/T +1Q78ctoX74E2nB958zwmtRykGrgE/6JAJDwGcgpY9kBPycGxTlCN926uGxHsDwVs +98cL6ZXptMLTR6T2XP36dAJZuOICSqmCSbFR8knc/gjUO36rXTxhwci8iDbmEVaf +BHpgBXGU5+SQ+QM++v6bHGf4LNQC5NZ4e4xvGax8ioYu/BRsB/T3Lx+RlItz4zdU +XuxCNcm3nhQV2ZHquRdbSdoyIxV5kJXel4wCmOhWIq7A2OBKdu5fQzIAzzLi65EN +RPAKsKB4h7hGgvciZQ7dsMrlGw0DLdJ6UrFyiR5Io7dXYT/+JP91lP5xsl6Lhg9O +FgALt7GSYRm2cZdgi9pO9rRr83Br1VjQT1vHz6yoZMXSqc4A2zcN2a2ZVq//rHvc +FZygs8miAhWPzqnpmgTj1cPiU1M= +-----END CERTIFICATE----- diff --git a/lighthouse/tests/tls/key.rsa b/lighthouse/tests/tls/key.rsa new file mode 100644 index 000000000..b13bf5d07 --- /dev/null +++ b/lighthouse/tests/tls/key.rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqVYYdfxTT9qr1np22UoIWq4v1E4cHncp35xxu4HNyZsoJBHR +K1gTvwh8x4LMe24lROW/LGWDRAyhaI8qDxxlitm0DPxU8p4iQoDQi3Z+oVKqsSwJ +pd3MRlu+4QFrveExwxgdahXvnhYgFJw5qG/IDWbQM0+ism/yRiXaxFNMI/kXe8FG ++JKSyJzR/yXPqM9ootgIzWxjmV50c+4eyr97DvbwAQcmHi3Ao96p4XoxzKlYWwE9 +TA+s0NvmCgYxOdjLEClP8YVKbvSpFMi4dHMZId86xYioeFbr7XPp+2njr9oyZjpd +Xa9Fy5UhwZZqCqh+nQk0m3XUC5pSu3ZrPLxNNQIDAQABAoIBAFKtZJgGsK6md4vq +kyiYSufrcBLaaEQ/rkQtYCJKyC0NAlZKFLRy9oEpJbNLm4cQSkYPXn3Qunx5Jj2k +2MYz+SgIDy7f7KHgr52Ew020dzNQ52JFvBgt6NTZaqL1TKOS1fcJSSNIvouTBerK +NCSXHzfb4P+MfEVe/w1c4ilE+kH9SzdEo2jK/sRbzHIY8TX0JbmQ4SCLLayr22YG +usIxtIYcWt3MMP/G2luRnYzzBCje5MXdpAhlHLi4TB6x4h5PmBKYc57uOVNngKLd +YyrQKcszW4Nx5v0a4HG3A5EtUXNCco1+5asXOg2lYphQYVh2R+1wgu5WiDjDVu+6 +EYgjFSkCgYEA0NBk6FDoxE/4L/4iJ4zIhu9BptN8Je/uS5c6wRejNC/VqQyw7SHb +hRFNrXPvq5Y+2bI/DxtdzZLKAMXOMjDjj0XEgfOIn2aveOo3uE7zf1i+njxwQhPu +uSYA9AlBZiKGr2PCYSDPnViHOspVJjxRuAgyWM1Qf+CTC0D95aj0oz8CgYEAz5n4 +Cb3/WfUHxMJLljJ7PlVmlQpF5Hk3AOR9+vtqTtdxRjuxW6DH2uAHBDdC3OgppUN4 +CFj55kzc2HUuiHtmPtx8mK6G+otT7Lww+nLSFL4PvZ6CYxqcio5MPnoYd+pCxrXY +JFo2W7e4FkBOxb5PF5So5plg+d0z/QiA7aFP1osCgYEAtgi1rwC5qkm8prn4tFm6 +hkcVCIXc+IWNS0Bu693bXKdGr7RsmIynff1zpf4ntYGpEMaeymClCY0ppDrMYlzU +RBYiFNdlBvDRj6s/H+FTzHRk2DT/99rAhY9nzVY0OQFoQIXK8jlURGrkmI/CYy66 +XqBmo5t4zcHM7kaeEBOWEKkCgYAYnO6VaRtPNQfYwhhoFFAcUc+5t+AVeHGW/4AY +M5qlAlIBu64JaQSI5KqwS0T4H+ZgG6Gti68FKPO+DhaYQ9kZdtam23pRVhd7J8y+ +xMI3h1kiaBqZWVxZ6QkNFzizbui/2mtn0/JB6YQ/zxwHwcpqx0tHG8Qtm5ZAV7PB +eLCYhQKBgQDALJxU/6hMTdytEU5CLOBSMby45YD/RrfQrl2gl/vA0etPrto4RkVq +UrkDO/9W4mZORClN3knxEFSTlYi8YOboxdlynpFfhcs82wFChs+Ydp1eEsVHAqtu +T+uzn0sroycBiBfVB949LExnzGDFUkhG0i2c2InarQYLTsIyHCIDEA== +-----END RSA PRIVATE KEY----- diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 8c1f0477c..062b7e778 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -260,6 +260,7 @@ fn http_flag() { fn http_address_flag() { let addr = "127.0.0.99".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("http", None) .flag("http-address", Some("127.0.0.99")) .flag("unencrypted-http-transport", None) .run() @@ -269,6 +270,7 @@ fn http_address_flag() { fn http_address_ipv6_flag() { let addr = "::1".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("http", None) .flag("http-address", Some("::1")) .flag("unencrypted-http-transport", None) .run() @@ -279,6 +281,7 @@ fn http_address_ipv6_flag() { fn missing_unencrypted_http_transport_flag() { let addr = "127.0.0.99".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("http", None) .flag("http-address", Some("127.0.0.99")) .run() .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); @@ -286,6 +289,7 @@ fn missing_unencrypted_http_transport_flag() { #[test] fn http_port_flag() { CommandLineTest::new() + .flag("http", None) .flag("http-port", Some("9090")) .run() .with_config(|config| assert_eq!(config.http_api.listen_port, 9090)); @@ -293,6 +297,7 @@ fn http_port_flag() { #[test] fn http_allow_origin_flag() { CommandLineTest::new() + .flag("http", None) .flag("http-allow-origin", Some("http://localhost:9009")) .run() .with_config(|config| { @@ -305,10 +310,41 @@ fn http_allow_origin_flag() { #[test] fn http_allow_origin_all_flag() { CommandLineTest::new() + .flag("http", None) .flag("http-allow-origin", Some("*")) .run() .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); } +#[test] +fn http_allow_keystore_export_default() { + CommandLineTest::new() + .flag("http", None) + .run() + .with_config(|config| assert!(!config.http_api.allow_keystore_export)); +} +#[test] +fn http_allow_keystore_export_present() { + CommandLineTest::new() + .flag("http", None) + .flag("http-allow-keystore-export", None) + .run() + .with_config(|config| assert!(config.http_api.allow_keystore_export)); +} +#[test] +fn http_store_keystore_passwords_in_secrets_dir_default() { + CommandLineTest::new() + .flag("http", None) + .run() + .with_config(|config| assert!(!config.http_api.store_passwords_in_secrets_dir)); +} +#[test] +fn http_store_keystore_passwords_in_secrets_dir_present() { + CommandLineTest::new() + .flag("http", None) + .flag("http-store-passwords-in-secrets-dir", None) + .run() + .with_config(|config| assert!(config.http_api.store_passwords_in_secrets_dir)); +} // Tests for Metrics flags. #[test] @@ -322,6 +358,7 @@ fn metrics_flag() { fn metrics_address_flag() { let addr = "127.0.0.99".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("metrics", None) .flag("metrics-address", Some("127.0.0.99")) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); @@ -330,6 +367,7 @@ fn metrics_address_flag() { fn metrics_address_ipv6_flag() { let addr = "::1".parse::<IpAddr>().unwrap(); CommandLineTest::new() + .flag("metrics", None) .flag("metrics-address", Some("::1")) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); @@ -337,6 +375,7 @@ fn metrics_address_ipv6_flag() { #[test] fn metrics_port_flag() { CommandLineTest::new() + .flag("metrics", None) .flag("metrics-port", Some("9090")) .run() .with_config(|config| assert_eq!(config.http_metrics.listen_port, 9090)); @@ -344,6 +383,7 @@ fn metrics_port_flag() { #[test] fn metrics_allow_origin_flag() { CommandLineTest::new() + .flag("metrics", None) .flag("metrics-allow-origin", Some("http://localhost:9009")) .run() .with_config(|config| { @@ -356,6 +396,7 @@ fn metrics_allow_origin_flag() { #[test] fn metrics_allow_origin_all_flag() { CommandLineTest::new() + .flag("metrics", None) .flag("metrics-allow-origin", Some("*")) .run() .with_config(|config| assert_eq!(config.http_metrics.allow_origin, Some("*".to_string()))); @@ -499,3 +540,24 @@ fn latency_measurement_service() { assert!(!config.enable_latency_measurement_service); }); } + +#[test] +fn validator_registration_batch_size() { + CommandLineTest::new().run().with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 500); + }); + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("100")) + .run() + .with_config(|config| { + assert_eq!(config.validator_registration_batch_size, 100); + }); +} + +#[test] +#[should_panic] +fn validator_registration_batch_size_zero_value() { + CommandLineTest::new() + .flag("validator-registration-batch-size", Some("0")) + .run(); +} diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs new file mode 100644 index 000000000..e0a1e92d6 --- /dev/null +++ b/lighthouse/tests/validator_manager.rs @@ -0,0 +1,344 @@ +use eth2::SensitiveUrl; +use serde::de::DeserializeOwned; +use std::fs; +use std::marker::PhantomData; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::str::FromStr; +use tempfile::{tempdir, TempDir}; +use types::*; +use validator_manager::{ + create_validators::CreateConfig, + import_validators::ImportConfig, + move_validators::{MoveConfig, PasswordSource, Validators}, +}; + +const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; + +const EXAMPLE_PUBKEY_0: &str = "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"; +const EXAMPLE_PUBKEY_1: &str = "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c"; + +struct CommandLineTest<T> { + cmd: Command, + config_path: PathBuf, + _dir: TempDir, + _phantom: PhantomData<T>, +} + +impl<T> Default for CommandLineTest<T> { + fn default() -> Self { + let dir = tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_lighthouse")); + cmd.arg("--dump-config") + .arg(config_path.as_os_str()) + .arg("validator-manager") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + Self { + cmd, + config_path, + _dir: dir, + _phantom: PhantomData, + } + } +} + +impl<T> CommandLineTest<T> { + fn flag(mut self, flag: &str, value: Option<&str>) -> Self { + self.cmd.arg(flag); + if let Some(value) = value { + self.cmd.arg(value); + } + self + } + + fn run(mut cmd: Command, should_succeed: bool) { + let output = cmd.output().expect("process should complete"); + if output.status.success() != should_succeed { + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + eprintln!("{}", stdout); + eprintln!("{}", stderr); + panic!( + "Command success was {} when expecting {}", + !should_succeed, should_succeed + ); + } + } +} + +impl<T: DeserializeOwned> CommandLineTest<T> { + fn assert_success<F: Fn(T)>(self, func: F) { + Self::run(self.cmd, true); + let contents = fs::read_to_string(self.config_path).unwrap(); + let config: T = serde_json::from_str(&contents).unwrap(); + func(config) + } + + fn assert_failed(self) { + Self::run(self.cmd, false); + } +} + +impl CommandLineTest<CreateConfig> { + fn validators_create() -> Self { + Self::default().flag("create", None) + } +} + +impl CommandLineTest<ImportConfig> { + fn validators_import() -> Self { + Self::default().flag("import", None) + } +} + +impl CommandLineTest<MoveConfig> { + fn validators_move() -> Self { + Self::default().flag("move", None) + } +} + +#[test] +pub fn validator_create_without_output_path() { + CommandLineTest::validators_create().assert_failed(); +} + +#[test] +pub fn validator_create_defaults() { + CommandLineTest::validators_create() + .flag("--output-path", Some("./meow")) + .flag("--count", Some("1")) + .assert_success(|config| { + let expected = CreateConfig { + output_path: PathBuf::from("./meow"), + first_index: 0, + count: 1, + deposit_gwei: MainnetEthSpec::default_spec().max_effective_balance, + mnemonic_path: None, + stdin_inputs: cfg!(windows) || false, + disable_deposits: false, + specify_voting_keystore_password: false, + eth1_withdrawal_address: None, + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + bn_url: None, + force_bls_withdrawal_credentials: false, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_create_misc_flags() { + CommandLineTest::validators_create() + .flag("--output-path", Some("./meow")) + .flag("--deposit-gwei", Some("42")) + .flag("--first-index", Some("12")) + .flag("--count", Some("9")) + .flag("--mnemonic-path", Some("./woof")) + .flag("--stdin-inputs", None) + .flag("--specify-voting-keystore-password", None) + .flag("--eth1-withdrawal-address", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--builder-proposals", Some("true")) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--force-bls-withdrawal-credentials", None) + .assert_success(|config| { + let expected = CreateConfig { + output_path: PathBuf::from("./meow"), + first_index: 12, + count: 9, + deposit_gwei: 42, + mnemonic_path: Some(PathBuf::from("./woof")), + stdin_inputs: true, + disable_deposits: false, + specify_voting_keystore_password: true, + eth1_withdrawal_address: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + builder_proposals: Some(true), + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + gas_limit: Some(1337), + bn_url: Some(SensitiveUrl::parse("http://localhost:1001").unwrap()), + force_bls_withdrawal_credentials: true, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_create_disable_deposits() { + CommandLineTest::validators_create() + .flag("--output-path", Some("./meow")) + .flag("--count", Some("1")) + .flag("--disable-deposits", None) + .flag("--builder-proposals", Some("false")) + .assert_success(|config| { + assert_eq!(config.disable_deposits, true); + assert_eq!(config.builder_proposals, Some(false)); + }); +} + +#[test] +pub fn validator_import_defaults() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--vc-token", Some("./token.json")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: PathBuf::from("./vals.json"), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_import_misc_flags() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--vc-token", Some("./token.json")) + .flag("--ignore-duplicates", None) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: PathBuf::from("./vals.json"), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: true, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_import_missing_token() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .assert_failed(); +} + +#[test] +pub fn validator_import_missing_validators_file() { + CommandLineTest::validators_import() + .flag("--vc-token", Some("./token.json")) + .assert_failed(); +} + +#[test] +pub fn validator_move_defaults() { + CommandLineTest::validators_move() + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) + .flag("--validators", Some("all")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::All, + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + password_source: PasswordSource::Interactive { + stdin_inputs: cfg!(windows) || false, + }, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_move_misc_flags_0() { + CommandLineTest::validators_move() + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--builder-proposals", Some("true")) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .flag("--stdin-inputs", None) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + 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_1).unwrap(), + ]), + builder_proposals: Some(true), + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + gas_limit: Some(1337), + password_source: PasswordSource::Interactive { stdin_inputs: true }, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_move_misc_flags_1() { + CommandLineTest::validators_move() + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) + .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--builder-proposals", Some("false")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + 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() + ]), + builder_proposals: Some(false), + fee_recipient: None, + gas_limit: None, + password_source: PasswordSource::Interactive { + stdin_inputs: cfg!(windows) || false, + }, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_move_count() { + CommandLineTest::validators_move() + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) + .flag("--count", Some("42")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::Count(42), + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + password_source: PasswordSource::Interactive { + stdin_inputs: cfg!(windows) || false, + }, + }; + assert_eq!(expected, config); + }); +} diff --git a/scripts/ci/publish.sh b/scripts/ci/publish.sh deleted file mode 100755 index f2cea95b7..000000000 --- a/scripts/ci/publish.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash - -# Based on: https://github.com/tokio-rs/tokio/blob/master/bin/publish - -set -e -USAGE="Publish a new release of a lighthouse crate -USAGE: - $(basename "$0") [OPTIONS] [CRATE_PATH] [CRATE] [TAG_NAME] -OPTIONS: - -v, --verbose Use verbose Cargo output - -d, --dry-run Perform a dry run (do not publish the release) - -h, --help Show this help text and exit - --allow-dirty Allow dirty working directories to be packaged" - -DRY_RUN="" -DIRTY="" -VERBOSE="" - -verify() { - echo "Verifying if $CRATE v$VERSION can be released" - - # `cargo pkgid` has different formats based on whether the `[lib]` name and `[package]` name - # are the same, necessitating the following logic. - # - # Try to match on `#` - ACTUAL=$(cargo pkgid | sed -n 's/.*#\([0-9]\)/\1/p' ) - if [ -z "$ACTUAL" ]; then - # Match on the final `:` - ACTUAL=$(cargo pkgid | sed -n 's/.*:\(.*\)/\1/p') - fi - - if [ "$ACTUAL" != "$VERSION" ]; then - echo "expected to release version $VERSION, but Cargo.toml contained $ACTUAL" - exit 1 - fi -} - -release() { - echo "Releasing $CRATE v$VERSION" - cargo package $VERBOSE $DIRTY - cargo publish $VERBOSE $DRY_RUN $DIRTY -} - -while [[ $# -gt 0 ]] -do - -case "$1" in - -h|--help) - echo "$USAGE" - exit 0 - ;; - -v|--verbose) - VERBOSE="--verbose" - set +x - shift - ;; - --allow-dirty) - DIRTY="--allow-dirty" - shift - ;; - -d|--dry-run) - DRY_RUN="--dry-run" - shift - ;; - -*) - echo "unknown flag \"$1\"" - echo "$USAGE" - exit 1 - ;; - *) # crate, crate path, or version - if [ -z "$CRATE_PATH" ]; then - CRATE_PATH="$1" - elif [ -z "$CRATE" ]; then - CRATE="$1" - elif [ -z "$TAG_NAME" ]; then - TAG_NAME="$1" - VERSION=$(sed -e 's#.*-v\([0-9]\)#\1#' <<< "$TAG_NAME") - else - echo "unknown positional argument \"$1\"" - echo "$USAGE" - exit 1 - fi - shift - ;; -esac -done -# set -- "${POSITIONAL[@]}" - -if [ -z "$VERSION" ]; then - echo "no version specified!" - HELP=1 -fi - -if [ -z "$CRATE" ]; then - echo "no crate specified!" - HELP=1 -fi - -if [ -n "$HELP" ]; then - echo "$USAGE" - exit 1 -fi - -if [ -d "$CRATE_PATH" ]; then - (cd "$CRATE_PATH" && verify && release ) -else - echo "no such dir \"$CRATE_PATH\"" - exit 1 -fi diff --git a/scripts/cross/Dockerfile b/scripts/cross/Dockerfile deleted file mode 100644 index 5472b980b..000000000 --- a/scripts/cross/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -ARG CROSS_BASE_IMAGE -FROM $CROSS_BASE_IMAGE - -RUN apt-get update -y && apt-get upgrade -y - -RUN apt-get install -y unzip && \ - PB_REL="https://github.com/protocolbuffers/protobuf/releases" && \ - curl -L $PB_REL/download/v3.15.8/protoc-3.15.8-linux-x86_64.zip -o protoc.zip && \ - unzip protoc.zip -d /usr && \ - chmod +x /usr/bin/protoc - -RUN apt-get install -y cmake clang-3.9 - -ENV PROTOC=/usr/bin/protoc diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index 1a04d12d4..70a36614c 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -39,10 +39,11 @@ done # Get positional arguments data_dir=${@:$OPTIND+0:1} -network_port=${@:$OPTIND+1:1} -http_port=${@:$OPTIND+2:1} -execution_endpoint=${@:$OPTIND+3:1} -execution_jwt=${@:$OPTIND+4:1} +tcp_port=${@:$OPTIND+1:1} +quic_port=${@:$OPTIND+2:1} +http_port=${@:$OPTIND+3:1} +execution_endpoint=${@:$OPTIND+4:1} +execution_jwt=${@:$OPTIND+5:1} lighthouse_binary=lighthouse @@ -56,9 +57,11 @@ exec $lighthouse_binary \ --disable-peer-scoring \ --staking \ --enr-address 127.0.0.1 \ - --enr-udp-port $network_port \ - --enr-tcp-port $network_port \ - --port $network_port \ + --enr-udp-port $tcp_port \ + --enr-tcp-port $tcp_port \ + --enr-quic-port $quic_port \ + --port $tcp_port \ + --quic-port $quic_port \ --http-port $http_port \ --disable-packet-filter \ --target-peers $((BN_COUNT - 1)) \ diff --git a/scripts/local_testnet/clean.sh b/scripts/local_testnet/clean.sh index 6db8753d0..cd915e470 100755 --- a/scripts/local_testnet/clean.sh +++ b/scripts/local_testnet/clean.sh @@ -9,5 +9,5 @@ set -Eeuo pipefail source ./vars.env if [ -d $DATADIR ]; then - rm -r $DATADIR + rm -rf $DATADIR fi diff --git a/scripts/local_testnet/genesis.json b/scripts/local_testnet/genesis.json index f92a5f5d0..3ac553e55 100644 --- a/scripts/local_testnet/genesis.json +++ b/scripts/local_testnet/genesis.json @@ -13,7 +13,8 @@ "londonBlock": 0, "mergeNetsplitBlock": 0, "shanghaiTime": 0, - "terminalTotalDifficulty": 0 + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true }, "alloc": { "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { diff --git a/scripts/local_testnet/setup.sh b/scripts/local_testnet/setup.sh index 283aa0c02..e026ba1c0 100755 --- a/scripts/local_testnet/setup.sh +++ b/scripts/local_testnet/setup.sh @@ -46,6 +46,6 @@ lcli \ insecure-validators \ --count $VALIDATOR_COUNT \ --base-dir $DATADIR \ - --node-count $BN_COUNT + --node-count $VC_COUNT echo Validators generated with keystore passwords at $DATADIR. diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 64111d562..fdf9ae172 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -103,7 +103,7 @@ echo "executing: ./setup.sh >> $LOG_DIR/setup.log" ./setup.sh >> $LOG_DIR/setup.log 2>&1 # Update future hardforks time in the EL genesis file based on the CL genesis time -GENESIS_TIME=$(lcli pretty-ssz state_merge $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d') +GENESIS_TIME=$(lcli pretty-ssz --spec $SPEC_PRESET --testnet-dir $TESTNET_DIR BeaconState $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d') echo $GENESIS_TIME CAPELLA_TIME=$((GENESIS_TIME + (CAPELLA_FORK_EPOCH * 32 * SECONDS_PER_SLOT))) echo $CAPELLA_TIME @@ -139,7 +139,7 @@ sed -i 's/"shanghaiTime".*$/"shanghaiTime": 0,/g' $genesis_file for (( bn=1; bn<=$BN_COUNT; bn++ )); do secret=$DATADIR/geth_datadir$bn/geth/jwtsecret echo $secret - execute_command_add_PID beacon_node_$bn.log ./beacon_node.sh $SAS -d $DEBUG_LEVEL $DATADIR/node_$bn $((BN_udp_tcp_base + $bn)) $((BN_http_port_base + $bn)) http://localhost:$((EL_base_auth_http + $bn)) $secret + execute_command_add_PID beacon_node_$bn.log ./beacon_node.sh $SAS -d $DEBUG_LEVEL $DATADIR/node_$bn $((BN_udp_tcp_base + $bn)) $((BN_udp_tcp_base + $bn + 100)) $((BN_http_port_base + $bn)) http://localhost:$((EL_base_auth_http + $bn)) $secret done # Start requested number of validator clients diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index e9d3e39ce..1eefa7cf5 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -43,23 +43,23 @@ sleep 10 echo "Starting local execution nodes" -exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir1 7000 6000 5000 $genesis_file &> geth.log & -exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir2 7100 6100 5100 $genesis_file &> /dev/null & -exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir3 7200 6200 5200 $genesis_file &> /dev/null & +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir1 6000 5000 4000 $genesis_file &> geth.log & +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir2 6100 5100 4100 $genesis_file &> /dev/null & +exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir3 6200 5200 4200 $genesis_file &> /dev/null & sleep 20 echo "Starting local beacon nodes" -exit_if_fails ../local_testnet/beacon_node.sh -d debug $HOME/.lighthouse/local-testnet/node_1 9000 8000 http://localhost:5000 $HOME/.lighthouse/local-testnet/geth_datadir1/geth/jwtsecret &> beacon1.log & -exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 9100 8100 http://localhost:5100 $HOME/.lighthouse/local-testnet/geth_datadir2/geth/jwtsecret &> /dev/null & -exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_3 9200 8200 http://localhost:5200 $HOME/.lighthouse/local-testnet/geth_datadir3/geth/jwtsecret &> /dev/null & +exit_if_fails ../local_testnet/beacon_node.sh -d debug $HOME/.lighthouse/local-testnet/node_1 8000 7000 9000 http://localhost:4000 $HOME/.lighthouse/local-testnet/geth_datadir1/geth/jwtsecret &> /dev/null & +exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 8100 7100 9100 http://localhost:4100 $HOME/.lighthouse/local-testnet/geth_datadir2/geth/jwtsecret &> /dev/null & +exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_3 8200 7200 9200 http://localhost:4200 $HOME/.lighthouse/local-testnet/geth_datadir3/geth/jwtsecret &> /dev/null & echo "Starting local validator clients" -exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:8000 &> /dev/null & -exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_2 http://localhost:8100 &> /dev/null & -exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_3 http://localhost:8200 &> /dev/null & +exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:9000 &> /dev/null & +exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_2 http://localhost:9100 &> /dev/null & +exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_3 http://localhost:9200 &> /dev/null & echo "Waiting an epoch before starting the next validator client" sleep $(( $SECONDS_PER_SLOT * 32 )) @@ -70,7 +70,7 @@ if [[ "$BEHAVIOR" == "failure" ]]; then # Use same keys as keys from VC1 and connect to BN2 # This process should not last longer than 2 epochs - timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1_doppelganger http://localhost:8100 + timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1_doppelganger http://localhost:9100 DOPPELGANGER_EXIT=$? echo "Shutting down" @@ -96,7 +96,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then echo "Starting the last validator client" - ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_4 http://localhost:8100 & + ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_4 http://localhost:9100 & DOPPELGANGER_FAILURE=0 # Sleep three epochs, then make sure all validators were active in epoch 2. Use @@ -110,7 +110,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then cd $HOME/.lighthouse/local-testnet/node_4/validators for val in 0x*; do [[ -e $val ]] || continue - curl -s localhost:8100/lighthouse/validator_inclusion/3/$val | jq | grep -q '"is_previous_epoch_target_attester": false' + curl -s localhost:9100/lighthouse/validator_inclusion/3/$val | jq | grep -q '"is_previous_epoch_target_attester": false' IS_ATTESTER=$? if [[ $IS_ATTESTER -eq 0 ]]; then echo "$val did not attest in epoch 2." @@ -128,7 +128,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then sleep $(( $SECONDS_PER_SLOT * 32 * 2 )) for val in 0x*; do [[ -e $val ]] || continue - curl -s localhost:8100/lighthouse/validator_inclusion/5/$val | jq | grep -q '"is_previous_epoch_target_attester": true' + curl -s localhost:9100/lighthouse/validator_inclusion/5/$val | jq | grep -q '"is_previous_epoch_target_attester": true' IS_ATTESTER=$? if [[ $IS_ATTESTER -eq 0 ]]; then echo "$val attested in epoch 4." @@ -154,4 +154,4 @@ if [[ "$BEHAVIOR" == "success" ]]; then fi fi -exit 0 \ No newline at end of file +exit 0 diff --git a/scripts/tests/genesis.json b/scripts/tests/genesis.json index 985bb9cef..ec3cd1e81 100644 --- a/scripts/tests/genesis.json +++ b/scripts/tests/genesis.json @@ -12,7 +12,8 @@ "berlinBlock": 0, "londonBlock": 0, "mergeForkBlock": 0, - "terminalTotalDifficulty": 0 + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true }, "alloc": { "0x0000000000000000000000000000000000000000": { diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 7f2ac456b..9df77daa1 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -2,34 +2,34 @@ name = "slasher" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [features] -default = ["mdbx"] +default = ["lmdb"] mdbx = ["dep:mdbx"] lmdb = ["lmdb-rkv", "lmdb-rkv-sys"] [dependencies] -bincode = "1.3.1" -byteorder = "1.3.4" -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" +bincode = { workspace = true } +byteorder = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } -lazy_static = "1.4.0" -lighthouse_metrics = { path = "../common/lighthouse_metrics" } -filesystem = { path = "../common/filesystem" } -lru = "0.7.1" -parking_lot = "0.12.0" -rand = "0.8.5" -safe_arith = { path = "../consensus/safe_arith" } -serde = "1.0" +lazy_static = { workspace = true } +lighthouse_metrics = { workspace = true } +filesystem = { workspace = true } +lru = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } serde_derive = "1.0" -slog = "2.5.2" -sloggers = { version = "2.1.1", features = ["json"] } -tree_hash = "0.5.0" -tree_hash_derive = "0.5.0" -types = { path = "../consensus/types" } -strum = { version = "0.24.1", features = ["derive"] } +slog = { workspace = true } +sloggers = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +types = { workspace = true } +strum = { workspace = true } # MDBX is pinned at the last version with Windows and macOS support. mdbx = { package = "libmdbx", git = "https://github.com/sigp/libmdbx-rs", tag = "v0.1.4", optional = true } @@ -37,8 +37,8 @@ lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b9426531 lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } [dev-dependencies] -maplit = "1.0.2" -rayon = "1.3.0" -tempfile = "3.1.0" -logging = { path = "../common/logging" } +maplit = { workspace = true } +rayon = { workspace = true } +tempfile = { workspace = true } +logging = { workspace = true } diff --git a/slasher/service/Cargo.toml b/slasher/service/Cargo.toml index 0a787defa..41e3b5b90 100644 --- a/slasher/service/Cargo.toml +++ b/slasher/service/Cargo.toml @@ -2,17 +2,17 @@ name = "slasher_service" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>"] -edition = "2021" +edition = { workspace = true } [dependencies] -beacon_chain = { path = "../../beacon_node/beacon_chain" } -directory = { path = "../../common/directory" } -lighthouse_network = { path = "../../beacon_node/lighthouse_network" } -network = { path = "../../beacon_node/network" } -slasher = { path = "..", default-features = false } -slog = "2.5.2" -slot_clock = { path = "../../common/slot_clock" } -state_processing = { path = "../../consensus/state_processing" } -task_executor = { path = "../../common/task_executor" } -tokio = { version = "1.14.0", features = ["full"] } -types = { path = "../../consensus/types" } +beacon_chain = { workspace = true } +directory = { workspace = true } +lighthouse_network = { workspace = true } +network = { workspace = true } +slasher = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +state_processing = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/slasher/src/config.rs b/slasher/src/config.rs index e2a58a406..361621d17 100644 --- a/slasher/src/config.rs +++ b/slasher/src/config.rs @@ -13,15 +13,16 @@ pub const DEFAULT_MAX_DB_SIZE: usize = 256 * 1024; // 256 GiB pub const DEFAULT_ATTESTATION_ROOT_CACHE_SIZE: usize = 100_000; pub const DEFAULT_BROADCAST: bool = false; -#[cfg(feature = "mdbx")] +#[cfg(all(feature = "mdbx", not(feature = "lmdb")))] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Mdbx; -#[cfg(all(feature = "lmdb", not(feature = "mdbx")))] +#[cfg(feature = "lmdb")] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Lmdb; #[cfg(not(any(feature = "mdbx", feature = "lmdb")))] pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Disabled; pub const MAX_HISTORY_LENGTH: usize = 1 << 16; pub const MEGABYTE: usize = 1 << 20; +pub const MDBX_DATA_FILENAME: &str = "mdbx.dat"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { @@ -64,6 +65,13 @@ pub enum DatabaseBackend { Disabled, } +#[derive(Debug, PartialEq)] +pub enum DatabaseBackendOverride { + Success(DatabaseBackend), + Failure(PathBuf), + Noop, +} + impl Config { pub fn new(database_path: PathBuf) -> Self { Self { @@ -161,4 +169,28 @@ impl Config { .filter(move |v| self.validator_chunk_index(**v) == validator_chunk_index) .copied() } + + pub fn override_backend(&mut self) -> DatabaseBackendOverride { + let mdbx_path = self.database_path.join(MDBX_DATA_FILENAME); + + #[cfg(feature = "mdbx")] + let already_mdbx = self.backend == DatabaseBackend::Mdbx; + #[cfg(not(feature = "mdbx"))] + let already_mdbx = false; + + if !already_mdbx && mdbx_path.exists() { + #[cfg(feature = "mdbx")] + { + let old_backend = self.backend; + self.backend = DatabaseBackend::Mdbx; + DatabaseBackendOverride::Success(old_backend) + } + #[cfg(not(feature = "mdbx"))] + { + DatabaseBackendOverride::Failure(mdbx_path) + } + } else { + DatabaseBackendOverride::Noop + } + } } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 132ce8b23..45cbef84f 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -21,7 +21,7 @@ pub use crate::slasher::Slasher; pub use attestation_queue::{AttestationBatch, AttestationQueue, SimpleBatch}; pub use attester_record::{AttesterRecord, CompactAttesterRecord, IndexedAttesterRecord}; pub use block_queue::BlockQueue; -pub use config::{Config, DatabaseBackend}; +pub use config::{Config, DatabaseBackend, DatabaseBackendOverride}; pub use database::{ interface::{Database, Environment, RwTransaction}, IndexedAttestationId, SlasherDB, diff --git a/slasher/tests/backend.rs b/slasher/tests/backend.rs new file mode 100644 index 000000000..fd1a6ae14 --- /dev/null +++ b/slasher/tests/backend.rs @@ -0,0 +1,57 @@ +#![cfg(feature = "lmdb")] + +use slasher::{config::MDBX_DATA_FILENAME, Config, DatabaseBackend, DatabaseBackendOverride}; +use std::fs::File; +use tempfile::tempdir; + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn override_no_existing_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + assert_eq!(config.override_backend(), DatabaseBackendOverride::Noop); +} + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + + File::create(config.database_path.join(MDBX_DATA_FILENAME)).unwrap(); + + assert_eq!( + config.override_backend(), + DatabaseBackendOverride::Success(DatabaseBackend::Lmdb) + ); + assert_eq!(config.backend, DatabaseBackend::Mdbx); +} + +#[test] +#[cfg(all(feature = "mdbx", feature = "lmdb"))] +fn no_override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + config.backend = DatabaseBackend::Mdbx; + + File::create(config.database_path.join(MDBX_DATA_FILENAME)).unwrap(); + + assert_eq!(config.override_backend(), DatabaseBackendOverride::Noop); + assert_eq!(config.backend, DatabaseBackend::Mdbx); +} + +#[test] +#[cfg(all(not(feature = "mdbx"), feature = "lmdb"))] +fn failed_override_with_existing_mdbx_db() { + let tempdir = tempdir().unwrap(); + let mut config = Config::new(tempdir.path().into()); + + let filename = config.database_path.join(MDBX_DATA_FILENAME); + File::create(&filename).unwrap(); + + assert_eq!( + config.override_backend(), + DatabaseBackendOverride::Failure(filename) + ); + assert_eq!(config.backend, DatabaseBackend::Lmdb); +} diff --git a/testing/antithesis/Dockerfile.libvoidstar b/testing/antithesis/Dockerfile.libvoidstar deleted file mode 100644 index ddc49e13c..000000000 --- a/testing/antithesis/Dockerfile.libvoidstar +++ /dev/null @@ -1,25 +0,0 @@ -FROM rust:1.68.2-bullseye AS builder -RUN apt-get update && apt-get -y upgrade && apt-get install -y cmake libclang-dev protobuf-compiler -COPY . lighthouse - -# Build lighthouse directly with a cargo build command, bypassing the Makefile. -RUN cd lighthouse && LD_LIBRARY_PATH=/lighthouse/testing/antithesis/libvoidstar/ RUSTFLAGS="-Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=3 -Cllvm-args=-sanitizer-coverage-trace-pc-guard -Ccodegen-units=1 -Cdebuginfo=2 -L/lighthouse/testing/antithesis/libvoidstar/ -lvoidstar" cargo build --release --manifest-path lighthouse/Cargo.toml --target x86_64-unknown-linux-gnu --features modern --verbose --bin lighthouse -# build lcli binary directly with cargo install command, bypassing the makefile -RUN cargo install --path /lighthouse/lcli --force --locked - -FROM ubuntu:latest -RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \ - libssl-dev \ - ca-certificates \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# create and move the libvoidstar file -RUN mkdir libvoidstar -COPY --from=builder /lighthouse/testing/antithesis/libvoidstar/libvoidstar.so /usr/lib/libvoidstar.so - -# set the env variable to avoid having to always set it -ENV LD_LIBRARY_PATH=/usr/lib -# move the lighthouse binary and lcli binary -COPY --from=builder /lighthouse/target/x86_64-unknown-linux-gnu/release/lighthouse /usr/local/bin/lighthouse -COPY --from=builder /lighthouse/target/release/lcli /usr/local/bin/lcli diff --git a/testing/antithesis/libvoidstar/libvoidstar.so b/testing/antithesis/libvoidstar/libvoidstar.so deleted file mode 100644 index 0f8a0f23c..000000000 Binary files a/testing/antithesis/libvoidstar/libvoidstar.so and /dev/null differ diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 11283052f..16fe22504 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -2,7 +2,7 @@ name = "ef_tests" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [features] # `ef_tests` feature must be enabled to actually run the tests @@ -11,28 +11,28 @@ milagro = ["bls/milagro"] fake_crypto = ["bls/fake_crypto"] [dependencies] -bls = { path = "../../crypto/bls", default-features = false } -compare_fields = { path = "../../common/compare_fields" } -compare_fields_derive = { path = "../../common/compare_fields_derive" } -derivative = "2.1.1" -ethereum-types = "0.14.1" -hex = "0.4.2" -rayon = "1.4.1" -serde = "1.0.116" +bls = { workspace = true } +compare_fields = { workspace = true } +compare_fields_derive = { workspace = true } +derivative = { workspace = true } +ethereum-types = { workspace = true } +hex = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -serde_repr = "0.1.6" -serde_yaml = "0.8.13" -ethereum_ssz = "0.5.0" -ethereum_ssz_derive = "0.5.0" -tree_hash = "0.5.0" -tree_hash_derive = "0.5.0" -cached_tree_hash = { path = "../../consensus/cached_tree_hash" } -state_processing = { path = "../../consensus/state_processing" } -swap_or_not_shuffle = { path = "../../consensus/swap_or_not_shuffle" } -types = { path = "../../consensus/types" } -snap = "1.0.1" -fs2 = "0.4.3" -beacon_chain = { path = "../../beacon_node/beacon_chain" } -store = { path = "../../beacon_node/store" } -fork_choice = { path = "../../consensus/fork_choice" } -execution_layer = { path = "../../beacon_node/execution_layer" } +serde_repr = { workspace = true } +serde_yaml = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +cached_tree_hash = { workspace = true } +state_processing = { workspace = true } +swap_or_not_shuffle = { workspace = true } +types = { workspace = true } +snap = { workspace = true } +fs2 = { workspace = true } +beacon_chain = { workspace = true } +store = { workspace = true } +fork_choice = { workspace = true } +execution_layer = { workspace = true } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index f7562f477..81a1739eb 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -13,7 +13,7 @@ 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) -WGET := $(if $(LIGHTHOUSE_GITHUB_TOKEN),wget --header="Authorization: $(LIGHTHOUSE_GITHUB_TOKEN)",wget) +CURL := $(if $(LIGHTHOUSE_GITHUB_TOKEN),curl -L --header "Authorization: $(LIGHTHOUSE_GITHUB_TOKEN)",curl -L) all: make $(OUTPUT_DIR) @@ -27,11 +27,11 @@ $(OUTPUT_DIR): $(TARBALLS) $(BLS_OUTPUT_DIR): mkdir $(BLS_OUTPUT_DIR) - $(WGET) $(BLS_BASE_URL)/$(BLS_TEST).tar.gz -O $(BLS_TARBALL) + $(CURL) $(BLS_BASE_URL)/$(BLS_TEST).tar.gz -o $(BLS_TARBALL) tar -xzf $(BLS_TARBALL) -C $(BLS_OUTPUT_DIR) %-$(TESTS_TAG).tar.gz: - $(WGET) $(BASE_URL)/$*.tar.gz -O $@ + $(CURL) $(BASE_URL)/$*.tar.gz -o $@ clean-test-files: rm -rf $(OUTPUT_DIR) $(BLS_OUTPUT_DIR) diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index 6095e1be6..5e7118715 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -6,9 +6,9 @@ use crate::type_name; use crate::type_name::TypeName; use serde_derive::Deserialize; use state_processing::per_epoch_processing::capella::process_historical_summaries_update; +use state_processing::per_epoch_processing::effective_balance_updates::process_effective_balance_updates; use state_processing::per_epoch_processing::{ altair, base, - effective_balance_updates::process_effective_balance_updates, historical_roots_update::process_historical_roots_update, process_registry_updates, process_slashings, resets::{process_eth1_data_reset, process_randao_mixes_reset, process_slashings_reset}, @@ -120,7 +120,7 @@ impl<E: EthSpec> EpochTransition<E> for RewardsAndPenalties { BeaconState::Base(_) => { let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; validator_statuses.process_attestations(state)?; - base::process_rewards_and_penalties(state, &mut validator_statuses, spec) + base::process_rewards_and_penalties(state, &validator_statuses, spec) } BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => { altair::process_rewards_and_penalties( @@ -173,7 +173,7 @@ impl<E: EthSpec> EpochTransition<E> for Eth1DataReset { impl<E: EthSpec> EpochTransition<E> for EffectiveBalanceUpdates { fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - process_effective_balance_updates(state, spec) + process_effective_balance_updates(state, None, spec) } } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 4f5d99830..c4f288a8a 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -7,7 +7,7 @@ use beacon_chain::{ obtain_indexed_attestation_and_committees_per_slot, VerifiedAttestation, }, test_utils::{BeaconChainHarness, EphemeralHarnessType}, - BeaconChainTypes, CachedHead, CountUnrealized, NotifyExecutionLayer, + BeaconChainTypes, CachedHead, ChainConfig, NotifyExecutionLayer, }; use execution_layer::{json_structures::JsonPayloadStatusV1Status, PayloadStatusV1}; use serde::Deserialize; @@ -18,7 +18,8 @@ use std::sync::Arc; use std::time::Duration; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconState, Checkpoint, EthSpec, - ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, SignedBeaconBlock, Slot, Uint256, + ExecutionBlockHash, ForkName, Hash256, IndexedAttestation, ProgressiveBalancesMode, + SignedBeaconBlock, Slot, Uint256, }; #[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)] @@ -302,6 +303,10 @@ impl<E: EthSpec> Tester<E> { let harness = BeaconChainHarness::builder(E::default()) .spec(spec.clone()) .keypairs(vec![]) + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) .genesis_state_ephemeral_store(case.anchor_state.clone()) .mock_execution_layer() .recalculate_fork_times_with_genesis(0) @@ -381,8 +386,8 @@ impl<E: EthSpec> Tester<E> { let result = self.block_on_dangerous(self.harness.chain.process_block( block_root, block.clone(), - CountUnrealized::True, NotifyExecutionLayer::Yes, + || Ok(()), ))?; if result.is_ok() != valid { return Err(Error::DidntFail(format!( @@ -440,8 +445,9 @@ impl<E: EthSpec> Tester<E> { block_delay, &state, PayloadVerificationStatus::Irrelevant, + ProgressiveBalancesMode::Strict, &self.harness.chain.spec, - CountUnrealized::True, + self.harness.logger(), ); if result.is_ok() { diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 5fd00285a..21a56dcf2 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -4,6 +4,7 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use crate::testing_spec; use serde_derive::Deserialize; +use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::{ per_block_processing::{ errors::BlockProcessingError, @@ -96,6 +97,7 @@ impl<E: EthSpec> Operation<E> for Attestation<E> { spec, ), BeaconState::Altair(_) | BeaconState::Merge(_) | BeaconState::Capella(_) => { + initialize_progressive_balances_cache(state, None, spec)?; altair::process_attestation(state, self, 0, &mut ctxt, VerifySignatures::True, spec) } } @@ -118,6 +120,7 @@ impl<E: EthSpec> Operation<E> for AttesterSlashing<E> { _: &Operations<E, Self>, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); + initialize_progressive_balances_cache(state, None, spec)?; process_attester_slashings( state, &[self.clone()], @@ -168,6 +171,7 @@ impl<E: EthSpec> Operation<E> for ProposerSlashing { _: &Operations<E, Self>, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); + initialize_progressive_balances_cache(state, None, spec)?; process_proposer_slashings( state, &[self.clone()], diff --git a/testing/ef_tests/src/cases/rewards.rs b/testing/ef_tests/src/cases/rewards.rs index c59ceabe0..ee0fc265e 100644 --- a/testing/ef_tests/src/cases/rewards.rs +++ b/testing/ef_tests/src/cases/rewards.rs @@ -118,7 +118,7 @@ impl<E: EthSpec> Case for RewardsTest<E> { let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; validator_statuses.process_attestations(&state)?; - let deltas = base::rewards_and_penalties::get_attestation_deltas( + let deltas = base::rewards_and_penalties::get_attestation_deltas_all( &state, &validator_statuses, spec, diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index e51fed190..191b45c33 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -67,7 +67,7 @@ impl<E: EthSpec> Case for SanityBlocks<E> { let spec = &testing_spec::<E>(fork_name); // Processing requires the epoch cache. - bulk_state.build_all_caches(spec).unwrap(); + bulk_state.build_caches(spec).unwrap(); // Spawning a second state to call the VerifyIndiviual strategy to avoid bitrot. // See https://github.com/sigp/lighthouse/issues/742. diff --git a/testing/ef_tests/src/cases/sanity_slots.rs b/testing/ef_tests/src/cases/sanity_slots.rs index a38a8930a..dd385d13f 100644 --- a/testing/ef_tests/src/cases/sanity_slots.rs +++ b/testing/ef_tests/src/cases/sanity_slots.rs @@ -61,7 +61,7 @@ impl<E: EthSpec> Case for SanitySlots<E> { let spec = &testing_spec::<E>(fork_name); // Processing requires the epoch cache. - state.build_all_caches(spec).unwrap(); + state.build_caches(spec).unwrap(); let mut result = (0..self.slots) .try_for_each(|_| per_slot_processing(&mut state, None, spec).map(|_| ())) diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index 5c78c0902..c76ef9118 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -2,15 +2,15 @@ name = "eth1_test_rig" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -tokio = { version = "1.14.0", features = ["time"] } -ethers-core = "1.0.2" -ethers-providers = "1.0.2" +tokio = { workspace = true } +ethers-core = { workspace = true } +ethers-providers = { workspace = true } ethers-contract = "1.0.2" -types = { path = "../../consensus/types"} -serde_json = "1.0.58" -deposit_contract = { path = "../../common/deposit_contract"} -unused_port = { path = "../../common/unused_port" } -hex = "0.4.2" +types = { workspace = true } +serde_json = { workspace = true } +deposit_contract = { workspace = true } +unused_port = { workspace = true } +hex = { workspace = true } diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index de3085d22..75a63fb46 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -1,24 +1,24 @@ [package] name = "execution_engine_integration" version = "0.1.0" -edition = "2021" +edition = { workspace = true } [dependencies] -tempfile = "3.1.0" -serde_json = "1.0.58" -task_executor = { path = "../../common/task_executor" } -tokio = { version = "1.14.0", features = ["rt-multi-thread", "macros"] } -futures = "0.3.7" -exit-future = "0.2.0" -environment = { path = "../../lighthouse/environment" } -execution_layer = { path = "../../beacon_node/execution_layer" } -sensitive_url = { path = "../../common/sensitive_url" } -types = { path = "../../consensus/types" } -unused_port = { path = "../../common/unused_port" } -ethers-core = "1.0.2" -ethers-providers = "1.0.2" -deposit_contract = { path = "../../common/deposit_contract" } -reqwest = { version = "0.11.0", features = ["json"] } -hex = "0.4.2" -fork_choice = { path = "../../consensus/fork_choice" } -logging = { path = "../../common/logging" } +tempfile = { workspace = true } +serde_json = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +exit-future = { workspace = true } +environment = { workspace = true } +execution_layer = { workspace = true } +sensitive_url = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } +ethers-core = { workspace = true } +ethers-providers = { workspace = true } +deposit_contract = { workspace = true } +reqwest = { workspace = true } +hex = { workspace = true } +fork_choice = { workspace = true } +logging = { workspace = true } diff --git a/testing/execution_engine_integration/src/nethermind.rs b/testing/execution_engine_integration/src/nethermind.rs index 485485c6f..8925f1cc8 100644 --- a/testing/execution_engine_integration/src/nethermind.rs +++ b/testing/execution_engine_integration/src/nethermind.rs @@ -11,7 +11,7 @@ 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`. /// We should fix this so we always pull the latest version of Nethermind. -const NETHERMIND_BRANCH: &str = "release/1.17.1"; +const NETHERMIND_BRANCH: &str = "release/1.18.2"; const NETHERMIND_REPO_URL: &str = "https://github.com/NethermindEth/nethermind"; fn build_result(repo_dir: &Path) -> Output { diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 726019a84..654b8628b 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -204,16 +204,6 @@ impl<E: GenericExecutionEngine> TestRig<E> { let account1 = ethers_core::types::Address::from_slice(&hex::decode(ACCOUNT1).unwrap()); let account2 = ethers_core::types::Address::from_slice(&hex::decode(ACCOUNT2).unwrap()); - /* - * Check the transition config endpoint. - */ - for ee in [&self.ee_a, &self.ee_b] { - ee.execution_layer - .exchange_transition_configuration(&self.spec) - .await - .unwrap(); - } - /* * Read the terminal block hash from both pairs, check it's equal. */ diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index ea5d005c1..5fe820d15 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -2,15 +2,16 @@ name = "node_test_rig" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } [dependencies] -environment = { path = "../../lighthouse/environment" } -beacon_node = { path = "../../beacon_node" } -types = { path = "../../consensus/types" } -tempfile = "3.1.0" -eth2 = { path = "../../common/eth2" } -validator_client = { path = "../../validator_client" } -validator_dir = { path = "../../common/validator_dir", features = ["insecure_keys"] } -sensitive_url = { path = "../../common/sensitive_url" } -execution_layer = { path = "../../beacon_node/execution_layer" } +environment = { workspace = true } +beacon_node = { workspace = true } +types = { workspace = true } +tempfile = { workspace = true } +eth2 = { workspace = true } +validator_client = { workspace = true } +validator_dir = { workspace = true } +sensitive_url = { workspace = true } +execution_layer = { workspace = true } +tokio = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index d4fd115be..0fdc5cd66 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -10,6 +10,7 @@ use std::path::PathBuf; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::{Builder as TempBuilder, TempDir}; +use tokio::time::timeout; use types::EthSpec; use validator_client::ProductionValidatorClient; use validator_dir::insecure_keys::build_deterministic_validator_dirs; @@ -23,7 +24,9 @@ pub use execution_layer::test_utils::{ pub use validator_client::Config as ValidatorConfig; /// The global timeout for HTTP requests to the beacon node. -const HTTP_TIMEOUT: Duration = Duration::from_secs(4); +const HTTP_TIMEOUT: Duration = Duration::from_secs(8); +/// The timeout for a beacon node to start up. +const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); /// Provides a beacon node that is running in the current process on a given tokio executor (it /// is _local_ to this process). @@ -51,12 +54,16 @@ impl<E: EthSpec> LocalBeaconNode<E> { client_config.set_data_dir(datadir.path().into()); client_config.network.network_dir = PathBuf::from(datadir.path()).join("network"); - ProductionBeaconNode::new(context, client_config) - .await - .map(move |client| Self { - client: client.into_inner(), - datadir, - }) + timeout( + STARTUP_TIMEOUT, + ProductionBeaconNode::new(context, client_config), + ) + .await + .map_err(|_| format!("Beacon node startup timed out after {:?}", STARTUP_TIMEOUT))? + .map(move |client| Self { + client: client.into_inner(), + datadir, + }) } } @@ -91,7 +98,7 @@ pub fn testing_client_config() -> ClientConfig { // Setting ports to `0` means that the OS will choose some available port. client_config .network - .set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 0, 0); + .set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 0, 0, 0); client_config.network.upnp_enabled = false; client_config.http_api.enabled = true; client_config.http_api.listen_port = 0; @@ -108,6 +115,14 @@ pub fn testing_client_config() -> ClientConfig { genesis_time: now, }; + // Simulator tests expect historic states to be available for post-run checks. + client_config.chain.reconstruct_historic_states = true; + + // Specify a constant count of beacon processor workers. Having this number + // too low can cause annoying HTTP timeouts, especially on Github runners + // with 2 logical CPUs. + client_config.beacon_processor.max_workers = 4; + client_config } @@ -205,14 +220,13 @@ impl<E: EthSpec> LocalValidatorClient<E> { config.validator_dir = files.validator_dir.path().into(); config.secrets_dir = files.secrets_dir.path().into(); - ProductionValidatorClient::new(context, config) + let mut client = ProductionValidatorClient::new(context, config).await?; + + client + .start_service() .await - .map(move |mut client| { - client - .start_service() - .expect("should start validator services"); - Self { client, files } - }) + .expect("should start validator services"); + Ok(Self { client, files }) } } diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index a01c133fd..eadcaf51b 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -2,20 +2,20 @@ name = "simulator" version = "0.2.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] node_test_rig = { path = "../node_test_rig" } -eth1 = {path = "../../beacon_node/eth1"} -execution_layer = {path = "../../beacon_node/execution_layer"} -types = { path = "../../consensus/types" } -parking_lot = "0.12.0" -futures = "0.3.7" -tokio = "1.14.0" -eth1_test_rig = { path = "../eth1_test_rig" } -env_logger = "0.9.0" -clap = "2.33.3" -rayon = "1.4.1" +eth1 = { workspace = true } +execution_layer = { workspace = true } +types = { workspace = true } +parking_lot = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +eth1_test_rig = { workspace = true } +env_logger = { workspace = true } +clap = { workspace = true } +rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } diff --git a/testing/simulator/src/cli.rs b/testing/simulator/src/cli.rs index 5dc2d5ec8..ff8020105 100644 --- a/testing/simulator/src/cli.rs +++ b/testing/simulator/src/cli.rs @@ -25,8 +25,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("4") .help("Number of beacon nodes")) .arg(Arg::with_name("proposer-nodes") - .short("n") - .long("nodes") + .short("p") + .long("proposer_nodes") .takes_value(true) .default_value("2") .help("Number of proposer-only beacon nodes")) @@ -64,8 +64,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("4") .help("Number of beacon nodes")) .arg(Arg::with_name("proposer-nodes") - .short("n") - .long("nodes") + .short("p") + .long("proposer_nodes") .takes_value(true) .default_value("2") .help("Number of proposer-only beacon nodes")) diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index 3e764d27d..57c944cf1 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -1,14 +1,16 @@ use crate::local_network::{EXECUTION_PORT, TERMINAL_BLOCK, TERMINAL_DIFFICULTY}; -use crate::{checks, LocalNetwork, E}; +use crate::{checks, LocalNetwork}; use clap::ArgMatches; use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID}; use eth1_test_rig::AnvilEth1Instance; +use crate::retry::with_retry; use execution_layer::http::deposit_methods::Eth1Id; use futures::prelude::*; +use node_test_rig::environment::RuntimeContext; use node_test_rig::{ environment::{EnvironmentBuilder, LoggerConfig}, - testing_client_config, testing_validator_config, ClientGenesis, ValidatorFiles, + testing_client_config, testing_validator_config, ClientConfig, ClientGenesis, ValidatorFiles, }; use rayon::prelude::*; use sensitive_url::SensitiveUrl; @@ -107,71 +109,24 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { let context = env.core_context(); let main_future = async { - /* - * Deploy the deposit contract, spawn tasks to keep creating new blocks and deposit - * validators. - */ - let anvil_eth1_instance = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await?; - let deposit_contract = anvil_eth1_instance.deposit_contract; - let chain_id = anvil_eth1_instance.anvil.chain_id(); - let anvil = anvil_eth1_instance.anvil; - let eth1_endpoint = SensitiveUrl::parse(anvil.endpoint().as_str()) - .expect("Unable to parse anvil endpoint."); - let deposit_contract_address = deposit_contract.address(); - - // Start a timer that produces eth1 blocks on an interval. - tokio::spawn(async move { - let mut interval = tokio::time::interval(eth1_block_time); - loop { - interval.tick().await; - let _ = anvil.evm_mine().await; - } - }); - - // Submit deposits to the deposit contract. - tokio::spawn(async move { - for i in 0..total_validator_count { - println!("Submitting deposit for validator {}...", i); - let _ = deposit_contract - .deposit_deterministic_async::<E>(i, deposit_amount) - .await; - } - }); - - let mut beacon_config = testing_client_config(); - - beacon_config.genesis = ClientGenesis::DepositContract; - beacon_config.eth1.endpoint = Eth1Endpoint::NoAuth(eth1_endpoint); - beacon_config.eth1.deposit_contract_address = deposit_contract_address; - beacon_config.eth1.deposit_contract_deploy_block = 0; - beacon_config.eth1.lowest_cached_block_number = 0; - beacon_config.eth1.follow_distance = 1; - beacon_config.eth1.node_far_behind_seconds = 20; - beacon_config.dummy_eth1_backend = false; - beacon_config.sync_eth1_chain = true; - beacon_config.eth1.auto_update_interval_millis = eth1_block_time.as_millis() as u64; - beacon_config.eth1.chain_id = Eth1Id::from(chain_id); - beacon_config.network.target_peers = node_count + proposer_nodes - 1; - - beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); - - if post_merge_sim { - let el_config = execution_layer::Config { - execution_endpoints: vec![SensitiveUrl::parse(&format!( - "http://localhost:{}", - EXECUTION_PORT - )) - .unwrap()], - ..Default::default() - }; - - beacon_config.execution_layer = Some(el_config); - } - /* * Create a new `LocalNetwork` with one beacon node. */ - let network = LocalNetwork::new(context.clone(), beacon_config.clone()).await?; + let max_retries = 3; + let (network, beacon_config) = with_retry(max_retries, || { + Box::pin(create_local_network( + LocalNetworkParams { + eth1_block_time, + total_validator_count, + deposit_amount, + node_count, + proposer_nodes, + post_merge_sim, + }, + context.clone(), + )) + }) + .await?; /* * One by one, add beacon nodes to the network. @@ -341,3 +296,88 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { Ok(()) } + +struct LocalNetworkParams { + eth1_block_time: Duration, + total_validator_count: usize, + deposit_amount: u64, + node_count: usize, + proposer_nodes: usize, + post_merge_sim: bool, +} + +async fn create_local_network<E: EthSpec>( + LocalNetworkParams { + eth1_block_time, + total_validator_count, + deposit_amount, + node_count, + proposer_nodes, + post_merge_sim, + }: LocalNetworkParams, + context: RuntimeContext<E>, +) -> Result<(LocalNetwork<E>, ClientConfig), String> { + /* + * Deploy the deposit contract, spawn tasks to keep creating new blocks and deposit + * validators. + */ + let anvil_eth1_instance = AnvilEth1Instance::new(DEFAULT_CHAIN_ID.into()).await?; + let deposit_contract = anvil_eth1_instance.deposit_contract; + let chain_id = anvil_eth1_instance.anvil.chain_id(); + let anvil = anvil_eth1_instance.anvil; + let eth1_endpoint = + SensitiveUrl::parse(anvil.endpoint().as_str()).expect("Unable to parse anvil endpoint."); + let deposit_contract_address = deposit_contract.address(); + + // Start a timer that produces eth1 blocks on an interval. + tokio::spawn(async move { + let mut interval = tokio::time::interval(eth1_block_time); + loop { + interval.tick().await; + let _ = anvil.evm_mine().await; + } + }); + + // Submit deposits to the deposit contract. + tokio::spawn(async move { + for i in 0..total_validator_count { + println!("Submitting deposit for validator {}...", i); + let _ = deposit_contract + .deposit_deterministic_async::<E>(i, deposit_amount) + .await; + } + }); + + let mut beacon_config = testing_client_config(); + + beacon_config.genesis = ClientGenesis::DepositContract; + beacon_config.eth1.endpoint = Eth1Endpoint::NoAuth(eth1_endpoint); + beacon_config.eth1.deposit_contract_address = deposit_contract_address; + beacon_config.eth1.deposit_contract_deploy_block = 0; + beacon_config.eth1.lowest_cached_block_number = 0; + beacon_config.eth1.follow_distance = 1; + beacon_config.eth1.node_far_behind_seconds = 20; + beacon_config.dummy_eth1_backend = false; + beacon_config.sync_eth1_chain = true; + beacon_config.eth1.auto_update_interval_millis = eth1_block_time.as_millis() as u64; + beacon_config.eth1.chain_id = Eth1Id::from(chain_id); + beacon_config.network.target_peers = node_count + proposer_nodes - 1; + + beacon_config.network.enr_address = (Some(Ipv4Addr::LOCALHOST), None); + + if post_merge_sim { + let el_config = execution_layer::Config { + execution_endpoints: vec![SensitiveUrl::parse(&format!( + "http://localhost:{}", + EXECUTION_PORT + )) + .unwrap()], + ..Default::default() + }; + + beacon_config.execution_layer = Some(el_config); + } + + let network = LocalNetwork::new(context, beacon_config.clone()).await?; + Ok((network, beacon_config)) +} diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index e35870d12..69fa8ded0 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -14,6 +14,7 @@ use std::{sync::Arc, time::Duration}; use types::{Epoch, EthSpec}; const BOOTNODE_PORT: u16 = 42424; +const QUIC_PORT: u16 = 43424; pub const INVALID_ADDRESS: &str = "http://127.0.0.1:42423"; pub const EXECUTION_PORT: u16 = 4000; @@ -63,6 +64,7 @@ impl<E: EthSpec> LocalNetwork<E> { std::net::Ipv4Addr::UNSPECIFIED, BOOTNODE_PORT, BOOTNODE_PORT, + QUIC_PORT, ); beacon_config.network.enr_udp4_port = Some(BOOTNODE_PORT); beacon_config.network.enr_tcp4_port = Some(BOOTNODE_PORT); @@ -154,6 +156,7 @@ impl<E: EthSpec> LocalNetwork<E> { std::net::Ipv4Addr::UNSPECIFIED, BOOTNODE_PORT + count, BOOTNODE_PORT + count, + QUIC_PORT + count, ); beacon_config.network.enr_udp4_port = Some(BOOTNODE_PORT + count); beacon_config.network.enr_tcp4_port = Some(BOOTNODE_PORT + count); diff --git a/testing/simulator/src/main.rs b/testing/simulator/src/main.rs index a19777c5a..e8af9c180 100644 --- a/testing/simulator/src/main.rs +++ b/testing/simulator/src/main.rs @@ -21,6 +21,7 @@ mod cli; mod eth1_sim; mod local_network; mod no_eth1_sim; +mod retry; mod sync_sim; use cli::cli_app; diff --git a/testing/simulator/src/retry.rs b/testing/simulator/src/retry.rs new file mode 100644 index 000000000..a4eb52cea --- /dev/null +++ b/testing/simulator/src/retry.rs @@ -0,0 +1,63 @@ +use std::fmt::Debug; +use std::future::Future; +use std::pin::Pin; + +/// Executes the function with a specified number of retries if the function returns an error. +/// Once it exceeds `max_retries` and still fails, the error is returned. +pub async fn with_retry<T, E, F>(max_retries: usize, mut func: F) -> Result<T, E> +where + F: FnMut() -> Pin<Box<dyn Future<Output = Result<T, E>>>>, + E: Debug, +{ + let mut retry_count = 0; + loop { + let result = Box::pin(func()).await; + if result.is_ok() || retry_count >= max_retries { + break result; + } + retry_count += 1; + + if let Err(e) = result { + eprintln!( + "Operation failed with error {:?}, retrying {} of {}", + e, retry_count, max_retries + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::VecDeque; + + async fn my_async_func(is_ok: bool) -> Result<(), ()> { + if is_ok { + Ok(()) + } else { + Err(()) + } + } + + #[tokio::test] + async fn test_with_retry_ok() { + let res = with_retry(3, || Box::pin(my_async_func(true))).await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_with_retry_2nd_ok() { + let mut mock_results = VecDeque::from([false, true]); + let res = with_retry(3, || { + Box::pin(my_async_func(mock_results.pop_front().unwrap())) + }) + .await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_with_retry_fail() { + let res = with_retry(3, || Box::pin(my_async_func(false))).await; + assert!(res.is_err()); + } +} diff --git a/testing/state_transition_vectors/Cargo.toml b/testing/state_transition_vectors/Cargo.toml index a25b3c31c..8d930d13b 100644 --- a/testing/state_transition_vectors/Cargo.toml +++ b/testing/state_transition_vectors/Cargo.toml @@ -2,14 +2,14 @@ name = "state_transition_vectors" version = "0.1.0" authors = ["Paul Hauner <paul@paulhauner.com>"] -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -state_processing = { path = "../../consensus/state_processing" } -types = { path = "../../consensus/types" } -ethereum_ssz = "0.5.0" -beacon_chain = { path = "../../beacon_node/beacon_chain" } -lazy_static = "1.4.0" -tokio = { version = "1.14.0", features = ["rt-multi-thread"] } +state_processing = { workspace = true } +types = { workspace = true } +ethereum_ssz = { workspace = true } +beacon_chain = { workspace = true } +lazy_static = { workspace = true } +tokio = { workspace = true } diff --git a/testing/test-test_logger/Cargo.toml b/testing/test-test_logger/Cargo.toml index 3d91862db..63bb87c06 100644 --- a/testing/test-test_logger/Cargo.toml +++ b/testing/test-test_logger/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "test-test_logger" version = "0.1.0" -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -logging = { path = "../../common/logging" } -slog = "2.7.0" +logging = { workspace = true } +slog = { workspace = true } diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index c0fbf6672..38b775b39 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -1,31 +1,30 @@ [package] name = "web3signer_tests" version = "0.1.0" -edition = "2021" +edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [dev-dependencies] -eth2_keystore = { path = "../../crypto/eth2_keystore" } -types = { path = "../../consensus/types" } -tempfile = "3.1.0" -tokio = { version = "1.14.0", features = ["rt-multi-thread", "macros"] } -reqwest = { version = "0.11.0", features = ["json","stream"] } -url = "2.2.2" -validator_client = { path = "../../validator_client" } -slot_clock = { path = "../../common/slot_clock" } -futures = "0.3.7" -exit-future = "0.2.0" -task_executor = { path = "../../common/task_executor" } -environment = { path = "../../lighthouse/environment" } -account_utils = { path = "../../common/account_utils" } -serde = "1.0.116" -serde_derive = "1.0.116" -serde_yaml = "0.8.13" -eth2_network_config = { path = "../../common/eth2_network_config" } -serde_json = "1.0.58" -zip = "0.5.13" -lazy_static = "1.4.0" -parking_lot = "0.12.0" \ No newline at end of file +eth2_keystore = { workspace = true } +types = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } +reqwest = { workspace = true } +url = { workspace = true } +validator_client = { workspace = true } +slot_clock = { workspace = true } +futures = { workspace = true } +exit-future = { workspace = true } +task_executor = { workspace = true } +environment = { workspace = true } +account_utils = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +eth2_network_config = { workspace = true } +serde_json = { workspace = true } +zip = { workspace = true } +lazy_static = { workspace = true } +parking_lot = { workspace = true } diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index dd17ae23b..463de0c8b 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -51,7 +51,7 @@ mod tests { /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will /// assume it failed to start. - const UPCHECK_TIMEOUT: Duration = Duration::from_secs(20); + const UPCHECK_TIMEOUT: Duration = Duration::from_secs(30); /// Set to `false` to send the Web3Signer logs to the console during tests. Logs are useful when /// debugging. diff --git a/testing/web3signer_tests/tls/README.md b/testing/web3signer_tests/tls/README.md index 53ff97c27..199e30437 100644 --- a/testing/web3signer_tests/tls/README.md +++ b/testing/web3signer_tests/tls/README.md @@ -1,6 +1,6 @@ ## TLS Testing Files The files in this directory are used for testing TLS with web3signer. We store them in this -repository since the are not sensitive and it's simpler than regenerating them for each test. +repository since they are not sensitive and it's simpler than regenerating them for each test. The files were generated using the `./generate.sh` script. diff --git a/testing/web3signer_tests/tls/lighthouse/cert.pem b/testing/web3signer_tests/tls/lighthouse/cert.pem index 061b0e3cd..5746d19a1 100644 --- a/testing/web3signer_tests/tls/lighthouse/cert.pem +++ b/testing/web3signer_tests/tls/lighthouse/cert.pem @@ -1,32 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFmzCCA4OgAwIBAgIUXpTV/0rd/GAoCfCyzPOtwcb4t7YwDQYJKoZIhvcNAQEL +MIIFujCCA6KgAwIBAgIUELASgYwStCn/u/8tPByRADyCwLEwDQYJKoZIhvcNAQEL BQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRMwEQYD -VQQDDApsaWdodGhvdXNlMCAXDTIyMDUxMTEzNDEwOFoYDzIxMjIwNDE3MTM0MTA4 +VQQDDApsaWdodGhvdXNlMCAXDTIzMDkyMjAzMDA1N1oYDzIxMjMwODI5MDMwMDU3 WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5 MRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xEzARBgNV -BAMMCmxpZ2h0aG91c2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0 -HrD6fJGcqm8zwEs+Y+FGIpRYPyjdlugj3qqwvMSI9jeDW2fr1zUl/wIuf4o+O16P -XZitHgAyg3lph1x/kKL59c4rwWxUabSudAQZ6YCJHo4jWf3hR+UmMQEdNPgNrofv -vGCA7CjLPKZfW6pzZo9kvMwbgeRNuJCuKZ0v/p9Y/lOplj+TTBq16HMtsSarib3b -nKEaRdLCQgTJS3vwbtEiCC9BcZAkvs0fmVUIENRVeKGZIqcAdiOTUPvs4zctchzJ -MGG+TA2ckKIpGT0F4be8gy1uHyP0fncJAtNvkGRPmVQcNew/HIIkJjiJvmrwewn4 -dYqYAe+aEL5AB4dZhlKjIPENfq38t7iY/aXV8COTQZGMEZ7Diext1JmEb34vEXgS -7Gk9ZSCp/1X+fk/wW4uQeRlGwblaRtRxBrfJWmEoQHohzyP4jog8dajSZTjUbsA+ -HGaeZo1k3M0i3lxRBbLGamPODIO9CVGwKaiEJTy4bEpreM2tLR1rk5JECf46WPUR -SN6OdHrO5x38wzQlUv+Hb4vN4p0ZkiGJO62Duuw6hbGA6UIBffM20QuJUtz3Pa8D -un/NunIagmIL5KCsrDtZkt5wBsX3XU6OPdfZrfgOIXNfQmpbbeAUOok1NOgszXjP -DKCsnxZZBtPhXC1VnRkiWK50GNmWe8MLqXR/G12TXwIDAQABozUwMzALBgNVHQ8E -BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkq -hkiG9w0BAQsFAAOCAgEAcCGqC1nhjDiuF87LgzwuKdMV4NEpTGHa8aHrta/UrzO3 -Lf7fcQvB83tR3ZFk9ndlnDbTVr0seAqDDdJxUHyaA3lX6F5g8G6W8bm76w8b5vot -Vl4ohfcA0CIxbCpp773V0qjyZNj9wDIZg8cX8mXcRi4XoUDltD5/yUwRLVjjvJba -tF+vD3NWWuCGRu65qdR3JYJGr4MtbVo06uoeBXcgZrcDsb93chlsuyH337twq2fn -QbqHbuyxAjFxtv125Jmu6li3pu9FUQrnQWQVHzvt2zvR44vOx+yDQHtil9U7H0aU -Nrzqr9OPOApCr7oQ8GoHYn4C7TAs12U/xiPsvuM1puTzbw8ofuKczFRIA8nuyUHU -XTP/9oYyZ/Vs9qyAtIVCCyEfhSobfwZLLFAT4RWzQZ4H0JmtXfNdt+PFPSWg5MZA -W321uulq/JSa4MQUJbNUEeNYeG+NqjhviM00irpt2Baz2EbVAJMT4ClndRQOwrKT -15+icdyvgx5uZbEuvXK6kyU0AHESHxhzN6C5eHPEYkMjVYgftbE7R3cp9TEj3VvK -Ecd1SXTtKOq2J91te10UrceURqquGuGXVUO7PYGVYBNugjlH47qRIwtI0njPg3ep -10XBwkOm1CgvZxHaj4P0NJf+wih+K8Z5Dg1+90nnJ4mxGFFIW8m7Cfn1tPFmEPo= +BAMMCmxpZ2h0aG91c2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCc +i30cib5B/B5QNd8grzi4LxmlyfZFi3VfpukwdwOD1Xk3ODk1OtjAzhK46YhDclvc +u98m1Dnib1Z+eTjRuEEoekIxz2+BbOle7G52LNvuDZpD+HKucqIU3TnEKPPuTYPp +lZ1n/9EyxXUwD5uTkn7xXzK8UFXUt73j6I6VFMdHlNcwLcx8KSwBDzvnGT4ew/UL ++ThON3j5rIT+nFHDcC2zoM+6ANdVkL6GHid4/cOcYW6GxB9TRZtEasqze41bC+kX +ZtPlV5V2nilAzVj8z9ynwBpHkLH+E6sMUhSEwA++QfI1gGf0FmSBgSIZ3RdPo/dp +hkLG8fZXKMkMzKkRm5hcstDP6DnTIYl+CfuVez5gZ0/yelAqXNvTqMKuDhHTTRRY +aOXZX4BAiQO2Q6a6WYLe87E2ka5AF2T2y/BPeXjUwDS/1mFIB3FUGlMLVJt8/RLz +nXVGoSsYapttiiPucQbMPEysCJ4/LZ9zxe3EDWWjpurLHGi/Y/dVziEvg1Eoycix +dZogKz0QVCz4++QI0kPDDX7So7CWni2JJuYguF/8CX8QbCT2L8jXf0uQrq76FLKj +88A7lS8DzXBt/pRryiIlDyLenJwHmrv6p+P/FYvgnJHvAEtTynxYm5GA16YWy+Dj +c5XVgNHjV4TdX3GueAp+NhBBaHDFvYCbP/oXkRvNRQIDAQABo1QwUjALBgNVHQ8E +BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATAdBgNV +HQ4EFgQUoeeF4G1qTRzLvO583qitbNDzr10wDQYJKoZIhvcNAQELBQADggIBAA9Y +YZP0pZLyovSnjyyuTR4KE9B+TSwqHe/LvH+7EAXLH+cwhyS7ADfJyt3mOCbKHZSo +dmJ5KWQ6M2Xn9Wq40BPk8mQPmAxy0nHg5beG03HYXOIsK8zgXTMad1+D1jnHPAda +ldXJ2Y+ljx4TDXKCWpTaq1+flqgRD3t98tOLuiULZ5jsTFX8Xbun7matcjziU5Lo +GWVQPWkb8Vx+3QyfbfiYJ7hggfYTxQsVJOXKuD8k2FMtKn5oTp3VwD2kY1q2X2Yk +HsDZJdYrvjWi2LcZDKoSNeusuLrv1XoUnwsAa3ng6drvoEU16vfILLYqH820UJ61 +/fFm3a9BFHRvPVd/WcSeIVc9jx9+32RIVxlppwCINnGMGE20kUZxu0TiMjTX9bCp +AouDuhwMt7z5jiQIi/CMxN6IlHBeVLqyK8ayWvH40xYgZTXlePpmLcQhcieNk7oJ +ard9jMfj4JhH5GbLXVptMBVJ0f9Ql4rW3EyNipvVKdkgTUNIeVm7LyUK220aT7ty +a0pGWHHViiF1MjGExo0P3gjZIML32TjZWlG3Nts5NAiyXDo4f78VeLyZQ7efVkub +GpjMf89vrmPdQhssoFr8fRFQObDe7hgxkgeiw9jgHItJl2/MWAxfsHV18HwiBqGW +QzaZR995YhU480jvA5XR8+EB6QUZeCEKunW8WK/F -----END CERTIFICATE----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.key b/testing/web3signer_tests/tls/lighthouse/key.key index bbc69ca38..91bee6a78 100644 --- a/testing/web3signer_tests/tls/lighthouse/key.key +++ b/testing/web3signer_tests/tls/lighthouse/key.key @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC0HrD6fJGcqm8z -wEs+Y+FGIpRYPyjdlugj3qqwvMSI9jeDW2fr1zUl/wIuf4o+O16PXZitHgAyg3lp -h1x/kKL59c4rwWxUabSudAQZ6YCJHo4jWf3hR+UmMQEdNPgNrofvvGCA7CjLPKZf -W6pzZo9kvMwbgeRNuJCuKZ0v/p9Y/lOplj+TTBq16HMtsSarib3bnKEaRdLCQgTJ -S3vwbtEiCC9BcZAkvs0fmVUIENRVeKGZIqcAdiOTUPvs4zctchzJMGG+TA2ckKIp -GT0F4be8gy1uHyP0fncJAtNvkGRPmVQcNew/HIIkJjiJvmrwewn4dYqYAe+aEL5A -B4dZhlKjIPENfq38t7iY/aXV8COTQZGMEZ7Diext1JmEb34vEXgS7Gk9ZSCp/1X+ -fk/wW4uQeRlGwblaRtRxBrfJWmEoQHohzyP4jog8dajSZTjUbsA+HGaeZo1k3M0i -3lxRBbLGamPODIO9CVGwKaiEJTy4bEpreM2tLR1rk5JECf46WPURSN6OdHrO5x38 -wzQlUv+Hb4vN4p0ZkiGJO62Duuw6hbGA6UIBffM20QuJUtz3Pa8Dun/NunIagmIL -5KCsrDtZkt5wBsX3XU6OPdfZrfgOIXNfQmpbbeAUOok1NOgszXjPDKCsnxZZBtPh -XC1VnRkiWK50GNmWe8MLqXR/G12TXwIDAQABAoICAQCXUo2W856Vwy5HiQ7t7JWv -CZAdj3pyp7yBnilC8GQhONGsntdw8M2rDVG05Nusqs4nnheNoX3C8mfHO7x/Q3FY -lKTQZ+DuDhyIz9k+N8kP6ca6dnlvkao3asYn1n9rZyy3QUjGJyGilWKlDGroJsrj -dCX6GidHEH8kgruXPdB7wLdi62KgCjkKiK5zPbhiNwd1gGJsoyqMn1BMGQmYFlHG -yJ+C2Lij1lSYboZcj18EK6N/9vfc0GPU+R2dh8qseIkskWQcruJknbJO2vBEh7yI -OKCrOqhHWRQCUwh1WxabNRLP3JGM+BNx8VZgisRnIsdeoMl+KWo1wklDm8+fa9Tx -4xquIy+4PzmobWXiWBpirF7bTNhyZ4vIaMSTOP5TYiliom/hJtcpAwLf9eXxMfti -vRAogZEtr0eKTieH72dwsBVx6wNlxhazvD+ZKIq7OIzJRA6Do2H+BAmz/l4mgVR/ -geL3u0fn0j/Y+8OyFE3P+8D/PqgPzLgTYa5QSp6JtHxNlVcmWefJiLtZDAJvPpeo -UVsA+E2BHsrGveLk15GF9F+vJ867qKT7luQac3zF7V0hE9pktUKM2gY+Jy455w5i -cMxyjt4RAKY8AHAmFvCRQHNdjU2o1UjVFgYsQTYsOdvAiyq0xEJFkbeR2Zxz2sJW -JWK+YlT+UEGDL5SCaXzP4QKCAQEA7gRAy/Xq0Fjq7UZvc7oJ62h6BmseFL9BuKlW -QmvVFAilYeQVejl/ubafyL4Z9ntEeCGTkv8H4DeALs9A/isFOcDxZDoelCETrSxI -CfXllob24276eTc5dBdHmofBjRgIbovnyuFRYzK5uDalVAxYsZPFOp9/qtGa25ex -uIcyJwX+ivqqtA9B5CHu7p/znNrp155xLwGpVczx4xGqjPPr5N2rwZFOXufGFULH -AKbJBSUxiMMJnb1rN8aIuTo/Utr3/i7hc7AUO3//qieyjLdXe8tESqgxzTNvfZk3 -qYtPk4GSHql7Eesxg19fzVdG+LTnzfRKOfOtcZJPRFGGW29fjwKCAQEAwbqXsZvC -7AmmmeVVAPL7q5pXAxSEMK7VsJzPJ7G6MRQ37YjkNRcCf7SRQqNBGQubVkv3Qzvc -rmMhT9I5QfCR2JXQtrH1y09eS45T6NYbRkT6NA3E3XNmRIPO+wIeDV32v5jJwhIk -7ayuG2zBsAryxNvg3us3pWHeIQ45sX0JqNil6BTemYRBrCZmCRWHndl72zDbtR23 -kVt9GKaycSPyCZQ7yE4ZWD2VsrbgEidVJEQagknsjQrldMO68GLbHCP2ZyrIUhKN -2eeuHJpZPz+pahQ55MAEvjIsJKPWsg8cut2Vo4sqgez+xiz0v/nWiPLtvxdN+DHP -tAVbrw+0NeqnMQKCAQB3GsO+DLpLNiOhRpzhAViTZ32glpu/8BEYMgzLQiCnXMg9 -myAwQHOs4DlG//IICJkzsEGjzmEHj15iji3MwoRj6SwiZn8EyySIhN8rtNQFplYH -a3KFk9/5OukG6CYvz7Xwc6wzNts+U5TiHN5Ql7kOa47HjicZuLfQaTFy0JyFMJe2 -vkcLwZLMcTqaSIpklJtt3Yhv6FnvaJYmdaGt1SXXKiIXw/m+via+XuMsbUmsfHc0 -I709JRtxFrU2U3J6qL5ugNEqzhLhz2SFpkXP6rMpbIcpAM+jCrkg1bon6mGQw8b1 -9wNx7Qqi3egX3jPSotxYkIVQSKMjcP6fhlhAixP7AoIBAH1ynKQwHurF3RIuxPqW -XY3jpZCjCm6T6GAzSpmDpvP9CbJRQKV4Pu//N0kVeiQDthUNoBHzg5WRL5MGqHkg -lPDRIpQLbQS4YnE+uus9KfA43mQyvlZAUerwB2nXFyrEu/GZuJxpL2yQszWjGVEr -5cTANT9kxWXcmACDu6xJMaYalGRSj0qNsBEP1GbxgB4hJOjtHHiNw77mpXz/BPHq -uuKlEIlGuXbAel19ul9HBQU07I2N3RYABlG0JStgeE4io35u38T1qtF+CusOr9gb -G1NLwal1Bh07VAZt6arnykzfC/UZOu9jTh96IQrnd5q65GUnbB/Z8Yu7JIGaA7Ie -PyECggEAPZlzqPCdNcmdoCSNIDCDYZBVf2xZX8591xdphMG59Jrckp5kl5LM5bjQ -tysj1LJpMK+l60b3r8BI8a4lvj+eBqwBUck82/IImTedE9/oLF3Z64kLd1tr3aGa -W5jLXjThFF20BqfD+YbmFVEdHTwN2L+4kN0VvP/6oLadxogTLwQruMFoPlsD4B19 -HDcAKe6OnyWMer/X9nq9OY6GFGc4X6wHjJ8pj4aa4HE8VNNq40GMkRZOZaJvaPqh -orK9SC50qdJtrVQeD4fhfZMVzmRyE4RSSQBPfc9zq/sO/pjUfV9uK4c99FDbviIf -JAkxGuYLZeyrHEyeKLm7S77SLipKWg== +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCci30cib5B/B5Q +Nd8grzi4LxmlyfZFi3VfpukwdwOD1Xk3ODk1OtjAzhK46YhDclvcu98m1Dnib1Z+ +eTjRuEEoekIxz2+BbOle7G52LNvuDZpD+HKucqIU3TnEKPPuTYPplZ1n/9EyxXUw +D5uTkn7xXzK8UFXUt73j6I6VFMdHlNcwLcx8KSwBDzvnGT4ew/UL+ThON3j5rIT+ +nFHDcC2zoM+6ANdVkL6GHid4/cOcYW6GxB9TRZtEasqze41bC+kXZtPlV5V2nilA +zVj8z9ynwBpHkLH+E6sMUhSEwA++QfI1gGf0FmSBgSIZ3RdPo/dphkLG8fZXKMkM +zKkRm5hcstDP6DnTIYl+CfuVez5gZ0/yelAqXNvTqMKuDhHTTRRYaOXZX4BAiQO2 +Q6a6WYLe87E2ka5AF2T2y/BPeXjUwDS/1mFIB3FUGlMLVJt8/RLznXVGoSsYaptt +iiPucQbMPEysCJ4/LZ9zxe3EDWWjpurLHGi/Y/dVziEvg1EoycixdZogKz0QVCz4 +++QI0kPDDX7So7CWni2JJuYguF/8CX8QbCT2L8jXf0uQrq76FLKj88A7lS8DzXBt +/pRryiIlDyLenJwHmrv6p+P/FYvgnJHvAEtTynxYm5GA16YWy+Djc5XVgNHjV4Td +X3GueAp+NhBBaHDFvYCbP/oXkRvNRQIDAQABAoICACCSBxxeblblQVtX8g4nVso/ +hnsPi61JiEi3/hGG2ZTe4AMEsCZqkXmABrYxZJf/3awN7K5z/n0lxB25VACScQAe +e9JIQf9wLRgCYjM1PycG7n9Q3G9+S0nDA4dUK/h7aUQ6zE68k4aYPbsbrDdmhgHr +WC+FGW6SMjCOjMfo1FOI3MLZ7I8ys8Seqkx5XIrjI4NzvWrMsN9lrSAaXwqmNuQG +Q+ID1cmoPXPDJ1xNlBrfzLK+cHQPafAwte7k+HKmhj9HtjOj5uWQn62ra+Xhy5ud +ZPpZ2Savaem81CcQnNXte5r1Fevbktq9Bt7RuM1ppIrwk8k3w5S72CTRGiYfNPJV +M1RMp46GrXVJdmx3k9LQfKdT6Gv9xTJXYQl7jN0+4uZ7QrVQHpcMpxPsATl+cQQH +wzCTbj2Oqn/30KqkZLyueN2MalRP8mVSe5nD+vvGb/sWLs52kp6QvHdlXER2RBFk +tJ5cGi+vgueoukb+qatiAE2y5MxYCqD02ShGcLos/SUQThRhL+iD8t0h+FoPTD5y +eTNZ85hF1HdypH1If8/YGETg55+fHYUAtYGT6R8lYeFMvBC05suorLBciXShOGuY +4zBbt32fPlsXlLneAtAAFv2BiJMt0TQavWHITLInFW1/aMHDV4/Pq69sRZuHdRaW +XFXD8CjnPUS5tBMQOqYhAoIBAQDLqUo7v3SpIstXmyU7BfUBuTYGS7MzjMhDxFUl +HvmbVZlOXhnPb3p4mW/XHrah9CjFBLJt3CF+PP/njwMw0YtPxCQpQwj0pI8CuveE +4Puq2wEfxVg+JKh1xidNj8230/WINzwfLCVfco7KKmjQX0MgMGaANQ0sGnt/r1eB +MwpY5uID+D5PORXUcHxBWlsVLyzZ9ZqKhAgewr3i7BLX2y7nwqEGlWTt1lxzZGCR +a8NZIAIs3qGzAgtm7O3hMz6XZulVyVSrMmmT8qXT4Lo1nW/9J6slV7Wdp9f++mr9 +m2vwrpJtmdPcA+YKPVgoFlKmZpZZbVvd+4uy8ksoxs1/cF7VAoIBAQDExnLQplq2 +BPoxEDAnlS+8Jju5en5Pk70HOfpQGUa4/6vY60x/N5sJqc6CcDySkkxRI8jLzMTe +AE9jqM+Z39MvGCH+SF9EPRopbAJIrcChXfvk2Imp7PLFRGrEBju63nQfaHdcefFy +Ia7RA8SCHLCReRdqPjSXbPAYPZK84vVNSfhrkytA4FJnaojvaqJqLQH9vB7CXv18 +Fu6w5fnrgARIoBhy2mb0QWzgd9JMsVDgS5XyX/4HBUejjXDdmLosOZ4PJ0GM2+tr +ilO/9KKhV9lqH7DcFgJBNkgVKRD1Ijr21yyOkttB5PULzaTQhzqkorGkWgyTzLWn +ksqOr2cWt0yxAoIBAElIazvAkpvht0WYv/bTF+w81uHBD4R3HgC0fubSlIJ+dKGs +XqEzvd/zZjkEBjeUga8TF5lMYojoLjUGUuGYJQbYzyJBytEs/YDAAhzAUA6Uq3zh +J/WEf1GRscbI/f8tt+YB6hJVckU8FYFNbVW9UYwdnmR3snuyM8ooL9Z/pTOEMMO4 +6cLcCazdpPhnKOsghIURSUCabcmTzXv/8m/VoLUoZYTW8PBb9/xVnCH3ot1JFT9M +BOdCzxOEIbytEdKs5z1FKsBHbZIc9+qbrKVqN0fipETVoLZQFPrc5O7IpDiAuJPT +jFZY2MfKdxRFpAvYUjVvkmT4BLapVL4hewRmTNkCggEBAKuJP8/KJSulvSEGNqRa +9kjzn376XKAsb02caixnTHK7Vuh7fq0sIThUUhT9mKBBbswRANtAv6Gz7YE4SPmf +1+6nAAM2ve2zwlm3sWoAJfvF/W+qoJ+EUsJK+TO3J1yozdwPanYwS52t5UKKIUU3 +k2jNge75GUmkCs1m58NHqoXc5PWKTtt4cf17LrJfaARdBe5Wjw3sVtdU+nE1mh+E +8rcI8Sc2Yyes3Sf07Fw0+wb8fVPUAJPIM4JNK8XRfQJOnA4jr44GrPyLkqS0sw0p +kvtjcv75JLAKjN39da3sUDCctVf4h7Cy0jee5n1uVV3uAiP+6BX0D6tsWK34FEsG +MZECggEBAIi/sjZNQjplD5zOULEWL8W6b+3CZymR5Qqa0brlx1Lz8h/daIITIFvm +bue/CjIht/oRGLVE8yzw2ojLf424h3h5PjmXMBNHlVkWQXfn6xCI8MjfZ71uA39O +RVCXAYwcghOWZL4Fkz+XQmIOdJ1OPXfU0py943joYZbgXXAYOc/zNylo9j7+bqDK +vLtFd4IIQoRzjsY//FoAuAditf4xDRqLwOh4amboZw1Qmn6bwDnCaKsFmA3o5BYR +4aRUm1dEbZgPtm2tuHQpEKuOPhWHroi3NsEdbhoyy3IUe0c3w4YGgnuvVy616wkV +GlPvUaKC1KX0CX1qT1anVZq9bSMTG+M= -----END PRIVATE KEY----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.p12 b/testing/web3signer_tests/tls/lighthouse/key.p12 index 22b7d7f42..d96ab4775 100644 Binary files a/testing/web3signer_tests/tls/lighthouse/key.p12 and b/testing/web3signer_tests/tls/lighthouse/key.p12 differ diff --git a/testing/web3signer_tests/tls/lighthouse/web3signer.pem b/testing/web3signer_tests/tls/lighthouse/web3signer.pem index 460cb8b40..455021447 100644 --- a/testing/web3signer_tests/tls/lighthouse/web3signer.pem +++ b/testing/web3signer_tests/tls/lighthouse/web3signer.pem @@ -1,32 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFmzCCA4OgAwIBAgIUSHwf3lJKpa1BNR9rFOmxhoKTD1MwDQYJKoZIhvcNAQEL +MIIFujCCA6KgAwIBAgIUOVccYETgo2YpKO85U4XRKifK09kwDQYJKoZIhvcNAQEL BQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRMwEQYD -VQQDDAp3ZWIzc2lnbmVyMCAXDTIyMDUxMTEzNDEwOFoYDzIxMjIwNDE3MTM0MTA4 +VQQDDAp3ZWIzc2lnbmVyMCAXDTIzMDkyMjAzMDA1NloYDzIxMjMwODI5MDMwMDU2 WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5 MRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xEzARBgNV -BAMMCndlYjNzaWduZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDr -aQUU4O7K/aBAiH86RV3ye/Q7vguwplUNku317chzyFdB+OnGSUga6+zjdUmr8+49 -nki1q0rLEU/xJ0NpffTdzFgk1nk6Jh7Ly26q18SNpwpuwdvbajnTeh+BPSWZQL85 -xfO9th/RkJkgpzKukxK/npjvU6PbwiufSWI7mXNIgR0lIIacFXZ4RsD1PxZo/07k -toF0N+yLGW76yfeINRw43bG1MQxklePsk6zAUqJEi0tZmXqzh1NZHH5Q1VAEKKPW -yAVTDi3bWmvh3iSfgmckesjwUHANFeMhLpdiVTOi31OaILpx9HGRYYnqjW1AUZLo -SMKkyPsm6IN60GpAVI7TP3URVpTPPW78UeEUyeYN06tABYJsFWGFChg9Hf2yvcZU -2DDGdHpxut6h4WAwx9oL5rG4VSxFjhVi6ty3Hb9B0YFE/WNfV07wWPSQADZSK/kt -fhE+8zavQzjsxm2f1Ko5L/x8cIc5MS1xyaXn/UkoqH3QdWZC1aLs9NCl4F8ZE06g -jjvN9WdsCXmTEShqaXoRsZG7SfcQsu4gUUZ/fjbJ5hRf+QxMMKv42SUpqsRhslEF -/Pqu0WQd82CgG1a7XnfUO8BYSchTJZL55vx40ZZuQAu/ULsF7toa0lktijBxCPn3 -8HEnyLEyA3e8a93P0myWoxFn/fUpegT3TVSv33anqwIDAQABozUwMzALBgNVHQ8E -BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkq -hkiG9w0BAQsFAAOCAgEA1Bn7mpa2eJUo4+1X5lVLWWwtXLAfKiBf6OWNfacLV6FL -gyKpvvESTGuA5VAS0O97TPd7uyzEbUMS75TdmfAT8zecO2aXMb7aTyX+QbMj2gmk -zou72Fl4o6V1IvYpjKaNBZCS3Hk67ivRYbQCamEOk5UX9/wCdLvC9PH5Y+WqcPaz -7RLXe3OXhRbfFax4+pWzZxsgSKrEi8ZZ5gRa/bdJVVsTqk9LwS/CbMjEAkdzIBLt -cQb9BcnTJcQvp6ehNIVMdEC7GLXcDkefw7CL1ZfEh3DoJD3hiR6QwdWtdG0etoUf -w8LHZhCJD0IZxLMHiE+qiN4xkx+cznol+gAc9sfmtVK1CAW9l1Aa8zw5AfAyCg3h -jr6ymfwY8zlO21yBmCTg2+yTbU/0CqkgimQeztoYCh7+67QgnSCJMk2ffR6GPj1q -pfLI/5QNoxdFvR/lkwj5h/HRp9JZKTV/R/g0Va4Arg3Y7RTezjCYkJnX37ScnQhg -JLIeXmksFkc+Oz3yA+r60rR72+lsVzE87BCs+L0y16zcQnU5NqJXrSMMqCkjbs9l -b682+tnJKLFGQrYia/FL/Sc2L2Tn5hba5wWQTMjGujg76fkMc6VIv1qG3VGR/V1G -r11UJ+WjEcdrwZUm7E76p9DfTce52kGqGXwfrv6kQjvLhipwjzgv429txzDy82k= +BAMMCndlYjNzaWduZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCr +JajqnvRQEREph+zu7rw1QjHQG1x2H44SJSMjX1Wzi9FErlRSOzywPFL2AzGsNvNS +tPmxN/kF9mBjQIQHxo90M4GcZgW1aljPaXLvQWFrP9ak+JjHuUG+j51fVJp8F2Qc +BG8i2LjjSLvkEYSULHI0kbMPws+DKcemvZJ6IhkoPkbtnx5Z1zDj8D6vvWGJguMO +VSNJY7SoBNuSB6CJ7wCWBg7UPtTUrtnuJVvUh+3k2wc7LJ+C9wd7rt+qYb8LxQuc +j8dDyncXXeI583oGzjTE+1kFrE5TuMDlnWyKPa6NQPeXQtygFTyQL9RMW6JkgWWg +tDFWqd2Mgb8sCRtl5uTJFGJ7PFBP4T69JqYhz817tDS3JrMbbzzhRzf3cB6V2NCC +zVKBrO7gfAyDwWVr5iUyaXhLGyzuGg2nMbFMj/Pr7csravs+Jq5apwyZDNTv+2WQ +xP6d2gGFwQOxcPt4OGPjtFpVHH3cxLkcGsSOZ31akuhdSJ6MqWI4tkgRpsf5Ff0+ +z8SLZaCQIp7M4O4LpMreAT7smvEQpLphK1oKWlsY6ukkJ1y8KD3EfeJRpDL0PBTy +jacQATPsqUzeryCfqAMulLLqUbNFqv6Slhzt2vr+lfIr+IeUa/7XMeZOZJu1T/7n +fTjpdokSTx8DageE4Z3j90q5d4hdXvMWq6MpQW7RqQIDAQABo1QwUjALBgNVHQ8E +BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATAdBgNV +HQ4EFgQUsBCvmwLPQDG+iN5qI6P7SgLZyP0wDQYJKoZIhvcNAQELBQADggIBAE/j +mwchm30rB+dheTRBcVD0yHgYL2tQlpfKZeX9JDVWNMQ5OYHtMVwdD7RBQJ2ypqIr +5VP6/Hf0M1GE03mnDpjv29q57AkuGFZpBvZ+1XCG87336QIPqkPR4uMJ86MalsX2 +f9GHMG4H0rd1j+ozM0jhJNoVG4lSq/GNn2E9oRjMG8lq0M7apWwK1FQUBECIlfw+ +tk9aq2zLl409vuqzgsWeffBcdVEDHGCLQauzYRtxMBbzLb33gWWd+9149dWeG5up +P0CZvdetgXhlcbusmrBWVn0O57/QDaGzEUZKxqoy8Ncv04KMYN1gOF+nO5cKn0R1 ++4yvb/NJTdo9WcdLcleqSL1Ju3kX1dCIPOpuaZ3aEwLHrvlNxT8Y5OMvRsYPINAU +6JfNGu21+Bq2nEqSqrw8Ys2hdGI+E95uXjPcsm8BZRCfxfkEeYVtx4ZaqMF+bkfD +d+uePSFp4VBWbg40RMVymr1YcNTX3CjvtLZDH4BZBdx/8YjUEUYPpC7xGoaQDGvA ++J9cVHRpxYpry5fbBmSvrKvKXU6aijLpM7etjYWzYFturpi52Ya9h3LIHd4RaBzB +0YzmatirLK/07YBUECsVcAlddIK5KOA5Nd7+oUikmrR1wMY+I/hym6fSTZGo/TDY +vDFERRj1XOOhlCzHx94SS1DS0rVTAj4uxbuZisaz -----END CERTIFICATE----- diff --git a/testing/web3signer_tests/tls/web3signer/cert.pem b/testing/web3signer_tests/tls/web3signer/cert.pem index 460cb8b40..455021447 100644 --- a/testing/web3signer_tests/tls/web3signer/cert.pem +++ b/testing/web3signer_tests/tls/web3signer/cert.pem @@ -1,32 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFmzCCA4OgAwIBAgIUSHwf3lJKpa1BNR9rFOmxhoKTD1MwDQYJKoZIhvcNAQEL +MIIFujCCA6KgAwIBAgIUOVccYETgo2YpKO85U4XRKifK09kwDQYJKoZIhvcNAQEL BQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRMwEQYD -VQQDDAp3ZWIzc2lnbmVyMCAXDTIyMDUxMTEzNDEwOFoYDzIxMjIwNDE3MTM0MTA4 +VQQDDAp3ZWIzc2lnbmVyMCAXDTIzMDkyMjAzMDA1NloYDzIxMjMwODI5MDMwMDU2 WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5 MRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xEzARBgNV -BAMMCndlYjNzaWduZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDr -aQUU4O7K/aBAiH86RV3ye/Q7vguwplUNku317chzyFdB+OnGSUga6+zjdUmr8+49 -nki1q0rLEU/xJ0NpffTdzFgk1nk6Jh7Ly26q18SNpwpuwdvbajnTeh+BPSWZQL85 -xfO9th/RkJkgpzKukxK/npjvU6PbwiufSWI7mXNIgR0lIIacFXZ4RsD1PxZo/07k -toF0N+yLGW76yfeINRw43bG1MQxklePsk6zAUqJEi0tZmXqzh1NZHH5Q1VAEKKPW -yAVTDi3bWmvh3iSfgmckesjwUHANFeMhLpdiVTOi31OaILpx9HGRYYnqjW1AUZLo -SMKkyPsm6IN60GpAVI7TP3URVpTPPW78UeEUyeYN06tABYJsFWGFChg9Hf2yvcZU -2DDGdHpxut6h4WAwx9oL5rG4VSxFjhVi6ty3Hb9B0YFE/WNfV07wWPSQADZSK/kt -fhE+8zavQzjsxm2f1Ko5L/x8cIc5MS1xyaXn/UkoqH3QdWZC1aLs9NCl4F8ZE06g -jjvN9WdsCXmTEShqaXoRsZG7SfcQsu4gUUZ/fjbJ5hRf+QxMMKv42SUpqsRhslEF -/Pqu0WQd82CgG1a7XnfUO8BYSchTJZL55vx40ZZuQAu/ULsF7toa0lktijBxCPn3 -8HEnyLEyA3e8a93P0myWoxFn/fUpegT3TVSv33anqwIDAQABozUwMzALBgNVHQ8E -BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkq -hkiG9w0BAQsFAAOCAgEA1Bn7mpa2eJUo4+1X5lVLWWwtXLAfKiBf6OWNfacLV6FL -gyKpvvESTGuA5VAS0O97TPd7uyzEbUMS75TdmfAT8zecO2aXMb7aTyX+QbMj2gmk -zou72Fl4o6V1IvYpjKaNBZCS3Hk67ivRYbQCamEOk5UX9/wCdLvC9PH5Y+WqcPaz -7RLXe3OXhRbfFax4+pWzZxsgSKrEi8ZZ5gRa/bdJVVsTqk9LwS/CbMjEAkdzIBLt -cQb9BcnTJcQvp6ehNIVMdEC7GLXcDkefw7CL1ZfEh3DoJD3hiR6QwdWtdG0etoUf -w8LHZhCJD0IZxLMHiE+qiN4xkx+cznol+gAc9sfmtVK1CAW9l1Aa8zw5AfAyCg3h -jr6ymfwY8zlO21yBmCTg2+yTbU/0CqkgimQeztoYCh7+67QgnSCJMk2ffR6GPj1q -pfLI/5QNoxdFvR/lkwj5h/HRp9JZKTV/R/g0Va4Arg3Y7RTezjCYkJnX37ScnQhg -JLIeXmksFkc+Oz3yA+r60rR72+lsVzE87BCs+L0y16zcQnU5NqJXrSMMqCkjbs9l -b682+tnJKLFGQrYia/FL/Sc2L2Tn5hba5wWQTMjGujg76fkMc6VIv1qG3VGR/V1G -r11UJ+WjEcdrwZUm7E76p9DfTce52kGqGXwfrv6kQjvLhipwjzgv429txzDy82k= +BAMMCndlYjNzaWduZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCr +JajqnvRQEREph+zu7rw1QjHQG1x2H44SJSMjX1Wzi9FErlRSOzywPFL2AzGsNvNS +tPmxN/kF9mBjQIQHxo90M4GcZgW1aljPaXLvQWFrP9ak+JjHuUG+j51fVJp8F2Qc +BG8i2LjjSLvkEYSULHI0kbMPws+DKcemvZJ6IhkoPkbtnx5Z1zDj8D6vvWGJguMO +VSNJY7SoBNuSB6CJ7wCWBg7UPtTUrtnuJVvUh+3k2wc7LJ+C9wd7rt+qYb8LxQuc +j8dDyncXXeI583oGzjTE+1kFrE5TuMDlnWyKPa6NQPeXQtygFTyQL9RMW6JkgWWg +tDFWqd2Mgb8sCRtl5uTJFGJ7PFBP4T69JqYhz817tDS3JrMbbzzhRzf3cB6V2NCC +zVKBrO7gfAyDwWVr5iUyaXhLGyzuGg2nMbFMj/Pr7csravs+Jq5apwyZDNTv+2WQ +xP6d2gGFwQOxcPt4OGPjtFpVHH3cxLkcGsSOZ31akuhdSJ6MqWI4tkgRpsf5Ff0+ +z8SLZaCQIp7M4O4LpMreAT7smvEQpLphK1oKWlsY6ukkJ1y8KD3EfeJRpDL0PBTy +jacQATPsqUzeryCfqAMulLLqUbNFqv6Slhzt2vr+lfIr+IeUa/7XMeZOZJu1T/7n +fTjpdokSTx8DageE4Z3j90q5d4hdXvMWq6MpQW7RqQIDAQABo1QwUjALBgNVHQ8E +BAMCBDAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATAdBgNV +HQ4EFgQUsBCvmwLPQDG+iN5qI6P7SgLZyP0wDQYJKoZIhvcNAQELBQADggIBAE/j +mwchm30rB+dheTRBcVD0yHgYL2tQlpfKZeX9JDVWNMQ5OYHtMVwdD7RBQJ2ypqIr +5VP6/Hf0M1GE03mnDpjv29q57AkuGFZpBvZ+1XCG87336QIPqkPR4uMJ86MalsX2 +f9GHMG4H0rd1j+ozM0jhJNoVG4lSq/GNn2E9oRjMG8lq0M7apWwK1FQUBECIlfw+ +tk9aq2zLl409vuqzgsWeffBcdVEDHGCLQauzYRtxMBbzLb33gWWd+9149dWeG5up +P0CZvdetgXhlcbusmrBWVn0O57/QDaGzEUZKxqoy8Ncv04KMYN1gOF+nO5cKn0R1 ++4yvb/NJTdo9WcdLcleqSL1Ju3kX1dCIPOpuaZ3aEwLHrvlNxT8Y5OMvRsYPINAU +6JfNGu21+Bq2nEqSqrw8Ys2hdGI+E95uXjPcsm8BZRCfxfkEeYVtx4ZaqMF+bkfD +d+uePSFp4VBWbg40RMVymr1YcNTX3CjvtLZDH4BZBdx/8YjUEUYPpC7xGoaQDGvA ++J9cVHRpxYpry5fbBmSvrKvKXU6aijLpM7etjYWzYFturpi52Ya9h3LIHd4RaBzB +0YzmatirLK/07YBUECsVcAlddIK5KOA5Nd7+oUikmrR1wMY+I/hym6fSTZGo/TDY +vDFERRj1XOOhlCzHx94SS1DS0rVTAj4uxbuZisaz -----END CERTIFICATE----- diff --git a/testing/web3signer_tests/tls/web3signer/key.key b/testing/web3signer_tests/tls/web3signer/key.key index 6e5171f37..42b7ad9a9 100644 --- a/testing/web3signer_tests/tls/web3signer/key.key +++ b/testing/web3signer_tests/tls/web3signer/key.key @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -MIIJRQIBADANBgkqhkiG9w0BAQEFAASCCS8wggkrAgEAAoICAQDraQUU4O7K/aBA -iH86RV3ye/Q7vguwplUNku317chzyFdB+OnGSUga6+zjdUmr8+49nki1q0rLEU/x -J0NpffTdzFgk1nk6Jh7Ly26q18SNpwpuwdvbajnTeh+BPSWZQL85xfO9th/RkJkg -pzKukxK/npjvU6PbwiufSWI7mXNIgR0lIIacFXZ4RsD1PxZo/07ktoF0N+yLGW76 -yfeINRw43bG1MQxklePsk6zAUqJEi0tZmXqzh1NZHH5Q1VAEKKPWyAVTDi3bWmvh -3iSfgmckesjwUHANFeMhLpdiVTOi31OaILpx9HGRYYnqjW1AUZLoSMKkyPsm6IN6 -0GpAVI7TP3URVpTPPW78UeEUyeYN06tABYJsFWGFChg9Hf2yvcZU2DDGdHpxut6h -4WAwx9oL5rG4VSxFjhVi6ty3Hb9B0YFE/WNfV07wWPSQADZSK/ktfhE+8zavQzjs -xm2f1Ko5L/x8cIc5MS1xyaXn/UkoqH3QdWZC1aLs9NCl4F8ZE06gjjvN9WdsCXmT -EShqaXoRsZG7SfcQsu4gUUZ/fjbJ5hRf+QxMMKv42SUpqsRhslEF/Pqu0WQd82Cg -G1a7XnfUO8BYSchTJZL55vx40ZZuQAu/ULsF7toa0lktijBxCPn38HEnyLEyA3e8 -a93P0myWoxFn/fUpegT3TVSv33anqwIDAQABAoICAQDihR2kp4Rfw4luT2nNUm5C -JFAxJH/vLT5uX1Gm8XWPI9oC21dnu6Asd5RskrGfSouWszZXyUmg+TmpXRSa796t -hjHS0KW59HBxvYDx18mEXJXHWbcK/L5D5iFmpMYHH6xiFT6i8BrR9ofCSeCU52SF -CkEzGZJ0pfR/w4dIvjGWNNcsoI2mp2hl9/84fco8ol7x6UPL5vwwJPsLS0hqwmAz -v+994IKCT1EQllEGhv0pY7fPscXF9pOXDbnmYjwqpEhzJekpsF0j03A32R/4dOx2 -x8eOpngLv2Hczg5RSpbzRF4X0yJVANg/AlJJZmkYGOZ5qXnSQqqZF+dcSCvVVwhO -GS7uci6Mcy7Ov0Gj9HWX8As0SofPtUMuO7k/nJYOzcgY+4agyIDrylIeG86gdCDQ -hGVz+T5reJZIBMp66GPT6M8r36q50cx2x9nJjxLlIjvly1EruVjQoSMUfjewHG91 -xJI0iFhlbBrCpyLx3X0smMEr0vJzM5J0GtdxQdcSocDy5244+4zuslAXgsEYwHYx -WYFMsotRif8aB2b3OSt0yH+Heh06dZehvwWa4F4/3qlP48e0/CWIL7Y/tBgZv8Gh -n3F7HsHvMx6qQqMY5AxudbkpKdM9W84gXriHPIsO2oZEU6N65J/Bpq5Ve4JBlih1 -Ji0CtvHlAR2dhGkj6Q36MQKCAQEA9z/HTd8hd4FNEfn595OVWr9CeZc1zAlNa94I -lvTLhLEFcMkGsZd9KyV87MOV3p9m+XI7UJmqllIHOkwrECF2wzFssguPk+RAJ5hW -LZJgsF0fPnhX0qJFXzSNzzqAICES6+s9jvHMO9PhtF59uv4zsRFEBmKAr0AN8Zsk -rEk+2Tl2RgC+sxzRS767De9CrbSjxm+qAHuFFh8QX/N/mPoLUa+V5Oh2srA5bTHn -t0vyfQQ9+gqTBJDy51VGYlYw5OQBAiOPTgzbSmm2gqdWYgGn2Sp5IBQLF5nGGGsV -70DvnsoxViqpsv+yObAF9PqXnu6UGoB023Jr8x683bU9/jQFLQKCAQEA8735Vbbc -kncVJIpIlG7SDPmlLCFnxokvWWmyJS2J4SrIJJykn30qknGGZFFn67NB5PulAEaw -mdG58FIxxkm8bEKwebEhdnB9sP8k3TvddPKlBXYb1WuGxzyF/xlHniEJ7jN0YAAz -D1BLxTP1OM47iX5ocyVpOPbAdq/yZK0bffvIUy/QKLeJNx0d59PKpJRb4y6T/LvS -tp3UHrBqCNYYoKsZluS9Kg6WJF4g269yn2LSdtzQlAW1IT3DgO7h+2UBYI4FwMao -BZVew44CjljGCTA2KL4jdsqnTyt0qzzAiJZ0CGkJY9gal16ODHcBUKfNGYvjU8pf -2qDEiCn0HayXNwKCAQEAlOscLuHy9Viyw94NWbnRXlwOPM//fgooUIzmHY4Jur0o -arsZxgNZR5CHws82yGS4EAxmf3Bel7WGVu2jjk6pin2NW1utOcVjgrW1SjN8+xzL -gcPYGazVHbe4phU1MKTbEa+ZXyxx96LxscKr9eG/3qlokHPp0CRDgb8RApgHO6zp -eNZgBd+YjAewAH+YaKmBbza4bRv4l89T/Ibb1pbcFHIuVTZSr+OGYyeIyhT7U6Mn -dR/DVx+6vezVvMrvHh3aIaCrYrZJqnMrk1wYomUe5KU5WUHZQHjFINX22ykAamKb -/qsplP9/KFHF9Lyub/KAz8mJGNe8/y0HUn4kfaR1bQKCAQEAhZHTsx8UXMcZNP76 -qyzXuviLhVWBExFWez8quqjr6BKTv0yAAk6LJ9lCdnMN6eI/+AXW9AHJAWIm7QV9 -9VWvBfy9zNI+rjMTDg2j3ADUaSQXPpjsw9W69C+8loD5+DPOx1Q3L+ysDnZIL3c7 -qLeLdNtqzb7wnKDL876TrIwYhr+VldCb19RMQ4GXQ9WSNQKAIE0EF/mtjRmMhozS -bqk0scdRrJkI+KUpriBPDVRmEeYLw8taGePO0LqSCnPeLu+5A3qQuIWkyfqDBdMq -n2sSizJ6W3Vm5dBEQ2Ri+Pu/3pnkWD+HP8nLOKw+V6JXfCWYhaldGCvMv3heeufS -uPg9nQKCAQEAp/boT63JB+ahU3VQGtqwlDXkRS/Ge8a7FRp4kjdK7d1mtUDqOJ9U -l2RHgOkqhNuAPy64/07caDK3R7vKeOFmSXCV/WHIcgt46SRwFQECZeyA1R+EkTes -tseTngdFrQ10Xf+DmLNqCyX5KpgQf+ccluyyH6uK6FRI/VfU4sLrUGyOblqHq/c4 -bRR4nMwiw5yga45YhQH8uJF54MI7XaD2/hPCAIJBkx88taRzMUlWl1u1VQosIvtZ -5hCRepq9A44P61c+HI/5fzXAn2xvwR2EiV0hAYLn+rmYgBId/RfcstWUR78A9wpT -/OsV3MTX1gCaTE9Q2GlZVybDh20ZvdBC/g== +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCrJajqnvRQEREp +h+zu7rw1QjHQG1x2H44SJSMjX1Wzi9FErlRSOzywPFL2AzGsNvNStPmxN/kF9mBj +QIQHxo90M4GcZgW1aljPaXLvQWFrP9ak+JjHuUG+j51fVJp8F2QcBG8i2LjjSLvk +EYSULHI0kbMPws+DKcemvZJ6IhkoPkbtnx5Z1zDj8D6vvWGJguMOVSNJY7SoBNuS +B6CJ7wCWBg7UPtTUrtnuJVvUh+3k2wc7LJ+C9wd7rt+qYb8LxQucj8dDyncXXeI5 +83oGzjTE+1kFrE5TuMDlnWyKPa6NQPeXQtygFTyQL9RMW6JkgWWgtDFWqd2Mgb8s +CRtl5uTJFGJ7PFBP4T69JqYhz817tDS3JrMbbzzhRzf3cB6V2NCCzVKBrO7gfAyD +wWVr5iUyaXhLGyzuGg2nMbFMj/Pr7csravs+Jq5apwyZDNTv+2WQxP6d2gGFwQOx +cPt4OGPjtFpVHH3cxLkcGsSOZ31akuhdSJ6MqWI4tkgRpsf5Ff0+z8SLZaCQIp7M +4O4LpMreAT7smvEQpLphK1oKWlsY6ukkJ1y8KD3EfeJRpDL0PBTyjacQATPsqUze +ryCfqAMulLLqUbNFqv6Slhzt2vr+lfIr+IeUa/7XMeZOZJu1T/7nfTjpdokSTx8D +ageE4Z3j90q5d4hdXvMWq6MpQW7RqQIDAQABAoICAAajqX2/kJn+DYDDUoJS3deB +k8HfW9sDvpzO1sH/p+kVEJdV3XTKskAiePPs/AtynsYbue+BbL5J2GOlTDHqhqRi +/qFQ9mH7KAqUzEYCjutMkNC5yaB+2/Fu7BOXij4r4KDzHZYYGvULPGm8sbxXTI9k +QxJmk+sCTwnVgxYMllYAs3ryoChrUAzZpC7oXX0qiBElZZ7qWKbneFaeB+Dt9gN7 +5O2gKdy90zu5NIqmQsjs48cMhDweBerrBed7zv/fgyOt0rS7KRtNk7H8k2Rp8bNe +Dk4paOj3yvjlXmFvAuNdLLWwHPOzWzP7PQTAzcgRGn6NWvgiExOJMX+9czQE7OVd +OY47PndUFU6zkiOMYipnsEOFrZvHrvuCquQ+5X6x8PXdK4aFJ8VphH2HTo6xXr6E +q3zTHZq7rXSuI2yLBE6JslqP3D2H022cow6iLGnuJKYVXMOcOOTrrVBJjjau/OfN +feOvEgut6T7BmdWrcdgQzh3rvvMKdawdekuQgPjNfLxR5JCjWKaKqkJ1iBZ1jkiC +LqoeelsJnWSG+P9QKO+ntt3TW7qUsMPBAHIk2UqbsZcnX9La9huiIfABP1L1qGTb +WQJiIumyCY7LDEKcaqrFbsBS45xoQVoVlDeJPAFk48947mZY+m6TnwEC/K000ENU +fYS0x+CsNmEaXGbItrZBAoIBAQDouRfE1B/bl8KktK3uQ+wwlTWpiZnzji8wg8FG +O68BsL1qmxDG0eShBQzwNdFY9HTgGu/BjPz02liXY+smB1DXgk1tuP6NXl7ZakE4 +gdaL9wifjvoTqzgf3nBJguUAxGRBpYzbYRMELnw/FSjwLykpGUTSv+jKhOqNqb8r +T/JIFq/DG2oioYuzksEdDNaWOD3CkTjkA4guBvM5iONSed4VIn4C/L31jNFXeG1u +ToowtFLr8zG2h6sfI2NWHD8cR1LKQA6hSaimrrHUFYBo4qzNJ7afVFkF/zO37UGL +isNAmMQfFE7Lqom7YcI+QRDhtBX3XsvN3Y/RPQASZWtOTr/BAoIBAQC8Q+ggBpVK +En2CWXTvoBys9Ad3le50RIH3pmM4Uv1AQeNNtT6PKRKiL18stRxDql0oGCslOJh4 +FvawJGfANVN0vu3aIwG6kg6myYxn4sP9x2VeQUktaKcdCZ4oVuG2aXwCeg92Cpmz +W7jok8qvWjmN8IDBM4iN2Q5auO0Xg7n6vjZ6EBkm+XCsIzSazgN2sLoNC2RUKbVT +U6shGkPGhHJwumXtcPp+Ogljlv/8Gc+oc5Ty+hdhmMzTGDYwy3bwd4yfIFRRSmCr +OS0V2cwnsUQkmH0c5DVVIa0s1i+nqM2epvxjQOIsBJpEwzHXY00YZb5d4jeELPqU +XUhnrKqKxQvpAoIBAFHTerL/LrBkPNDyolErWql+XR7ePd4v+RGi0dsi8xayEPeh +zBVMCYpAH1t6YMBZO5rsfa5dJzfkac/ZFv4JBniv3Q+eQwprywfA32vB4zDVTBfm +CrHNuu8ho/OE7YYGh4W5crxT9n665X68ruc8fclwlA1R4sUKVPo4W/obowGL0ILW +acwBZwBdsj7Hm8+3uKdnrkwlncUpNm3dXqhKJzbhKNNeEGB9AcIymq91OAuF674A +hVM7goRxSeUmC16jCU4JldtJ7d2lgOskIEFAqid8Ni7xVlfQclvSNQCeaqaU0Chp +WIct0D2tUsHW2NuzGSIgF6Krq3yTaSoOtNsUv0ECggEAFR9lVtqGO3ZRoKNazFvh +e8IxaEhpJaBeGKQRc8tT4LbDwv830qYgEhRQkFqNnkXqB8qWZKmx6Z9h9CdRgK46 ++9lEJHpTAlTK0gnA+BLoPHv3spiOlkqsnURr+0isMGQrZre9LlhIIGiFGYsjbYMo ++/Tk7UhT5N5ajvE6oK3F2w0mXZGa0NWhv55/k3LTzqhLZ5VEn3DCiGPVynQA8LAB +iwZO01IeuLTYQtU5SVa4BsVZC93la6zSJkkMI3Ngl+BB5cSh0TEQIYXbuhzim/12 +kMiPGQO9vBx4KpSpah01XLyNirFH7vphOJ/R4sGgb8FSl4P/CJRnVOgWbJNh2wn6 +qQKCAQAkZMqlOokxcpiNLDyBS33mLzVuVIXIBsKmZibmurWxcXvyHGA7K/uHRvE/ +5pajoO8Pw9dQhAX2LmOISW8YJwR0UR9LmDOeYUW+8nypG2jprKezMVSNu+lWHanE +vw+fLvRWyDEdKQK6RHOytHppFn48eC5HrPdOe4EaNQ09vUiMsJmVL6ep4nuAg4nr +WilB9iJQtrFcItB5tnfD2puJQKaFV3rgqWCFIgJJg0ThuiWyoVNKtlRvv5o3mQyz +Y+jyCm4RtgSDm9+e/Tcv2vUeoiNt2bVb9tK3r2M2cZ6N1PuHV/cmBjf6I/ssPqmM +CXDusRSlsQNpzHc6QKq8IDZLut9g -----END PRIVATE KEY----- diff --git a/testing/web3signer_tests/tls/web3signer/key.p12 b/testing/web3signer_tests/tls/web3signer/key.p12 index 459f4fb62..51269be8d 100644 Binary files a/testing/web3signer_tests/tls/web3signer/key.p12 and b/testing/web3signer_tests/tls/web3signer/key.p12 differ diff --git a/testing/web3signer_tests/tls/web3signer/known_clients.txt b/testing/web3signer_tests/tls/web3signer/known_clients.txt index de80bb7ce..330132731 100644 --- a/testing/web3signer_tests/tls/web3signer/known_clients.txt +++ b/testing/web3signer_tests/tls/web3signer/known_clients.txt @@ -1 +1 @@ -lighthouse 1B:43:E1:58:26:7D:3F:70:BD:DA:32:E9:29:A5:A9:50:EA:B2:A8:C3:0C:82:BF:90:13:ED:5B:E0:7D:5B:0A:C0 +lighthouse FF:4C:84:A6:37:28:EC:7E:A7:D8:C6:49:0D:C6:F9:5D:C1:06:BA:6D:69:49:0A:AA:38:32:01:2B:ED:D9:F2:FA diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 494ebcb3d..46f523c99 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -2,65 +2,63 @@ name = "validator_client" version = "0.3.5" authors = ["Paul Hauner <paul@paulhauner.com>", "Age Manning <Age@AgeManning.com>", "Luke Anderson <luke@lukeanderson.com.au>"] -edition = "2021" +edition = { workspace = true } [lib] name = "validator_client" path = "src/lib.rs" [dev-dependencies] -tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } -logging = { path = "../common/logging" } +tokio = { workspace = true } [dependencies] -tree_hash = "0.5.0" -clap = "2.33.3" -slashing_protection = { path = "./slashing_protection" } -slot_clock = { path = "../common/slot_clock" } -types = { path = "../consensus/types" } -safe_arith = { path = "../consensus/safe_arith" } -serde = "1.0.116" +tree_hash = { workspace = true } +clap = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +types = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } serde_derive = "1.0.116" -bincode = "1.3.1" -serde_json = "1.0.58" -slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } -tokio = { version = "1.14.0", features = ["time"] } -tokio-stream = { version = "0.1.3", features = ["sync"] } -futures = "0.3.7" -dirs = "3.0.1" -directory = { path = "../common/directory" } -lockfile = { path = "../common/lockfile" } -environment = { path = "../lighthouse/environment" } -parking_lot = "0.12.0" -exit-future = "0.2.0" -filesystem = { path = "../common/filesystem" } -hex = "0.4.2" -deposit_contract = { path = "../common/deposit_contract" } -bls = { path = "../crypto/bls" } -eth2 = { path = "../common/eth2" } -tempfile = "3.1.0" -validator_dir = { path = "../common/validator_dir" } -clap_utils = { path = "../common/clap_utils" } -eth2_keystore = { path = "../crypto/eth2_keystore" } -account_utils = { path = "../common/account_utils" } -lighthouse_version = { path = "../common/lighthouse_version" } -warp_utils = { path = "../common/warp_utils" } -warp = "0.3.2" -hyper = "0.14.4" -ethereum_serde_utils = "0.5.0" -libsecp256k1 = "0.7.0" -ring = "0.16.19" -rand = { version = "0.8.5", features = ["small_rng"] } -lighthouse_metrics = { path = "../common/lighthouse_metrics" } -lazy_static = "1.4.0" -itertools = "0.10.0" -monitoring_api = { path = "../common/monitoring_api" } -sensitive_url = { path = "../common/sensitive_url" } -task_executor = { path = "../common/task_executor" } -reqwest = { version = "0.11.0", features = ["json","stream"] } -url = "2.2.2" -malloc_utils = { path = "../common/malloc_utils" } -sysinfo = "0.26.5" +bincode = { workspace = true } +serde_json = { workspace = true } +slog = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +futures = { workspace = true } +dirs = { workspace = true } +directory = { workspace = true } +lockfile = { workspace = true } +environment = { workspace = true } +parking_lot = { workspace = true } +exit-future = { workspace = true } +filesystem = { workspace = true } +hex = { workspace = true } +deposit_contract = { workspace = true } +bls = { workspace = true } +eth2 = { workspace = true } +tempfile = { workspace = true } +validator_dir = { workspace = true } +clap_utils = { workspace = true } +eth2_keystore = { workspace = true } +account_utils = { workspace = true } +lighthouse_version = { workspace = true } +warp_utils = { workspace = true } +warp = { workspace = true } +hyper = { workspace = true } +ethereum_serde_utils = { workspace = true } +libsecp256k1 = { workspace = true } +ring = { workspace = true } +rand = { workspace = true, features = ["small_rng"] } +lighthouse_metrics = { workspace = true } +lazy_static = { workspace = true } +itertools = { workspace = true } +monitoring_api = { workspace = true } +sensitive_url = { workspace = true } +task_executor = { workspace = true } +reqwest = { workspace = true } +url = { workspace = true } +malloc_utils = { workspace = true } +sysinfo = { workspace = true } system_health = { path = "../common/system_health" } -logging = { path = "../common/logging" } - +logging = { workspace = true } diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 278dc22d0..cc90c979b 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -2,7 +2,7 @@ name = "slashing_protection" version = "0.1.0" authors = ["Michael Sproul <michael@sigmaprime.io>", "pscott <scottpiriou@gmail.com>"] -edition = "2021" +edition = { workspace = true } autotests = false [[test]] @@ -10,21 +10,21 @@ name = "slashing_protection_tests" path = "tests/main.rs" [dependencies] -tempfile = "3.1.0" -types = { path = "../../consensus/types" } -rusqlite = { version = "0.28.0", features = ["bundled"] } -r2d2 = "0.8.9" +tempfile = { workspace = true } +types = { workspace = true } +rusqlite = { workspace = true } +r2d2 = { workspace = true } r2d2_sqlite = "0.21.0" -serde = "1.0.116" +serde = { workspace = true } serde_derive = "1.0.116" -serde_json = "1.0.58" -ethereum_serde_utils = "0.5.0" -filesystem = { path = "../../common/filesystem" } -arbitrary = { version = "1.0", features = ["derive"], optional = true } +serde_json = { workspace = true } +ethereum_serde_utils = { workspace = true } +filesystem = { workspace = true } +arbitrary = { workspace = true, features = ["derive"] } [dev-dependencies] -lazy_static = "1.4.0" -rayon = "1.4.1" +lazy_static = { workspace = true } +rayon = { workspace = true } [features] -arbitrary-fuzz = ["arbitrary", "types/arbitrary-fuzz"] +arbitrary-fuzz = ["types/arbitrary-fuzz"] diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index c8be85147..406913bfd 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -23,7 +23,7 @@ pub const POOL_SIZE: u32 = 1; #[cfg(not(test))] pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); #[cfg(test)] -pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100); +pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(500); /// Supported version of the interchange format. pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 5; diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index a7118aa94..f0a9258c7 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -2,12 +2,12 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; use crate::{ duties_service::{DutiesService, DutyAndProof}, http_metrics::metrics, - validator_store::ValidatorStore, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, OfflineOnFailure, }; use environment::RuntimeContext; use futures::future::join_all; -use slog::{crit, error, info, trace}; +use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; @@ -395,6 +395,20 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { .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!( + log, + "Missing pubkey for attestation"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "validator" => ?duty.pubkey, + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ); + None + } Err(e) => { crit!( log, @@ -527,10 +541,20 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> { .await { Ok(aggregate) => Some(aggregate), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for aggregate"; + "pubkey" => ?pubkey, + ); + None + } Err(e) => { crit!( log, - "Failed to sign attestation"; + "Failed to sign aggregate"; "error" => ?e, "pubkey" => ?duty.pubkey, ); diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 61a5a094c..094b85bf8 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -5,9 +5,13 @@ use crate::{ graffiti_file::GraffitiFile, OfflineOnFailure, }; -use crate::{http_metrics::metrics, validator_store::ValidatorStore}; +use crate::{ + http_metrics::metrics, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, +}; use environment::RuntimeContext; -use eth2::BeaconNodeHttpClient; +use eth2::{BeaconNodeHttpClient, StatusCode}; +use slog::Logger; use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::fmt::Debug; @@ -338,35 +342,61 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { let log = log.clone(); self.inner.context.executor.spawn( async move { - let publish_result = if builder_proposals { - let mut result = service.clone() + if builder_proposals { + let result = service + .clone() .publish_block::<BlindedPayload<E>>(slot, validator_pubkey) .await; - match result.as_ref() { + match result { Err(BlockError::Recoverable(e)) => { - error!(log, "Error whilst producing a blinded block, attempting to \ - publish full block"; "error" => ?e); - result = service + error!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "blinded proposal failed, attempting full block" + ); + if let Err(e) = service .publish_block::<FullPayload<E>>(slot, validator_pubkey) - .await; - }, - Err(BlockError::Irrecoverable(e)) => { - error!(log, "Error whilst producing a blinded block, cannot fallback \ - because the block was signed"; "error" => ?e); - }, - _ => {}, + .await + { + // Log a `crit` since a full block + // (non-builder) proposal failed. + crit!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "full block attempted after a blinded failure", + ); + } + } + Err(BlockError::Irrecoverable(e)) => { + // Only log an `error` since it's common for + // builders to timeout on their response, only + // to publish the block successfully themselves. + error!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "this error may or may not result in a missed block", + ) + } + Ok(_) => {} }; - result - } else { - service - .publish_block::<FullPayload<E>>(slot, validator_pubkey) - .await - }; - if let Err(e) = publish_result { + } else if let Err(e) = service + .publish_block::<FullPayload<E>>(slot, validator_pubkey) + .await + { + // Log a `crit` since a full block (non-builder) + // proposal failed. crit!( log, "Error whilst producing block"; - "message" => ?e + "message" => ?e, + "block_slot" => ?slot, + "info" => "proposal did not use a builder", ); } }, @@ -391,17 +421,31 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { BlockError::Recoverable("Unable to determine current slot from clock".to_string()) })?; - let randao_reveal = self + let randao_reveal = match self .validator_store .randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch())) .await - .map_err(|e| { - BlockError::Recoverable(format!( + { + Ok(signature) => signature.into(), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently removed + // via the API. + warn!( + log, + "Missing pubkey for block randao"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "slot" => ?slot + ); + return Ok(()); + } + Err(e) => { + return Err(BlockError::Recoverable(format!( "Unable to produce randao reveal signature: {:?}", e - )) - })? - .into(); + ))) + } + }; let graffiti = determine_graffiti( &validator_pubkey, @@ -496,11 +540,31 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { .await?; let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES); - let signed_block = self_ref + let signed_block = match self_ref .validator_store .sign_block::<Payload>(*validator_pubkey_ref, block, current_slot) .await - .map_err(|e| BlockError::Recoverable(format!("Unable to sign block: {:?}", e)))?; + { + Ok(block) => block, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently removed + // via the API. + warn!( + log, + "Missing pubkey for block"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "slot" => ?slot + ); + return Ok(()); + } + Err(e) => { + return Err(BlockError::Recoverable(format!( + "Unable to sign block: {:?}", + e + ))) + } + }; let signing_time_ms = Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); @@ -530,12 +594,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { beacon_node .post_beacon_blocks(&signed_block) .await - .map_err(|e| { - BlockError::Irrecoverable(format!( - "Error from beacon node when publishing block: {:?}", - e - )) - })? + .or_else(|e| handle_block_post_error(e, slot, log))? } BlockType::Blinded => { let _post_timer = metrics::start_timer_vec( @@ -545,12 +604,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { beacon_node .post_beacon_blinded_blocks(&signed_block) .await - .map_err(|e| { - BlockError::Irrecoverable(format!( - "Error from beacon node when publishing block: {:?}", - e - )) - })? + .or_else(|e| handle_block_post_error(e, slot, log))? } } Ok::<_, BlockError>(()) @@ -571,3 +625,29 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> { Ok(()) } } + +fn handle_block_post_error(err: eth2::Error, slot: Slot, log: &Logger) -> Result<(), BlockError> { + // Handle non-200 success codes. + if let Some(status) = err.status() { + if status == StatusCode::ACCEPTED { + info!( + log, + "Block is already known to BN or might be invalid"; + "slot" => slot, + "status_code" => status.as_u16(), + ); + return Ok(()); + } else if status.is_success() { + debug!( + log, + "Block published with non-standard success code"; + "slot" => slot, + "status_code" => status.as_u16(), + ); + return Ok(()); + } + } + Err(BlockError::Irrecoverable(format!( + "Error from beacon node when publishing block: {err:?}", + ))) +} diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 6e199cb17..0af92a9e3 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -170,6 +170,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-address") .long("http-address") + .requires("http") .value_name("ADDRESS") .help("Set the address for the HTTP address. The HTTP server is not encrypted \ and therefore it is unsafe to publish on a public network. When this \ @@ -189,14 +190,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-port") .long("http-port") + .requires("http") .value_name("PORT") .help("Set the listen TCP port for the RESTful HTTP API server.") - .default_value("5062") + .default_value_if("http", None, "5062") .takes_value(true), ) .arg( Arg::with_name("http-allow-origin") .long("http-allow-origin") + .requires("http") .value_name("ORIGIN") .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ Use * to allow any origin (not recommended in production). \ @@ -204,6 +207,26 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { address of this server (e.g., http://localhost:5062).") .takes_value(true), ) + .arg( + Arg::with_name("http-allow-keystore-export") + .long("http-allow-keystore-export") + .requires("http") + .help("If present, allow access to the DELETE /lighthouse/keystores HTTP \ + API method, which allows exporting keystores and passwords to HTTP API \ + consumers who have access to the API token. This method is useful for \ + exporting validators, however it should be used with caution since it \ + exposes private key data to authorized users.") + .takes_value(false), + ) + .arg( + Arg::with_name("http-store-passwords-in-secrets-dir") + .long("http-store-passwords-in-secrets-dir") + .requires("http") + .help("If present, any validators created via the HTTP will have keystore \ + passwords stored in the secrets-dir rather than the validator \ + definitions file.") + .takes_value(false), + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::with_name("metrics") @@ -214,22 +237,25 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("metrics-address") .long("metrics-address") + .requires("metrics") .value_name("ADDRESS") .help("Set the listen address for the Prometheus metrics HTTP server.") - .default_value("127.0.0.1") + .default_value_if("metrics", None, "127.0.0.1") .takes_value(true), ) .arg( Arg::with_name("metrics-port") .long("metrics-port") + .requires("metrics") .value_name("PORT") .help("Set the listen TCP port for the Prometheus metrics HTTP server.") - .default_value("5064") + .default_value_if("metrics", None, "5064") .takes_value(true), ) .arg( Arg::with_name("metrics-allow-origin") .long("metrics-allow-origin") + .requires("metrics") .value_name("ORIGIN") .help("Set the value of the Access-Control-Allow-Origin response HTTP header. \ Use * to allow any origin (not recommended in production). \ @@ -333,6 +359,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value("true") .takes_value(true), ) + .arg( + Arg::with_name("validator-registration-batch-size") + .long("validator-registration-batch-size") + .value_name("INTEGER") + .help("Defines the number of validators per \ + validator/register_validator request sent to the BN. This value \ + can be reduced to avoid timeouts from builders.") + .default_value("500") + .takes_value(true), + ) /* * Experimental/development options. */ diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index fa297dcfe..7c662db93 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -77,6 +77,8 @@ pub struct Config { pub disable_run_on_all: bool, /// Enables a service which attempts to measure latency between the VC and BNs. pub enable_latency_measurement_service: bool, + /// Defines the number of validators per `validator/register_validator` request sent to the BN. + pub validator_registration_batch_size: usize, } impl Default for Config { @@ -117,6 +119,7 @@ impl Default for Config { gas_limit: None, disable_run_on_all: false, enable_latency_measurement_service: true, + validator_registration_batch_size: 500, } } } @@ -291,6 +294,14 @@ impl Config { config.http_api.allow_origin = Some(allow_origin.to_string()); } + if cli_args.is_present("http-allow-keystore-export") { + config.http_api.allow_keystore_export = true; + } + + if cli_args.is_present("http-store-passwords-in-secrets-dir") { + config.http_api.store_passwords_in_secrets_dir = true; + } + /* * Prometheus metrics HTTP server */ @@ -380,6 +391,12 @@ impl Config { config.enable_latency_measurement_service = parse_optional(cli_args, "latency-measurement-service")?.unwrap_or(true); + config.validator_registration_batch_size = + parse_required(cli_args, "validator-registration-batch-size")?; + if config.validator_registration_batch_size == 0 { + return Err("validator-registration-batch-size cannot be 0".to_string()); + } + /* * Experimental */ diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 83cdb936a..a3b3cabcc 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -932,6 +932,20 @@ async fn fill_in_selection_proofs<T: SlotClock + 'static, E: EthSpec>( for result in duty_and_proof_results { let duty_and_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!( + log, + "Missing pubkey for duty and proof"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + ); + // Do not abort the entire batch for a single failure. + continue; + } Err(e) => { error!( log, @@ -1007,7 +1021,7 @@ async fn fill_in_selection_proofs<T: SlotClock + 'static, E: EthSpec>( /// 2. We won't miss a block if the duties for the current slot happen to change with this poll. /// /// This sounds great, but is it safe? Firstly, the additional notification will only contain block -/// producers that were not included in the first notification. This should be safety enough. +/// producers that were not included in the first notification. This should be safe enough. /// However, we also have the slashing protection as a second line of defence. These two factors /// provide an acceptable level of safety. /// diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs index 7a852091a..cf63d8ac6 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/src/duties_service/sync.rs @@ -2,7 +2,10 @@ use crate::beacon_node_fallback::{OfflineOnFailure, RequireSynced}; use crate::{ doppelganger_service::DoppelgangerStatus, duties_service::{DutiesService, Error}, + http_metrics::metrics, + validator_store::Error as ValidatorStoreError, }; + use futures::future::join_all; use itertools::Itertools; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; @@ -425,6 +428,10 @@ pub async fn poll_sync_committee_duties_for_period<T: SlotClock + 'static, E: Et RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { + let _timer = metrics::start_timer_vec( + &metrics::DUTIES_SERVICE_TIMES, + &[metrics::VALIDATOR_DUTIES_SYNC_HTTP_POST], + ); beacon_node .post_validator_duties_sync(period_start_epoch, local_indices) .await @@ -539,6 +546,18 @@ pub async fn fill_in_aggregation_proofs<T: SlotClock + 'static, E: EthSpec>( .await { Ok(proof) => proof, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync selection proof"; + "pubkey" => ?pubkey, + "pubkey" => ?duty.pubkey, + "slot" => slot, + ); + return None; + } Err(e) => { warn!( log, diff --git a/validator_client/src/http_api/create_signed_voluntary_exit.rs b/validator_client/src/http_api/create_signed_voluntary_exit.rs index b777d1580..a9586da57 100644 --- a/validator_client/src/http_api/create_signed_voluntary_exit.rs +++ b/validator_client/src/http_api/create_signed_voluntary_exit.rs @@ -1,5 +1,6 @@ use crate::validator_store::ValidatorStore; use bls::{PublicKey, PublicKeyBytes}; +use eth2::types::GenericResponse; use slog::{info, Logger}; use slot_clock::SlotClock; use std::sync::Arc; @@ -11,7 +12,7 @@ pub async fn create_signed_voluntary_exit<T: 'static + SlotClock + Clone, E: Eth validator_store: Arc<ValidatorStore<T, E>>, slot_clock: T, log: Logger, -) -> Result<SignedVoluntaryExit, warp::Rejection> { +) -> Result<GenericResponse<SignedVoluntaryExit>, warp::Rejection> { let epoch = match maybe_epoch { Some(epoch) => epoch, None => get_current_epoch::<T, E>(slot_clock).ok_or_else(|| { @@ -60,7 +61,7 @@ pub async fn create_signed_voluntary_exit<T: 'static + SlotClock + Clone, E: Eth )) })?; - Ok(signed_voluntary_exit) + Ok(GenericResponse::from(signed_voluntary_exit)) } /// Calculates the current epoch from the genesis time and current time. diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/src/http_api/create_validator.rs index f3107cfed..52336afa5 100644 --- a/validator_client/src/http_api/create_validator.rs +++ b/validator_client/src/http_api/create_validator.rs @@ -1,15 +1,16 @@ use crate::ValidatorStore; -use account_utils::validator_definitions::ValidatorDefinition; +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use account_utils::{ + eth2_keystore::Keystore, eth2_wallet::{bip39::Mnemonic, WalletBuilder}, random_mnemonic, random_password, ZeroizeString, }; use eth2::lighthouse_vc::types::{self as api_types}; use slot_clock::SlotClock; -use std::path::Path; +use std::path::{Path, PathBuf}; use types::ChainSpec; use types::EthSpec; -use validator_dir::Builder as ValidatorDirBuilder; +use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in /// this validator client. @@ -27,6 +28,7 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock, key_derivation_path_offset: Option<u32>, validator_requests: &[api_types::ValidatorRequest], validator_dir: P, + secrets_dir: Option<PathBuf>, validator_store: &ValidatorStore<T, E>, spec: &ChainSpec, ) -> Result<(Vec<api_types::CreatedValidator>, Mnemonic), warp::Rejection> { @@ -95,7 +97,11 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock, )) })?; + let voting_password_storage = + get_voting_password_storage(&secrets_dir, &keystores.voting, &voting_password_string)?; + let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into()) + .password_dir_opt(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .create_eth1_tx_data(request.deposit_gwei, spec) @@ -136,7 +142,7 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock, validator_store .add_validator_keystore( voting_keystore_path, - voting_password_string, + voting_password_storage, request.enable, request.graffiti.clone(), request.suggested_fee_recipient, @@ -185,3 +191,26 @@ pub async fn create_validators_web3signer<T: 'static + SlotClock, E: EthSpec>( Ok(()) } + +/// Attempts to return a `PasswordStorage::File` if `secrets_dir` is defined. +/// Otherwise, returns a `PasswordStorage::ValidatorDefinitions`. +pub fn get_voting_password_storage( + secrets_dir: &Option<PathBuf>, + voting_keystore: &Keystore, + voting_password_string: &ZeroizeString, +) -> Result<PasswordStorage, warp::Rejection> { + if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, voting_keystore); + if password_path.exists() { + Err(warp_utils::reject::custom_server_error( + "Duplicate keystore password path".to_string(), + )) + } else { + Ok(PasswordStorage::File(password_path)) + } + } else { + Ok(PasswordStorage::ValidatorDefinitions( + voting_password_string.clone(), + )) + } +} diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs index b886f6043..c2d9b4d67 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/src/http_api/keystores.rs @@ -3,11 +3,14 @@ use crate::{ initialized_validators::Error, signing_method::SigningMethod, InitializedValidators, ValidatorStore, }; -use account_utils::ZeroizeString; -use eth2::lighthouse_vc::std_types::{ - DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, - ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, - ListKeystoresResponse, SingleKeystoreResponse, Status, +use account_utils::{validator_definitions::PasswordStorage, ZeroizeString}; +use eth2::lighthouse_vc::{ + std_types::{ + DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, + ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, + KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status, + }, + types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }; use eth2_keystore::Keystore; use slog::{info, warn, Logger}; @@ -17,7 +20,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; use types::{EthSpec, PublicKeyBytes}; -use validator_dir::Builder as ValidatorDirBuilder; +use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; @@ -58,6 +61,7 @@ pub fn list<T: SlotClock + 'static, E: EthSpec>( pub fn import<T: SlotClock + 'static, E: EthSpec>( request: ImportKeystoresRequest, validator_dir: PathBuf, + secrets_dir: Option<PathBuf>, validator_store: Arc<ValidatorStore<T, E>>, task_executor: TaskExecutor, log: Logger, @@ -128,6 +132,7 @@ pub fn import<T: SlotClock + 'static, E: EthSpec>( keystore, password, validator_dir.clone(), + secrets_dir.clone(), &validator_store, handle, ) { @@ -158,6 +163,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>( keystore: Keystore, password: ZeroizeString, validator_dir_path: PathBuf, + secrets_dir: Option<PathBuf>, validator_store: &ValidatorStore<T, E>, handle: Handle, ) -> Result<ImportKeystoreStatus, String> { @@ -179,6 +185,16 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>( } } + let password_storage = if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, &keystore); + if password_path.exists() { + return Ok(ImportKeystoreStatus::Duplicate); + } + PasswordStorage::File(password_path) + } else { + PasswordStorage::ValidatorDefinitions(password.clone()) + }; + // Check that the password is correct. // In future we should re-structure to avoid the double decryption here. It's not as simple // as removing this check because `add_validator_keystore` will break if provided with an @@ -189,6 +205,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>( .map_err(|e| format!("incorrect password: {:?}", e))?; let validator_dir = ValidatorDirBuilder::new(validator_dir_path) + .password_dir_opt(secrets_dir) .voting_keystore(keystore, password.as_ref()) .store_withdrawal_keystore(false) .build() @@ -201,7 +218,7 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>( handle .block_on(validator_store.add_validator_keystore( voting_keystore_path, - password, + password_storage, true, None, None, @@ -219,11 +236,28 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>( task_executor: TaskExecutor, log: Logger, ) -> Result<DeleteKeystoresResponse, Rejection> { + let export_response = export(request, validator_store, task_executor, log)?; + Ok(DeleteKeystoresResponse { + data: export_response + .data + .into_iter() + .map(|response| response.status) + .collect(), + slashing_protection: export_response.slashing_protection, + }) +} + +pub fn export<T: SlotClock + 'static, E: EthSpec>( + request: DeleteKeystoresRequest, + validator_store: Arc<ValidatorStore<T, E>>, + task_executor: TaskExecutor, + log: Logger, +) -> Result<ExportKeystoresResponse, Rejection> { // Remove from initialized validators. let initialized_validators_rwlock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rwlock.write(); - let mut statuses = request + let mut responses = request .pubkeys .iter() .map(|pubkey_bytes| { @@ -232,7 +266,7 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>( &mut initialized_validators, task_executor.clone(), ) { - Ok(status) => Status::ok(status), + Ok(status) => status, Err(error) => { warn!( log, @@ -240,7 +274,11 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>( "pubkey" => ?pubkey_bytes, "error" => ?error, ); - Status::error(DeleteKeystoreStatus::Error, error) + SingleExportKeystoresResponse { + status: Status::error(DeleteKeystoreStatus::Error, error), + validating_keystore: None, + validating_keystore_password: None, + } } } }) @@ -263,19 +301,19 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>( })?; // Update stasuses based on availability of slashing protection data. - for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { - if status.status == DeleteKeystoreStatus::NotFound + for (pubkey, response) in request.pubkeys.iter().zip(responses.iter_mut()) { + if response.status.status == DeleteKeystoreStatus::NotFound && slashing_protection .data .iter() .any(|interchange_data| interchange_data.pubkey == *pubkey) { - status.status = DeleteKeystoreStatus::NotActive; + response.status.status = DeleteKeystoreStatus::NotActive; } } - Ok(DeleteKeystoresResponse { - data: statuses, + Ok(ExportKeystoresResponse { + data: responses, slashing_protection, }) } @@ -284,7 +322,7 @@ fn delete_single_keystore( pubkey_bytes: &PublicKeyBytes, initialized_validators: &mut InitializedValidators, task_executor: TaskExecutor, -) -> Result<DeleteKeystoreStatus, String> { +) -> Result<SingleExportKeystoresResponse, String> { if let Some(handle) = task_executor.handle() { let pubkey = pubkey_bytes .decompress() @@ -292,9 +330,22 @@ fn delete_single_keystore( match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true)) { - Ok(_) => Ok(DeleteKeystoreStatus::Deleted), + Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::Deleted), + validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)), + validating_keystore_password: keystore_and_password.password, + }), + Ok(None) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::Deleted), + validating_keystore: None, + validating_keystore_password: None, + }), Err(e) => match e { - Error::ValidatorNotInitialized(_) => Ok(DeleteKeystoreStatus::NotFound), + Error::ValidatorNotInitialized(_) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::NotFound), + validating_keystore: None, + validating_keystore_password: None, + }), _ => Err(format!("unable to disable and delete: {:?}", e)), }, } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index fa6cde3ed..f654833cb 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -5,6 +5,8 @@ mod keystores; mod remotekeys; mod tests; +pub mod test_utils; + use crate::http_api::create_signed_voluntary_exit::create_signed_voluntary_exit; use crate::{determine_graffiti, GraffitiFile, ValidatorStore}; use account_utils::{ @@ -12,7 +14,9 @@ use account_utils::{ validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition}, }; pub use api_secret::ApiSecret; -use create_validator::{create_validators_mnemonic, create_validators_web3signer}; +use create_validator::{ + create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, +}; use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{self as api_types, GenericResponse, Graffiti, PublicKey, PublicKeyBytes}, @@ -71,6 +75,7 @@ pub struct Context<T: SlotClock, E: EthSpec> { pub api_secret: ApiSecret, pub validator_store: Option<Arc<ValidatorStore<T, E>>>, pub validator_dir: Option<PathBuf>, + pub secrets_dir: Option<PathBuf>, pub graffiti_file: Option<GraffitiFile>, pub graffiti_flag: Option<Graffiti>, pub spec: ChainSpec, @@ -88,6 +93,8 @@ pub struct Config { pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option<String>, + pub allow_keystore_export: bool, + pub store_passwords_in_secrets_dir: bool, } impl Default for Config { @@ -97,6 +104,8 @@ impl Default for Config { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, + allow_keystore_export: false, + store_passwords_in_secrets_dir: false, } } } @@ -121,6 +130,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( shutdown: impl Future<Output = ()> + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { 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 log = ctx.log.clone(); // Configure CORS. @@ -187,6 +198,17 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( }) }); + let inner_secrets_dir = ctx.secrets_dir.clone(); + let secrets_dir_filter = warp::any().map(move || inner_secrets_dir.clone()).and_then( + |secrets_dir: Option<_>| async move { + secrets_dir.ok_or_else(|| { + warp_utils::reject::custom_not_found( + "secrets_dir directory is not initialized.".to_string(), + ) + }) + }, + ); + let inner_graffiti_file = ctx.graffiti_file.clone(); let graffiti_file_filter = warp::any().map(move || inner_graffiti_file.clone()); @@ -357,7 +379,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::path("graffiti")) .and(warp::path::end()) .and(validator_store_filter.clone()) - .and(graffiti_file_filter) + .and(graffiti_file_filter.clone()) .and(graffiti_flag_filter) .and(signer.clone()) .and(log_filter.clone()) @@ -394,18 +416,21 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter.clone()) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: Vec<api_types::ValidatorRequest>, - validator_dir: PathBuf, - validator_store: Arc<ValidatorStore<T, E>>, - spec: Arc<ChainSpec>, - signer, - task_executor: TaskExecutor| { + move |body: Vec<api_types::ValidatorRequest>, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc<ValidatorStore<T, E>>, + spec: Arc<ChainSpec>, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let (validators, mnemonic) = handle.block_on(create_validators_mnemonic( @@ -413,6 +438,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( None, &body, &validator_dir, + secrets_dir, &validator_store, &spec, ))?; @@ -437,18 +463,21 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: api_types::CreateValidatorsMnemonicRequest, - validator_dir: PathBuf, - validator_store: Arc<ValidatorStore<T, E>>, - spec: Arc<ChainSpec>, - signer, - task_executor: TaskExecutor| { + move |body: api_types::CreateValidatorsMnemonicRequest, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc<ValidatorStore<T, E>>, + spec: Arc<ChainSpec>, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let mnemonic = mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| { @@ -463,6 +492,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( Some(body.key_derivation_path_offset), &body.validators, &validator_dir, + secrets_dir, &validator_store, &spec, ))?; @@ -483,15 +513,17 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: api_types::KeystoreValidatorsPostRequest, - validator_dir: PathBuf, - validator_store: Arc<ValidatorStore<T, E>>, - signer, - task_executor: TaskExecutor| { + move |body: api_types::KeystoreValidatorsPostRequest, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc<ValidatorStore<T, E>>, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { // Check to ensure the password is correct. let keypair = body @@ -504,7 +536,12 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( )) })?; + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); + let password_storage = + get_voting_password_storage(&secrets_dir, &body.keystore, &body.password)?; + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) + .password_dir_opt(secrets_dir) .voting_keystore(body.keystore.clone(), body.password.as_ref()) .store_withdrawal_keystore(false) .build() @@ -518,7 +555,6 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( // Drop validator dir so that `add_validator_keystore` can re-lock the keystore. let voting_keystore_path = validator_dir.voting_keystore_path(); drop(validator_dir); - let voting_password = body.password.clone(); let graffiti = body.graffiti.clone(); let suggested_fee_recipient = body.suggested_fee_recipient; let gas_limit = body.gas_limit; @@ -529,7 +565,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( handle .block_on(validator_store.add_validator_keystore( voting_keystore_path, - voting_password, + password_storage, body.enable, graffiti, suggested_fee_recipient, @@ -617,18 +653,27 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::path::end()) .and(warp::body::json()) .and(validator_store_filter.clone()) + .and(graffiti_file_filter) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( |validator_pubkey: PublicKey, body: api_types::ValidatorPatchRequest, validator_store: Arc<ValidatorStore<T, E>>, + graffiti_file: Option<GraffitiFile>, signer, task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + if body.graffiti.is_some() && graffiti_file.is_some() { + return Err(warp_utils::reject::custom_bad_request( + "Unable to update graffiti as the \"--graffiti-file\" flag is set" + .to_string(), + )); + } + + let maybe_graffiti = body.graffiti.clone().map(Into::into); let initialized_validators_rw_lock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rw_lock.write(); - match ( initialized_validators.is_enabled(&validator_pubkey), initialized_validators.validator(&validator_pubkey.compress()), @@ -641,7 +686,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( if Some(is_enabled) == body.enabled && initialized_validator.get_gas_limit() == body.gas_limit && initialized_validator.get_builder_proposals() - == body.builder_proposals => + == body.builder_proposals + && initialized_validator.get_graffiti() == maybe_graffiti => { Ok(()) } @@ -654,6 +700,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( body.enabled, body.gas_limit, body.builder_proposals, + body.graffiti, ), ) .map_err(|e| { @@ -687,6 +734,29 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( }) }); + // DELETE /lighthouse/keystores + let delete_lighthouse_keystores = warp::path("lighthouse") + .and(warp::path("keystores")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(signer.clone()) + .and(validator_store_filter.clone()) + .and(task_executor_filter.clone()) + .and(log_filter.clone()) + .and_then( + move |request, signer, validator_store, task_executor, log| { + blocking_signed_json_task(signer, move || { + if allow_keystore_export { + keystores::export(request, validator_store, task_executor, log) + } else { + Err(warp_utils::reject::custom_bad_request( + "keystore export is disabled".to_string(), + )) + } + }) + }, + ); + // 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()); @@ -971,13 +1041,28 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( .and(warp::body::json()) .and(signer.clone()) .and(validator_dir_filter) + .and(secrets_dir_filter) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .and(log_filter.clone()) .and_then( - |request, signer, validator_dir, validator_store, task_executor, log| { + move |request, + signer, + validator_dir, + secrets_dir, + validator_store, + task_executor, + log| { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); blocking_signed_json_task(signer, move || { - keystores::import(request, validator_dir, validator_store, task_executor, log) + keystores::import( + request, + validator_dir, + secrets_dir, + validator_store, + task_executor, + log, + ) }) }, ); @@ -1106,7 +1191,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>( )) .or(warp::patch().and(patch_validators)) .or(warp::delete().and( - delete_fee_recipient + delete_lighthouse_keystores + .or(delete_fee_recipient) .or(delete_gas_limit) .or(delete_std_keystores) .or(delete_std_remotekeys), diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs new file mode 100644 index 000000000..c7558dd58 --- /dev/null +++ b/validator_client/src/http_api/test_utils.rs @@ -0,0 +1,631 @@ +use crate::doppelganger_service::DoppelgangerService; +use crate::key_cache::{KeyCache, CACHE_FILENAME}; +use crate::{ + http_api::{ApiSecret, Config as HttpConfig, Context}, + initialized_validators::{InitializedValidators, OnDecryptFailure}, + Config, ValidatorDefinitions, ValidatorStore, +}; +use account_utils::{ + eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, + ZeroizeString, +}; +use deposit_contract::decode_eth1_tx_data; +use eth2::{ + lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, + types::ErrorMessage as ApiErrorMessage, + Error as ApiError, +}; +use eth2_keystore::KeystoreBuilder; +use logging::test_logger; +use parking_lot::RwLock; +use sensitive_url::SensitiveUrl; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slot_clock::{SlotClock, TestingSlotClock}; +use std::future::Future; +use std::marker::PhantomData; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::Arc; +use std::time::Duration; +use task_executor::test_utils::TestRuntime; +use tempfile::{tempdir, TempDir}; +use tokio::sync::oneshot; + +pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; +pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); + +type E = MainnetEthSpec; + +pub struct HdValidatorScenario { + pub count: usize, + pub specify_mnemonic: bool, + pub key_derivation_path_offset: u32, + pub disabled: Vec<usize>, +} + +pub struct KeystoreValidatorScenario { + pub enabled: bool, + pub correct_password: bool, +} + +pub struct Web3SignerValidatorScenario { + pub count: usize, + pub enabled: bool, +} + +pub struct ApiTester { + pub client: ValidatorClientHttpClient, + pub initialized_validators: Arc<RwLock<InitializedValidators>>, + pub validator_store: Arc<ValidatorStore<TestingSlotClock, E>>, + pub url: SensitiveUrl, + pub api_token: String, + pub test_runtime: TestRuntime, + pub _server_shutdown: oneshot::Sender<()>, + pub validator_dir: TempDir, + pub secrets_dir: TempDir, +} + +impl ApiTester { + pub async fn new() -> Self { + Self::new_with_http_config(Self::default_http_config()).await + } + + pub async fn new_with_http_config(http_config: HttpConfig) -> Self { + let log = test_logger(); + + let validator_dir = tempdir().unwrap(); + let secrets_dir = tempdir().unwrap(); + + let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); + + let initialized_validators = InitializedValidators::from_definitions( + validator_defs, + validator_dir.path().into(), + log.clone(), + ) + .await + .unwrap(); + + let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_pubkey = api_secret.api_token(); + + let config = Config { + validator_dir: validator_dir.path().into(), + secrets_dir: secrets_dir.path().into(), + fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT), + ..Default::default() + }; + + let spec = E::default_spec(); + + let slashing_db_path = config.validator_dir.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(ValidatorStore::<_, E>::new( + initialized_validators, + slashing_protection, + Hash256::repeat_byte(42), + spec, + Some(Arc::new(DoppelgangerService::new(log.clone()))), + slot_clock.clone(), + &config, + test_runtime.task_executor.clone(), + log.clone(), + )); + + validator_store + .register_all_in_doppelganger_protection_if_enabled() + .expect("Should attach doppelganger service"); + + let initialized_validators = validator_store.initialized_validators(); + + let context = Arc::new(Context { + task_executor: test_runtime.task_executor.clone(), + api_secret, + validator_dir: Some(validator_dir.path().into()), + secrets_dir: Some(secrets_dir.path().into()), + validator_store: Some(validator_store.clone()), + graffiti_file: None, + graffiti_flag: Some(Graffiti::default()), + spec: E::default_spec(), + config: http_config, + log, + sse_logging_components: None, + slot_clock, + _phantom: PhantomData, + }); + let ctx = context; + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + + tokio::spawn(server); + + let url = SensitiveUrl::parse(&format!( + "http://{}:{}", + listening_socket.ip(), + listening_socket.port() + )) + .unwrap(); + + let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey.clone()).unwrap(); + + Self { + client, + initialized_validators, + validator_store, + url, + api_token: api_pubkey, + test_runtime, + _server_shutdown: shutdown_tx, + validator_dir, + secrets_dir, + } + } + + pub fn default_http_config() -> HttpConfig { + HttpConfig { + enabled: true, + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + listen_port: 0, + allow_origin: None, + allow_keystore_export: true, + store_passwords_in_secrets_dir: false, + } + } + + /// Checks that the key cache exists and can be decrypted with the current + /// set of known validators. + #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. + pub async fn ensure_key_cache_consistency(&self) { + assert!( + self.validator_dir.as_ref().join(CACHE_FILENAME).exists(), + "the key cache should exist" + ); + let key_cache = + KeyCache::open_or_create(self.validator_dir.as_ref()).expect("should open a key cache"); + + self.initialized_validators + .read() + .decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::Error) + .await + .expect("key cache should decypt"); + } + + pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { + let tmp = tempdir().unwrap(); + let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let invalid_pubkey = api_secret.api_token(); + ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap() + } + + pub async fn test_with_invalid_auth<F, A, T>(self, func: F) -> Self + where + F: Fn(ValidatorClientHttpClient) -> A, + A: Future<Output = Result<T, ApiError>>, + { + /* + * Test with an invalid Authorization header. + */ + match func(self.invalid_token_client()).await { + Err(ApiError::ServerMessage(ApiErrorMessage { code: 403, .. })) => (), + Err(other) => panic!("expected authorized error, got {:?}", other), + Ok(_) => panic!("expected authorized error, got Ok"), + } + + /* + * Test with a missing Authorization header. + */ + let mut missing_token_client = self.client.clone(); + missing_token_client.send_authorization_header(false); + match func(missing_token_client).await { + Err(ApiError::ServerMessage(ApiErrorMessage { + code: 401, message, .. + })) if message.contains("missing Authorization header") => (), + Err(other) => panic!("expected missing header error, got {:?}", other), + Ok(_) => panic!("expected missing header error, got Ok"), + } + + self + } + + pub fn invalidate_api_token(mut self) -> Self { + self.client = self.invalid_token_client(); + self + } + + pub async fn test_get_lighthouse_version_invalid(self) -> Self { + self.client.get_lighthouse_version().await.unwrap_err(); + self + } + + pub async fn test_get_lighthouse_spec(self) -> Self { + let result = self + .client + .get_lighthouse_spec::<ConfigAndPresetBellatrix>() + .await + .map(|res| ConfigAndPreset::Bellatrix(res.data)) + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::<E>(&E::default_spec(), None); + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_lighthouse_version(self) -> Self { + let result = self.client.get_lighthouse_version().await.unwrap().data; + + let expected = VersionData { + version: lighthouse_version::version_with_platform(), + }; + + assert_eq!(result, expected); + + self + } + + #[cfg(target_os = "linux")] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap(); + + self + } + + #[cfg(not(target_os = "linux"))] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap_err(); + + self + } + pub fn vals_total(&self) -> usize { + self.initialized_validators.read().num_total() + } + + pub fn vals_enabled(&self) -> usize { + self.initialized_validators.read().num_enabled() + } + + pub fn assert_enabled_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_enabled(), count); + self + } + + pub fn assert_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_total(), count); + self + } + + pub async fn create_hd_validators(self, s: HdValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let validators = (0..s.count) + .map(|i| ValidatorRequest { + enable: !s.disabled.contains(&i), + description: format!("boi #{}", i), + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + deposit_gwei: E::default_spec().max_effective_balance, + }) + .collect::<Vec<_>>(); + + let (response, mnemonic) = if s.specify_mnemonic { + let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let request = CreateValidatorsMnemonicRequest { + mnemonic: mnemonic.clone(), + key_derivation_path_offset: s.key_derivation_path_offset, + validators: validators.clone(), + }; + let response = self + .client + .post_lighthouse_validators_mnemonic(&request) + .await + .unwrap() + .data; + + (response, mnemonic) + } else { + assert_eq!( + s.key_derivation_path_offset, 0, + "cannot use a derivation offset without specifying a mnemonic" + ); + let response = self + .client + .post_lighthouse_validators(validators.clone()) + .await + .unwrap() + .data; + (response.validators.clone(), response.mnemonic) + }; + + assert_eq!(response.len(), s.count); + assert_eq!(self.vals_total(), initial_vals + s.count); + assert_eq!( + self.vals_enabled(), + initial_enabled_vals + s.count - s.disabled.len() + ); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + // 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)); + } + + /* + * Verify that we can regenerate all the keys from the mnemonic. + */ + + let mnemonic = mnemonic_from_phrase(mnemonic.as_str()).unwrap(); + let mut wallet = WalletBuilder::from_mnemonic(&mnemonic, PASSWORD_BYTES, "".to_string()) + .unwrap() + .build() + .unwrap(); + + wallet + .set_nextaccount(s.key_derivation_path_offset) + .unwrap(); + + for item in response.iter().take(s.count) { + let keypairs = wallet + .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) + .unwrap(); + let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + assert_eq!( + item.voting_pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the server response" + ); + + let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + let deposit_bytes = serde_utils::hex::decode(&item.eth1_deposit_tx_data).unwrap(); + + let (deposit_data, _) = + decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) + .unwrap(); + + assert_eq!( + deposit_data.pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the deposit data" + ); + + assert_eq!( + deposit_data.withdrawal_credentials, + Hash256::from_slice(&bls::get_withdrawal_credentials( + &withdrawal_keypair.pk, + E::default_spec().bls_withdrawal_prefix_byte + )), + "the locally generated withdrawal creds should match the deposit data" + ); + + assert_eq!( + deposit_data.signature, + deposit_data.create_signature(&voting_keypair.sk, &E::default_spec()), + "the locally-generated deposit sig should create the same deposit sig" + ); + } + + self + } + + pub async fn create_keystore_validators(self, s: KeystoreValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + + if !s.correct_password { + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(random_password().as_ref().to_vec()) + .unwrap() + .into(), + keystore, + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + }; + + self.client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap_err(); + + return self; + } + + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(password.as_ref().to_vec()) + .unwrap() + .into(), + keystore, + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + }; + + let response = self + .client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap() + .data; + + let num_enabled = s.enabled as usize; + + assert_eq!(self.vals_total(), initial_vals + 1); + assert_eq!(self.vals_enabled(), initial_enabled_vals + num_enabled); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + assert_eq!(response.voting_pubkey, keypair.pk.into()); + assert_eq!(response.enabled, s.enabled); + + self + } + + pub async fn create_web3signer_validators(self, s: Web3SignerValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let request: Vec<_> = (0..s.count) + .map(|i| { + let kp = Keypair::random(); + Web3SignerValidatorRequest { + enable: s.enabled, + description: format!("{}", i), + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + voting_public_key: kp.pk, + url: format!("http://signer_{}.com/", i), + root_certificate_path: None, + request_timeout_ms: None, + client_identity_path: None, + client_identity_password: None, + } + }) + .collect(); + + self.client + .post_lighthouse_validators_web3signer(&request) + .await + .unwrap(); + + assert_eq!(self.vals_total(), initial_vals + s.count); + if s.enabled { + assert_eq!(self.vals_enabled(), initial_enabled_vals + s.count); + } else { + assert_eq!(self.vals_enabled(), initial_enabled_vals); + }; + + self + } + + pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None) + .await + .unwrap(); + + assert_eq!( + self.initialized_validators + .read() + .is_enabled(&validator.voting_pubkey.decompress().unwrap()) + .unwrap(), + 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()); + + // Check the server via an individual request. + assert_eq!( + self.client + .get_lighthouse_validators_pubkey(&validator.voting_pubkey) + .await + .unwrap() + .unwrap() + .data + .enabled, + enabled + ); + + self + } + + pub async fn set_gas_limit(self, index: usize, gas_limit: u64) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + Some(gas_limit), + None, + None, + ) + .await + .unwrap(); + + self + } + + pub async fn assert_gas_limit(self, index: usize, gas_limit: u64) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + assert_eq!( + self.validator_store.get_gas_limit(&validator.voting_pubkey), + gas_limit + ); + + self + } + + pub async fn set_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + None, + Some(builder_proposals), + None, + ) + .await + .unwrap(); + + self + } + + pub async fn assert_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + assert_eq!( + self.validator_store + .get_builder_proposals(&validator.voting_pubkey), + builder_proposals + ); + + self + } +} diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 84d2fe437..db9d0613b 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -28,12 +28,12 @@ use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use task_executor::TaskExecutor; +use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; -use tokio::runtime::Runtime; -use tokio::sync::oneshot; +use types::graffiti::GraffitiString; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -46,23 +46,12 @@ struct ApiTester { validator_store: Arc<ValidatorStore<TestingSlotClock, E>>, url: SensitiveUrl, slot_clock: TestingSlotClock, - _server_shutdown: oneshot::Sender<()>, _validator_dir: TempDir, - _runtime_shutdown: exit_future::Signal, -} - -// Builds a runtime to be used in the testing configuration. -fn build_runtime() -> Arc<Runtime> { - Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("Should be able to build a testing runtime"), - ) + _test_runtime: TestRuntime, } impl ApiTester { - pub async fn new(runtime: std::sync::Weak<Runtime>) -> Self { + pub async fn new() -> Self { let log = test_logger(); let validator_dir = tempdir().unwrap(); @@ -98,9 +87,7 @@ impl ApiTester { Duration::from_secs(1), ); - let (runtime_shutdown, exit) = exit_future::signal(); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); + let test_runtime = TestRuntime::default(); let validator_store = Arc::new(ValidatorStore::<_, E>::new( initialized_validators, @@ -110,7 +97,7 @@ impl ApiTester { Some(Arc::new(DoppelgangerService::new(log.clone()))), slot_clock.clone(), &config, - executor.clone(), + test_runtime.task_executor.clone(), log.clone(), )); @@ -121,9 +108,10 @@ impl ApiTester { let initialized_validators = validator_store.initialized_validators(); let context = Arc::new(Context { - task_executor: executor, + task_executor: test_runtime.task_executor.clone(), api_secret, validator_dir: Some(validator_dir.path().into()), + secrets_dir: Some(secrets_dir.path().into()), validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), @@ -133,6 +121,8 @@ impl ApiTester { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, + allow_keystore_export: true, + store_passwords_in_secrets_dir: false, }, sse_logging_components: None, log, @@ -140,12 +130,8 @@ impl ApiTester { _phantom: PhantomData, }); let ctx = context.clone(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + let (listening_socket, server) = + super::serve(ctx, test_runtime.task_executor.exit()).unwrap(); tokio::spawn(async { server.await }); @@ -164,9 +150,8 @@ impl ApiTester { validator_store, url, slot_clock, - _server_shutdown: shutdown_tx, _validator_dir: validator_dir, - _runtime_shutdown: runtime_shutdown, + _test_runtime: test_runtime, } } @@ -517,7 +502,7 @@ impl ApiTester { .await; assert!(resp.is_ok()); - assert_eq!(resp.unwrap().message.epoch, expected_exit_epoch); + assert_eq!(resp.unwrap().data.message.epoch, expected_exit_epoch); self } @@ -533,7 +518,7 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None) .await .unwrap(); @@ -575,7 +560,13 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + Some(gas_limit), + None, + None, + ) .await .unwrap(); @@ -602,6 +593,7 @@ impl ApiTester { None, None, Some(builder_proposals), + None, ) .await .unwrap(); @@ -620,6 +612,34 @@ impl ApiTester { self } + + pub async fn set_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + None, + None, + Some(graffiti_str), + ) + .await + .unwrap(); + + self + } + + pub async fn assert_graffiti(self, index: usize, graffiti: &str) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + let graffiti_str = GraffitiString::from_str(graffiti).unwrap(); + assert_eq!( + self.validator_store.graffiti(&validator.voting_pubkey), + Some(graffiti_str.into()) + ); + + self + } } struct HdValidatorScenario { @@ -639,346 +659,341 @@ struct Web3SignerValidatorScenario { enabled: bool, } -#[test] -fn invalid_pubkey() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .invalidate_api_token() - .test_get_lighthouse_version_invalid() - .await; - }); +#[tokio::test] +async fn invalid_pubkey() { + ApiTester::new() + .await + .invalidate_api_token() + .test_get_lighthouse_version_invalid() + .await; } -#[test] -fn routes_with_invalid_auth() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) - .await - .test_with_invalid_auth(|client| async move { - client.get_lighthouse_spec::<types::Config>().await - }) - .await - .test_with_invalid_auth( - |client| async move { client.get_lighthouse_validators().await }, - ) - .await - .test_with_invalid_auth(|client| async move { - client - .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators(vec![ValidatorRequest { - enable: <_>::default(), - description: <_>::default(), - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - deposit_gwei: <_>::default(), - }]) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { - mnemonic: String::default().into(), - key_derivation_path_offset: <_>::default(), - validators: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) - .unwrap() - .build() - .unwrap(); - client - .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { - password: String::default().into(), - enable: <_>::default(), - keystore, - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) - .await - }) - .await - .test_with_invalid_auth(|client| async move { client.get_keystores().await }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password_string(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) - .unwrap() - .build() - .map(KeystoreJsonStr) - .unwrap(); - client - .post_keystores(&ImportKeystoresRequest { - keystores: vec![keystore], - passwords: vec![password], - slashing_protection: None, - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let keypair = Keypair::random(); - client - .delete_keystores(&DeleteKeystoresRequest { - pubkeys: vec![keypair.pk.compress()], - }) - .await - }) - .await - }); +#[tokio::test] +async fn routes_with_invalid_auth() { + ApiTester::new() + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) + .await + .test_with_invalid_auth(|client| async move { + client.get_lighthouse_spec::<types::Config>().await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_validators().await }) + .await + .test_with_invalid_auth(|client| async move { + client + .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators(vec![ValidatorRequest { + enable: <_>::default(), + description: <_>::default(), + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + deposit_gwei: <_>::default(), + }]) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { + mnemonic: String::default().into(), + key_derivation_path_offset: <_>::default(), + validators: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + client + .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { + password: String::default().into(), + enable: <_>::default(), + keystore, + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .patch_lighthouse_validators( + &PublicKeyBytes::empty(), + Some(false), + None, + None, + None, + ) + .await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_keystores().await }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password_string(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) + .unwrap() + .build() + .map(KeystoreJsonStr) + .unwrap(); + client + .post_keystores(&ImportKeystoresRequest { + keystores: vec![keystore], + passwords: vec![password], + slashing_protection: None, + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let keypair = Keypair::random(); + client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: vec![keypair.pk.compress()], + }) + .await + }) + .await; } -#[test] -fn simple_getters() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_get_lighthouse_version() - .await - .test_get_lighthouse_health() - .await - .test_get_lighthouse_spec() - .await; - }); +#[tokio::test] +async fn simple_getters() { + ApiTester::new() + .await + .test_get_lighthouse_version() + .await + .test_get_lighthouse_health() + .await + .test_get_lighthouse_spec() + .await; } -#[test] -fn hd_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: true, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .create_hd_validators(HdValidatorScenario { - count: 1, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![0], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3) - .create_hd_validators(HdValidatorScenario { - count: 0, - specify_mnemonic: true, - key_derivation_path_offset: 4, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3); - }); +#[tokio::test] +async fn hd_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: true, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .create_hd_validators(HdValidatorScenario { + count: 1, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![0], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3) + .create_hd_validators(HdValidatorScenario { + count: 0, + specify_mnemonic: true, + key_derivation_path_offset: 4, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3); } -#[test] -fn validator_exit() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .test_sign_voluntary_exits(0, None) - .await - .test_sign_voluntary_exits(0, Some(Epoch::new(256))) - .await; - }); +#[tokio::test] +async fn validator_exit() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .test_sign_voluntary_exits(0, None) + .await + .test_sign_voluntary_exits(0, Some(Epoch::new(256))) + .await; } -#[test] -fn validator_enabling() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2); - }); +#[tokio::test] +async fn validator_enabling() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2); } -#[test] -fn validator_gas_limit() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_gas_limit(0, 500) - .await - .assert_gas_limit(0, 500) - .await - // Update gas limit while validator is disabled. - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_gas_limit(0, 1000) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_gas_limit(0, 1000) - .await - }); +#[tokio::test] +async fn validator_gas_limit() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_gas_limit(0, 500) + .await + .assert_gas_limit(0, 500) + .await + // Update gas limit while validator is disabled. + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_gas_limit(0, 1000) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_gas_limit(0, 1000) + .await; } -#[test] -fn validator_builder_proposals() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_builder_proposals(0, true) - .await - // Test setting builder proposals while the validator is disabled - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_builder_proposals(0, false) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_builder_proposals(0, false) - .await - }); +#[tokio::test] +async fn validator_builder_proposals() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_builder_proposals(0, true) + .await + // Test setting builder proposals while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_builder_proposals(0, false) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_builder_proposals(0, false) + .await; } -#[test] -fn keystore_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: false, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: false, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2); - }); +#[tokio::test] +async fn validator_graffiti() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here") + .await + .assert_graffiti(0, "Mr F was here") + .await + // Test setting graffiti while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here again") + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_graffiti(0, "Mr F was here again") + .await; } -#[test] -fn web3signer_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_web3signer_validators(Web3SignerValidatorScenario { - count: 1, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1); - }); +#[tokio::test] +async fn keystore_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: false, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: false, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2); +} + +#[tokio::test] +async fn web3signer_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_web3signer_validators(Web3SignerValidatorScenario { + count: 1, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1); } diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index 769d8a1d4..d60872e49 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -12,6 +12,7 @@ use itertools::Itertools; use rand::{rngs::SmallRng, Rng, SeedableRng}; use slashing_protection::interchange::{Interchange, InterchangeMetadata}; use std::{collections::HashMap, path::Path}; +use tokio::runtime::Handle; use types::Address; fn new_keystore(password: ZeroizeString) -> Keystore { @@ -64,31 +65,23 @@ fn remotekey_validator_with_pubkey(pubkey: PublicKey) -> SingleImportRemotekeysR } } -fn run_test<F, V>(f: F) +async fn run_test<F, V>(f: F) where F: FnOnce(ApiTester) -> V, V: Future<Output = ()>, { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester = ApiTester::new(weak_runtime).await; - f(tester).await - }); + let tester = ApiTester::new().await; + f(tester).await } -fn run_dual_vc_test<F, V>(f: F) +async fn run_dual_vc_test<F, V>(f: F) where F: FnOnce(ApiTester, ApiTester) -> V, V: Future<Output = ()>, { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester1 = ApiTester::new(weak_runtime.clone()).await; - let tester2 = ApiTester::new(weak_runtime).await; - f(tester1, tester2).await - }); + let tester1 = ApiTester::new().await; + let tester2 = ApiTester::new().await; + f(tester1, tester2).await } fn keystore_pubkey(keystore: &Keystore) -> PublicKeyBytes { @@ -199,8 +192,8 @@ fn check_remotekey_delete_response( } } -#[test] -fn get_auth_no_token() { +#[tokio::test] +async fn get_auth_no_token() { run_test(|mut tester| async move { let _ = &tester; tester.client.send_authorization_header(false); @@ -213,19 +206,21 @@ fn get_auth_no_token() { // The token should match the one that the client was originally initialised with. assert!(tester.client.api_token() == Some(&token)); }) + .await; } -#[test] -fn get_empty_keystores() { +#[tokio::test] +async fn get_empty_keystores() { run_test(|tester| async move { let _ = &tester; let res = tester.client.get_keystores().await.unwrap(); assert_eq!(res, ListKeystoresResponse { data: vec![] }); }) + .await; } -#[test] -fn import_new_keystores() { +#[tokio::test] +async fn import_new_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -250,10 +245,11 @@ fn import_new_keystores() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &keystores); }) + .await; } -#[test] -fn import_only_duplicate_keystores() { +#[tokio::test] +async fn import_only_duplicate_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -279,10 +275,11 @@ fn import_only_duplicate_keystores() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &keystores); }) + .await; } -#[test] -fn import_some_duplicate_keystores() { +#[tokio::test] +async fn import_some_duplicate_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -330,10 +327,11 @@ fn import_some_duplicate_keystores() { let import_res = tester.client.post_keystores(&req2).await.unwrap(); check_keystore_import_response(&import_res, expected); }) + .await; } -#[test] -fn import_wrong_number_of_passwords() { +#[tokio::test] +async fn import_wrong_number_of_passwords() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -352,10 +350,11 @@ fn import_wrong_number_of_passwords() { .unwrap_err(); assert_eq!(err.status().unwrap(), 400); }) + .await; } -#[test] -fn get_web3_signer_keystores() { +#[tokio::test] +async fn get_web3_signer_keystores() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -412,10 +411,11 @@ fn get_web3_signer_keystores() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await; } -#[test] -fn import_and_delete_conflicting_web3_signer_keystores() { +#[tokio::test] +async fn import_and_delete_conflicting_web3_signer_keystores() { run_test(|tester| async move { let _ = &tester; let num_keystores = 3; @@ -468,7 +468,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() { for pubkey in &pubkeys { tester .client - .patch_lighthouse_validators(pubkey, Some(false), None, None) + .patch_lighthouse_validators(pubkey, Some(false), None, None, None) .await .unwrap(); } @@ -477,10 +477,11 @@ fn import_and_delete_conflicting_web3_signer_keystores() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_delete_error(keystores.len())); }) + .await; } -#[test] -fn import_keystores_wrong_password() { +#[tokio::test] +async fn import_keystores_wrong_password() { run_test(|tester| async move { let _ = &tester; let num_keystores = 4; @@ -551,11 +552,12 @@ fn import_keystores_wrong_password() { &import_res, (0..num_keystores).map(|_| ImportKeystoreStatus::Duplicate), ); - }); + }) + .await; } -#[test] -fn import_invalid_slashing_protection() { +#[tokio::test] +async fn import_invalid_slashing_protection() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -589,10 +591,11 @@ fn import_invalid_slashing_protection() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &[]); }) + .await; } -#[test] -fn check_get_set_fee_recipient() { +#[tokio::test] +async fn check_get_set_fee_recipient() { run_test(|tester: ApiTester| async move { let _ = &tester; let password = random_password_string(); @@ -768,10 +771,11 @@ fn check_get_set_fee_recipient() { ); } }) + .await; } -#[test] -fn check_get_set_gas_limit() { +#[tokio::test] +async fn check_get_set_gas_limit() { run_test(|tester: ApiTester| async move { let _ = &tester; let password = random_password_string(); @@ -943,14 +947,15 @@ fn check_get_set_gas_limit() { ); } }) + .await } fn all_indices(count: usize) -> Vec<usize> { (0..count).collect() } -#[test] -fn migrate_all_with_slashing_protection() { +#[tokio::test] +async fn migrate_all_with_slashing_protection() { let n = 3; generic_migration_test( n, @@ -967,11 +972,12 @@ fn migrate_all_with_slashing_protection() { (1, make_attestation(2, 3), false), (2, make_attestation(1, 2), false), ], - ); + ) + .await; } -#[test] -fn migrate_some_with_slashing_protection() { +#[tokio::test] +async fn migrate_some_with_slashing_protection() { let n = 3; generic_migration_test( n, @@ -989,11 +995,12 @@ fn migrate_some_with_slashing_protection() { (0, make_attestation(2, 3), true), (1, make_attestation(3, 4), true), ], - ); + ) + .await; } -#[test] -fn migrate_some_missing_slashing_protection() { +#[tokio::test] +async fn migrate_some_missing_slashing_protection() { let n = 3; generic_migration_test( n, @@ -1010,11 +1017,12 @@ fn migrate_some_missing_slashing_protection() { (1, make_attestation(2, 3), true), (0, make_attestation(2, 3), true), ], - ); + ) + .await; } -#[test] -fn migrate_some_extra_slashing_protection() { +#[tokio::test] +async fn migrate_some_extra_slashing_protection() { let n = 3; generic_migration_test( n, @@ -1033,7 +1041,8 @@ fn migrate_some_extra_slashing_protection() { (1, make_attestation(3, 4), true), (2, make_attestation(2, 3), false), ], - ); + ) + .await; } /// Run a test that creates some validators on one VC, and then migrates them to a second VC. @@ -1051,7 +1060,7 @@ fn migrate_some_extra_slashing_protection() { /// - `import_indices`: validators to transfer. It needn't be a subset of `delete_indices`. /// - `second_vc_attestations`: attestations to sign on the second VC after the transfer. The bool /// indicates whether the signing should be successful. -fn generic_migration_test( +async fn generic_migration_test( num_validators: usize, first_vc_attestations: Vec<(usize, Attestation<E>)>, delete_indices: Vec<usize>, @@ -1169,11 +1178,12 @@ fn generic_migration_test( Err(e) => assert!(!should_succeed, "{:?}", e), } } - }); + }) + .await } -#[test] -fn delete_keystores_twice() { +#[tokio::test] +async fn delete_keystores_twice() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1201,10 +1211,11 @@ fn delete_keystores_twice() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_not_active(keystores.len())); }) + .await } -#[test] -fn delete_nonexistent_keystores() { +#[tokio::test] +async fn delete_nonexistent_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1219,6 +1230,7 @@ fn delete_nonexistent_keystores() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_not_found(keystores.len())); }) + .await } fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation<E> { @@ -1242,9 +1254,9 @@ fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation<E> { } } -#[test] -fn delete_concurrent_with_signing() { - let runtime = build_runtime(); +#[tokio::test] +async fn delete_concurrent_with_signing() { + let handle = Handle::try_current().unwrap(); let num_keys = 8; let num_signing_threads = 8; let num_attestations = 100; @@ -1257,115 +1269,112 @@ fn delete_concurrent_with_signing() { "num_keys should be divisible by num threads for simplicity" ); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester = ApiTester::new(weak_runtime).await; + let tester = ApiTester::new().await; - // Generate a lot of keys and import them. - let password = random_password_string(); - let keystores = (0..num_keys) - .map(|_| new_keystore(password.clone())) - .collect::<Vec<_>>(); - let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::<Vec<_>>(); + // Generate a lot of keys and import them. + let password = random_password_string(); + let keystores = (0..num_keys) + .map(|_| new_keystore(password.clone())) + .collect::<Vec<_>>(); + let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::<Vec<_>>(); - let import_res = tester - .client - .post_keystores(&ImportKeystoresRequest { - keystores: keystores.clone(), - passwords: vec![password.clone(); keystores.len()], - slashing_protection: None, - }) - .await - .unwrap(); - check_keystore_import_response(&import_res, all_imported(keystores.len())); + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + check_keystore_import_response(&import_res, all_imported(keystores.len())); - // Start several threads signing attestations at sequential epochs. - let mut join_handles = vec![]; + // Start several threads signing attestations at sequential epochs. + let mut join_handles = vec![]; - for thread_index in 0..num_signing_threads { - let keys_per_thread = num_keys / num_signing_threads; - let validator_store = tester.validator_store.clone(); - let thread_pubkeys = all_pubkeys - [thread_index * keys_per_thread..(thread_index + 1) * keys_per_thread] - .to_vec(); + for thread_index in 0..num_signing_threads { + let keys_per_thread = num_keys / num_signing_threads; + let validator_store = tester.validator_store.clone(); + let thread_pubkeys = all_pubkeys + [thread_index * keys_per_thread..(thread_index + 1) * keys_per_thread] + .to_vec(); - let handle = runtime.spawn(async move { - for j in 0..num_attestations { - let mut att = make_attestation(j, j + 1); - for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { - let _ = validator_store - .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) - .await; - } + let handle = handle.spawn(async move { + for j in 0..num_attestations { + let mut att = make_attestation(j, j + 1); + for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { + let _ = validator_store + .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) + .await; } - }); - join_handles.push(handle); - } - - // Concurrently, delete each validator one at a time. Store the slashing protection - // data so we can ensure it doesn't change after a key is exported. - let mut delete_handles = vec![]; - for _ in 0..num_delete_threads { - let client = tester.client.clone(); - let all_pubkeys = all_pubkeys.clone(); - - let handle = runtime.spawn(async move { - let mut rng = SmallRng::from_entropy(); - - let mut slashing_protection = vec![]; - for _ in 0..num_delete_attempts { - let to_delete = all_pubkeys - .iter() - .filter(|_| rng.gen_bool(delete_prob)) - .copied() - .collect::<Vec<_>>(); - - if !to_delete.is_empty() { - let delete_res = client - .delete_keystores(&DeleteKeystoresRequest { pubkeys: to_delete }) - .await - .unwrap(); - - for status in delete_res.data.iter() { - assert_ne!(status.status, DeleteKeystoreStatus::Error); - } - - slashing_protection.push(delete_res.slashing_protection); - } - } - slashing_protection - }); - - delete_handles.push(handle); - } - - // Collect slashing protection. - let mut slashing_protection_map = HashMap::new(); - let collected_slashing_protection = futures::future::join_all(delete_handles).await; - - for interchange in collected_slashing_protection - .into_iter() - .flat_map(Result::unwrap) - { - for validator_data in interchange.data { - slashing_protection_map - .entry(validator_data.pubkey) - .and_modify(|existing| { - assert_eq!( - *existing, validator_data, - "slashing protection data changed after first export" - ) - }) - .or_insert(validator_data); } - } + }); + join_handles.push(handle); + } - futures::future::join_all(join_handles).await - }); + // Concurrently, delete each validator one at a time. Store the slashing protection + // data so we can ensure it doesn't change after a key is exported. + let mut delete_handles = vec![]; + for _ in 0..num_delete_threads { + let client = tester.client.clone(); + let all_pubkeys = all_pubkeys.clone(); + + let handle = handle.spawn(async move { + let mut rng = SmallRng::from_entropy(); + + let mut slashing_protection = vec![]; + for _ in 0..num_delete_attempts { + let to_delete = all_pubkeys + .iter() + .filter(|_| rng.gen_bool(delete_prob)) + .copied() + .collect::<Vec<_>>(); + + if !to_delete.is_empty() { + let delete_res = client + .delete_keystores(&DeleteKeystoresRequest { pubkeys: to_delete }) + .await + .unwrap(); + + for status in delete_res.data.iter() { + assert_ne!(status.status, DeleteKeystoreStatus::Error); + } + + slashing_protection.push(delete_res.slashing_protection); + } + } + slashing_protection + }); + + delete_handles.push(handle); + } + + // Collect slashing protection. + let mut slashing_protection_map = HashMap::new(); + let collected_slashing_protection = futures::future::join_all(delete_handles).await; + + for interchange in collected_slashing_protection + .into_iter() + .flat_map(Result::unwrap) + { + for validator_data in interchange.data { + slashing_protection_map + .entry(validator_data.pubkey) + .and_modify(|existing| { + assert_eq!( + *existing, validator_data, + "slashing protection data changed after first export" + ) + }) + .or_insert(validator_data); + } + } + + futures::future::join_all(join_handles).await; } -#[test] -fn delete_then_reimport() { +#[tokio::test] +async fn delete_then_reimport() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1396,19 +1405,21 @@ fn delete_then_reimport() { let import_res = tester.client.post_keystores(&import_req).await.unwrap(); check_keystore_import_response(&import_res, all_imported(keystores.len())); }) + .await } -#[test] -fn get_empty_remotekeys() { +#[tokio::test] +async fn get_empty_remotekeys() { run_test(|tester| async move { let _ = &tester; let res = tester.client.get_remotekeys().await.unwrap(); assert_eq!(res, ListRemotekeysResponse { data: vec![] }); }) + .await } -#[test] -fn import_new_remotekeys() { +#[tokio::test] +async fn import_new_remotekeys() { run_test(|tester| async move { let _ = &tester; @@ -1443,10 +1454,11 @@ fn import_new_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_same_remotekey_different_url() { +#[tokio::test] +async fn import_same_remotekey_different_url() { run_test(|tester| async move { let _ = &tester; @@ -1485,10 +1497,11 @@ fn import_same_remotekey_different_url() { }], ); }) + .await } -#[test] -fn delete_remotekey_then_reimport_different_url() { +#[tokio::test] +async fn delete_remotekey_then_reimport_different_url() { run_test(|tester| async move { let _ = &tester; @@ -1534,10 +1547,11 @@ fn delete_remotekey_then_reimport_different_url() { vec![ImportRemotekeyStatus::Imported].into_iter(), ); }) + .await } -#[test] -fn import_only_duplicate_remotekeys() { +#[tokio::test] +async fn import_only_duplicate_remotekeys() { run_test(|tester| async move { let _ = &tester; let remotekeys = (0..3) @@ -1582,10 +1596,11 @@ fn import_only_duplicate_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_some_duplicate_remotekeys() { +#[tokio::test] +async fn import_some_duplicate_remotekeys() { run_test(|tester| async move { let _ = &tester; let num_remotekeys = 5; @@ -1649,10 +1664,11 @@ fn import_some_duplicate_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remote_and_local_keys() { +#[tokio::test] +async fn import_remote_and_local_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1714,10 +1730,11 @@ fn import_remote_and_local_keys() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await } -#[test] -fn import_same_local_and_remote_keys() { +#[tokio::test] +async fn import_same_local_and_remote_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1782,9 +1799,10 @@ fn import_same_local_and_remote_keys() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await } -#[test] -fn import_same_remote_and_local_keys() { +#[tokio::test] +async fn import_same_remote_and_local_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1847,10 +1865,11 @@ fn import_same_remote_and_local_keys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn delete_remotekeys_twice() { +#[tokio::test] +async fn delete_remotekeys_twice() { run_test(|tester| async move { let _ = &tester; @@ -1893,10 +1912,11 @@ fn delete_remotekeys_twice() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, Vec::new()); }) + .await } -#[test] -fn delete_nonexistent_remotekey() { +#[tokio::test] +async fn delete_nonexistent_remotekey() { run_test(|tester| async move { let _ = &tester; @@ -1919,10 +1939,11 @@ fn delete_nonexistent_remotekey() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, Vec::new()); }) + .await } -#[test] -fn delete_then_reimport_remotekeys() { +#[tokio::test] +async fn delete_then_reimport_remotekeys() { run_test(|tester| async move { let _ = &tester; @@ -1984,10 +2005,11 @@ fn delete_then_reimport_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer() { +#[tokio::test] +async fn import_remotekey_web3signer() { run_test(|tester| async move { let _ = &tester; @@ -2043,10 +2065,11 @@ fn import_remotekey_web3signer() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer_disabled() { +#[tokio::test] +async fn import_remotekey_web3signer_disabled() { run_test(|tester| async move { let _ = &tester; @@ -2096,10 +2119,11 @@ fn import_remotekey_web3signer_disabled() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer_enabled() { +#[tokio::test] +async fn import_remotekey_web3signer_enabled() { run_test(|tester| async move { let _ = &tester; @@ -2156,4 +2180,5 @@ fn import_remotekey_web3signer_enabled() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } diff --git a/validator_client/src/http_metrics/metrics.rs b/validator_client/src/http_metrics/metrics.rs index 8a52a4d35..52b52126b 100644 --- a/validator_client/src/http_metrics/metrics.rs +++ b/validator_client/src/http_metrics/metrics.rs @@ -29,6 +29,7 @@ pub const UPDATE_ATTESTERS_FETCH: &str = "update_attesters_fetch"; pub const UPDATE_ATTESTERS_STORE: &str = "update_attesters_store"; pub const ATTESTER_DUTIES_HTTP_POST: &str = "attester_duties_http_post"; pub const PROPOSER_DUTIES_HTTP_GET: &str = "proposer_duties_http_get"; +pub const VALIDATOR_DUTIES_SYNC_HTTP_POST: &str = "validator_duties_sync_http_post"; pub const VALIDATOR_ID_HTTP_GET: &str = "validator_id_http_get"; pub const SUBSCRIPTIONS_HTTP_POST: &str = "subscriptions_http_post"; pub const UPDATE_PROPOSERS: &str = "update_proposers"; diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 468fc2b06..f15ea27c9 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -8,7 +8,7 @@ use crate::signing_method::SigningMethod; use account_utils::{ - read_password, read_password_from_user, + read_password, read_password_from_user, read_password_string, validator_definitions::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, CONFIG_FILENAME, @@ -27,6 +27,7 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; @@ -43,6 +44,19 @@ const DEFAULT_REMOTE_SIGNER_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; +pub enum OnDecryptFailure { + /// If the key cache fails to decrypt, create a new cache. + CreateNew, + /// Return an error if the key cache fails to decrypt. This should only be + /// used in testing. + Error, +} + +pub struct KeystoreAndPassword { + pub keystore: Keystore, + pub password: Option<ZeroizeString>, +} + #[derive(Debug)] pub enum Error { /// Refused to open a validator with an existing lockfile since that validator may be in-use by @@ -97,6 +111,11 @@ pub enum Error { UnableToBuildWeb3SignerClient(ReqwestError), /// Unable to apply an action to a validator. InvalidActionOnValidator, + UnableToReadValidatorPassword(String), + UnableToReadKeystoreFile(eth2_keystore::Error), + UnableToSaveKeyCache(key_cache::Error), + UnableToDecryptKeyCache(key_cache::Error), + UnableToDeletePasswordFile(PathBuf, io::Error), } impl From<LockfileError> for Error { @@ -147,6 +166,10 @@ impl InitializedValidator { pub fn get_index(&self) -> Option<u64> { self.index } + + pub fn get_graffiti(&self) -> Option<Graffiti> { + self.graffiti + } } fn open_keystore(path: &Path) -> Result<Keystore, Error> { @@ -534,33 +557,78 @@ impl InitializedValidators { &mut self, pubkey: &PublicKey, is_local_keystore: bool, - ) -> Result<(), Error> { + ) -> Result<Option<KeystoreAndPassword>, Error> { // 1. Disable the validator definition. // // We disable before removing so that in case of a crash the auto-discovery mechanism // won't re-activate the keystore. - if let Some(def) = self + let mut uuid_opt = None; + let mut password_path_opt = None; + let keystore_and_password = if let Some(def) = self .definitions .as_mut_slice() .iter_mut() .find(|def| &def.voting_public_key == pubkey) { - // Update definition for local keystore - if def.signing_definition.is_local_keystore() && is_local_keystore { - def.enabled = false; - self.definitions - .save(&self.validators_dir) - .map_err(Error::UnableToSaveDefinitions)?; - } else if !def.signing_definition.is_local_keystore() && !is_local_keystore { - def.enabled = false; - } else { - return Err(Error::InvalidActionOnValidator); + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password, + voting_keystore_password_path, + .. + } if is_local_keystore => { + let password = match (voting_keystore_password, voting_keystore_password_path) { + (Some(password), _) => Some(password.clone()), + (_, Some(path)) => { + password_path_opt = Some(path.clone()); + read_password_string(path) + .map(Option::Some) + .map_err(Error::UnableToReadValidatorPassword)? + } + (None, None) => None, + }; + let keystore = Keystore::from_json_file(voting_keystore_path) + .map_err(Error::UnableToReadKeystoreFile)?; + uuid_opt = Some(*keystore.uuid()); + + def.enabled = false; + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Some(KeystoreAndPassword { keystore, password }) + } + SigningDefinition::Web3Signer(_) if !is_local_keystore => { + def.enabled = false; + None + } + _ => return Err(Error::InvalidActionOnValidator), } } else { return Err(Error::ValidatorNotInitialized(pubkey.clone())); + }; + + // 2. Remove the validator from the key cache. This ensures the key + // cache is consistent next time the VC starts. + // + // It's not a big deal if this succeeds and something fails later in + // this function because the VC will self-heal from a corrupt key cache. + // + // Do this before modifying `self.validators` or deleting anything from + // the filesystem. + if let Some(uuid) = uuid_opt { + let key_cache = KeyCache::open_or_create(&self.validators_dir) + .map_err(Error::UnableToOpenKeyCache)?; + let mut decrypted_key_cache = self + .decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::CreateNew) + .await?; + decrypted_key_cache.remove(&uuid); + decrypted_key_cache + .save(&self.validators_dir) + .map_err(Error::UnableToSaveKeyCache)?; } - // 2. Delete from `self.validators`, which holds the signing method. + // 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 { @@ -578,14 +646,28 @@ impl InitializedValidators { } } - // 3. Delete from validator definitions entirely. + // 4. Delete from validator definitions entirely. self.definitions .retain(|def| &def.voting_public_key != pubkey); self.definitions .save(&self.validators_dir) .map_err(Error::UnableToSaveDefinitions)?; - Ok(()) + // 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 + .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))?; + } + } + + Ok(keystore_and_password) } /// Attempt to delete the voting keystore file, or its entire validator directory. @@ -671,8 +753,8 @@ impl InitializedValidators { self.validators.get(public_key) } - /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals` - /// values. + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, + /// `builder_proposals`, and `graffiti` values. /// /// ## Notes /// @@ -682,7 +764,7 @@ impl InitializedValidators { /// /// If a `gas_limit` is included in the call to this function, it will also be updated and saved /// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition` - /// or `InitializedValidator`. The same logic applies to `builder_proposals`. + /// or `InitializedValidator`. The same logic applies to `builder_proposals` and `graffiti`. /// /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. pub async fn set_validator_definition_fields( @@ -691,6 +773,7 @@ impl InitializedValidators { enabled: Option<bool>, gas_limit: Option<u64>, builder_proposals: Option<bool>, + graffiti: Option<GraffitiString>, ) -> Result<(), Error> { if let Some(def) = self .definitions @@ -708,6 +791,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { def.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti.clone() { + def.graffiti = Some(graffiti); + } } self.update_validators().await?; @@ -723,6 +809,9 @@ impl InitializedValidators { if let Some(builder_proposals) = builder_proposals { val.builder_proposals = Some(builder_proposals); } + if let Some(graffiti) = graffiti { + val.graffiti = Some(graffiti.into()); + } } self.definitions @@ -888,10 +977,11 @@ impl InitializedValidators { /// filesystem accesses for keystores that are already known. In the case that a keystore /// from the validator definitions is not yet in this map, it will be loaded from disk and /// inserted into the map. - async fn decrypt_key_cache( + pub async fn decrypt_key_cache( &self, mut cache: KeyCache, key_stores: &mut HashMap<PathBuf, Keystore>, + on_failure: OnDecryptFailure, ) -> Result<KeyCache, Error> { // Read relevant key stores from the filesystem. let mut definitions_map = HashMap::new(); @@ -959,11 +1049,13 @@ impl InitializedValidators { //decrypt tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) { - Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache, - _ => KeyCache::new(), + Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => Ok(cache), + _ if matches!(on_failure, OnDecryptFailure::CreateNew) => Ok(KeyCache::new()), + Err(e) => Err(e), }) .await - .map_err(Error::TokioJoin) + .map_err(Error::TokioJoin)? + .map_err(Error::UnableToDecryptKeyCache) } /// Scans `self.definitions` and attempts to initialize and validators which are not already @@ -1001,7 +1093,8 @@ impl InitializedValidators { // Only decrypt cache when there is at least one local definition. // Decrypting cache is a very expensive operation which is never used for web3signer. let mut key_cache = if has_local_definitions { - self.decrypt_key_cache(cache, &mut key_stores).await? + self.decrypt_key_cache(cache, &mut key_stores, OnDecryptFailure::CreateNew) + .await? } else { // Assign an empty KeyCache if all definitions are of the Web3Signer type. KeyCache::new() @@ -1179,4 +1272,41 @@ impl InitializedValidators { val.index = Some(index); } } + + /// Deletes any passwords stored in the validator definitions file and + /// returns a map of pubkey to deleted password. + /// + /// This should only be used for testing, it's rather destructive. + pub fn delete_passwords_from_validator_definitions( + &mut self, + ) -> Result<HashMap<PublicKey, ZeroizeString>, Error> { + let mut passwords = HashMap::default(); + + for def in self.definitions.as_mut_slice() { + match &mut def.signing_definition { + SigningDefinition::LocalKeystore { + ref mut voting_keystore_password, + .. + } => { + if let Some(password) = voting_keystore_password.take() { + passwords.insert(def.voting_public_key.clone(), password); + } + } + // Remote signers don't have passwords. + SigningDefinition::Web3Signer { .. } => (), + }; + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(passwords) + } + + /// Prefer other methods in production. Arbitrarily modifying a validator + /// definition manually may result in inconsistencies. + pub fn as_mut_slice_testing_only(&mut self) -> &mut [ValidatorDefinition] { + self.definitions.as_mut_slice() + } } diff --git a/validator_client/src/key_cache.rs b/validator_client/src/key_cache.rs index b7abaaed0..c2dd7aa8f 100644 --- a/validator_client/src/key_cache.rs +++ b/validator_client/src/key_cache.rs @@ -47,6 +47,12 @@ pub struct KeyCache { type SerializedKeyMap = HashMap<Uuid, ZeroizeHash>; +impl Default for KeyCache { + fn default() -> Self { + Self::new() + } +} + impl KeyCache { pub fn new() -> Self { KeyCache { diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 3dde49f22..6925e285f 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -82,6 +82,7 @@ const HTTP_SYNC_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_SSZ_TIMEOUT_QUOTIENT: u32 = 4; const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; @@ -98,6 +99,8 @@ pub struct ProductionValidatorClient<T: EthSpec> { slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option<SocketAddr>, config: Config, + beacon_nodes: Arc<BeaconNodeFallback<SystemTimeSlotClock, T>>, + genesis_time: u64, } impl<T: EthSpec> ProductionValidatorClient<T> { @@ -146,7 +149,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { context .clone() .executor - .spawn_without_exit(async move { server.await }, "metrics-api"); + .spawn_without_exit(server, "metrics-api"); Some(ctx) } else { @@ -172,9 +175,12 @@ impl<T: EthSpec> ProductionValidatorClient<T> { let new_validators = validator_defs .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) .map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?; - validator_defs - .save(&config.validator_dir) - .map_err(|e| format!("Unable to update validator definitions: {:?}", e))?; + validator_defs.save(&config.validator_dir).map_err(|e| { + format!( + "Provide --suggested-fee-recipient or update validator definitions: {:?}", + e + ) + })?; info!( log, "Completed validator discovery"; @@ -304,6 +310,8 @@ impl<T: EthSpec> ProductionValidatorClient<T> { / 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_ssz: slot_duration + / HTTP_GET_VALIDATOR_BLOCK_SSZ_TIMEOUT_QUOTIENT, } } else { Timeouts::set_all(slot_duration) @@ -487,6 +495,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> { .beacon_nodes(beacon_nodes.clone()) .runtime_context(context.service_context("preparation".into())) .builder_registration_timestamp_override(config.builder_registration_timestamp_override) + .validator_registration_batch_size(config.validator_registration_batch_size) .build()?; let sync_committee_service = SyncCommitteeService::new( @@ -497,12 +506,6 @@ impl<T: EthSpec> ProductionValidatorClient<T> { context.service_context("sync_committee".into()), ); - // Wait until genesis has occurred. - // - // It seems most sensible to move this into the `start_service` function, but I'm caution - // of making too many changes this close to genesis (<1 week). - wait_for_genesis(&beacon_nodes, genesis_time, &context).await?; - Ok(Self { context, duties_service, @@ -515,17 +518,57 @@ impl<T: EthSpec> ProductionValidatorClient<T> { config, slot_clock, http_api_listen_addr: None, + genesis_time, + beacon_nodes, }) } - pub fn start_service(&mut self) -> Result<(), String> { + pub async fn start_service(&mut self) -> Result<(), String> { // We use `SLOTS_PER_EPOCH` as the capacity of the block notification channel, because - // we don't except notifications to be delayed by more than a single slot, let alone a + // we don't expect notifications to be delayed by more than a single slot, let alone a // whole epoch! let channel_capacity = T::slots_per_epoch() as usize; let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); let log = self.context.log(); + let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; + + self.http_api_listen_addr = if self.config.http_api.enabled { + let ctx = Arc::new(http_api::Context { + task_executor: self.context.executor.clone(), + api_secret, + validator_store: Some(self.validator_store.clone()), + validator_dir: Some(self.config.validator_dir.clone()), + secrets_dir: Some(self.config.secrets_dir.clone()), + graffiti_file: self.config.graffiti_file.clone(), + graffiti_flag: self.config.graffiti, + spec: self.context.eth2_config.spec.clone(), + config: self.config.http_api.clone(), + sse_logging_components: self.context.sse_logging_components.clone(), + slot_clock: self.slot_clock.clone(), + log: log.clone(), + _phantom: PhantomData, + }); + + let exit = self.context.executor.exit(); + + let (listen_addr, server) = http_api::serve(ctx, exit) + .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; + + self.context + .clone() + .executor + .spawn_without_exit(server, "http-api"); + + Some(listen_addr) + } else { + info!(log, "HTTP API server is disabled"); + None + }; + + // Wait until genesis has occurred. + wait_for_genesis(&self.beacon_nodes, self.genesis_time, &self.context).await?; + duties_service::start_update_service(self.duties_service.clone(), block_service_tx); self.block_service @@ -564,40 +607,6 @@ impl<T: EthSpec> ProductionValidatorClient<T> { spawn_notifier(self).map_err(|e| format!("Failed to start notifier: {}", e))?; - let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; - - self.http_api_listen_addr = if self.config.http_api.enabled { - let ctx = Arc::new(http_api::Context { - task_executor: self.context.executor.clone(), - api_secret, - validator_store: Some(self.validator_store.clone()), - validator_dir: Some(self.config.validator_dir.clone()), - graffiti_file: self.config.graffiti_file.clone(), - graffiti_flag: self.config.graffiti, - spec: self.context.eth2_config.spec.clone(), - config: self.config.http_api.clone(), - sse_logging_components: self.context.sse_logging_components.clone(), - slot_clock: self.slot_clock.clone(), - log: log.clone(), - _phantom: PhantomData, - }); - - let exit = self.context.executor.exit(); - - let (listen_addr, server) = http_api::serve(ctx, exit) - .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; - - self.context - .clone() - .executor - .spawn_without_exit(async move { server.await }, "http-api"); - - Some(listen_addr) - } else { - info!(log, "HTTP API server is disabled"); - None - }; - if self.config.enable_latency_measurement_service { latency::start_latency_service( self.context.clone(), @@ -622,8 +631,8 @@ async fn init_from_beacon_node<E: EthSpec>( let num_available = beacon_nodes.num_available().await; let num_total = beacon_nodes.num_total(); - let proposer_available = beacon_nodes.num_available().await; - let proposer_total = beacon_nodes.num_total(); + let proposer_available = proposer_nodes.num_available().await; + let proposer_total = proposer_nodes.num_total(); if proposer_total > 0 && proposer_available == 0 { warn!( diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index 732ae68ff..909e64a78 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -94,8 +94,7 @@ async fn notify<T: SlotClock + 'static, E: EthSpec>( info!( log, "No validators present"; - "msg" => "see `lighthouse account validator create --help` \ - or the HTTP API documentation" + "msg" => "see `lighthouse vm create --help` or the HTTP API documentation" ) } else if total_validators == attesting_validators { info!( diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index 5bd93a505..2d2221680 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -1,5 +1,5 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; -use crate::validator_store::{DoppelgangerStatus, ValidatorStore}; +use crate::validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; use crate::OfflineOnFailure; use bls::PublicKeyBytes; use environment::RuntimeContext; @@ -23,9 +23,6 @@ const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; /// Number of epochs to wait before re-submitting validator registration. const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1; -/// The number of validator registrations to include per request to the beacon node. -const VALIDATOR_REGISTRATION_BATCH_SIZE: usize = 500; - /// Builds an `PreparationService`. pub struct PreparationServiceBuilder<T: SlotClock + 'static, E: EthSpec> { validator_store: Option<Arc<ValidatorStore<T, E>>>, @@ -33,6 +30,7 @@ pub struct PreparationServiceBuilder<T: SlotClock + 'static, E: EthSpec> { beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>, context: Option<RuntimeContext<E>>, builder_registration_timestamp_override: Option<u64>, + validator_registration_batch_size: Option<usize>, } impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> { @@ -43,6 +41,7 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> { beacon_nodes: None, context: None, builder_registration_timestamp_override: None, + validator_registration_batch_size: None, } } @@ -74,6 +73,14 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> { self } + pub fn validator_registration_batch_size( + mut self, + validator_registration_batch_size: usize, + ) -> Self { + self.validator_registration_batch_size = Some(validator_registration_batch_size); + self + } + pub fn build(self) -> Result<PreparationService<T, E>, String> { Ok(PreparationService { inner: Arc::new(Inner { @@ -91,6 +98,9 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> { .ok_or("Cannot build PreparationService without runtime_context")?, builder_registration_timestamp_override: self .builder_registration_timestamp_override, + validator_registration_batch_size: self.validator_registration_batch_size.ok_or( + "Cannot build PreparationService without validator_registration_batch_size", + )?, validator_registration_cache: RwLock::new(HashMap::new()), }), }) @@ -107,6 +117,7 @@ pub struct Inner<T, E: EthSpec> { // Used to track unpublished validator registration changes. validator_registration_cache: RwLock<HashMap<ValidatorRegistrationKey, SignedValidatorRegistrationData>>, + validator_registration_batch_size: usize, } #[derive(Hash, Eq, PartialEq, Debug, Clone)] @@ -431,8 +442,23 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> { .await { Ok(data) => data, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for registration data"; + "pubkey" => ?pubkey, + ); + continue; + } Err(e) => { - error!(log, "Unable to sign validator registration data"; "error" => ?e, "pubkey" => ?pubkey); + error!( + log, + "Unable to sign validator registration data"; + "error" => ?e, + "pubkey" => ?pubkey + ); continue; } }; @@ -447,7 +473,7 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> { } if !signed.is_empty() { - for batch in signed.chunks(VALIDATOR_REGISTRATION_BATCH_SIZE) { + for batch in signed.chunks(self.validator_registration_batch_size) { match self .beacon_nodes .first_success( @@ -462,7 +488,7 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> { Ok(()) => info!( log, "Published validator registrations to the builder network"; - "count" => registration_data_len, + "count" => batch.len(), ), Err(e) => warn!( log, diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index cc20cedfc..e01bf09cf 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -1,5 +1,9 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; -use crate::{duties_service::DutiesService, validator_store::ValidatorStore, OfflineOnFailure}; +use crate::{ + duties_service::DutiesService, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, + OfflineOnFailure, +}; use environment::RuntimeContext; use eth2::types::BlockId; use futures::future::join_all; @@ -264,6 +268,18 @@ impl<T: SlotClock + 'static, E: EthSpec> SyncCommitteeService<T, E> { .await { Ok(signature) => Some(signature), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync committee signature"; + "pubkey" => ?pubkey, + "validator_index" => duty.validator_index, + "slot" => slot, + ); + None + } Err(e) => { crit!( log, @@ -405,6 +421,17 @@ impl<T: SlotClock + 'static, E: EthSpec> SyncCommitteeService<T, E> { .await { Ok(signed_contribution) => Some(signed_contribution), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync contribution"; + "pubkey" => ?pubkey, + "slot" => slot, + ); + None + } Err(e) => { crit!( log, diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 73843579a..365f7f734 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -5,7 +5,7 @@ use crate::{ signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}, Config, }; -use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use parking_lot::{Mutex, RwLock}; use slashing_protection::{ interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, @@ -170,7 +170,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { pub async fn add_validator_keystore<P: AsRef<Path>>( &self, voting_keystore_path: P, - password: ZeroizeString, + password_storage: PasswordStorage, enable: bool, graffiti: Option<GraffitiString>, suggested_fee_recipient: Option<Address>, @@ -179,7 +179,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> { ) -> Result<ValidatorDefinition, String> { let mut validator_def = ValidatorDefinition::new_keystore_with_password( voting_keystore_path, - Some(password), + password_storage, graffiti.map(Into::into), suggested_fee_recipient, gas_limit, diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml new file mode 100644 index 000000000..35af2b1ce --- /dev/null +++ b/validator_manager/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "validator_manager" +version = "0.1.0" +edition = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls = { workspace = true } +clap = { workspace = true } +types = { workspace = true } +environment = { workspace = true } +eth2_network_config = { workspace = true } +clap_utils = { workspace = true } +eth2_wallet = { workspace = true } +eth2_keystore = { workspace = true } +account_utils = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +ethereum_serde_utils = { workspace = true } +tree_hash = { workspace = true } +eth2 = { workspace = true } +hex = { workspace = true } +tokio = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +regex = { workspace = true } +validator_client = { workspace = true } diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs new file mode 100644 index 000000000..6a3f93a3f --- /dev/null +++ b/validator_manager/src/common.rs @@ -0,0 +1,361 @@ +use account_utils::{strip_off_newlines, ZeroizeString}; +use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; +use eth2::{ + lighthouse_vc::{ + http_client::ValidatorClientHttpClient, + std_types::{ImportKeystoreStatus, ImportKeystoresRequest, SingleKeystoreResponse, Status}, + types::UpdateFeeRecipientRequest, + }, + SensitiveUrl, +}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use tree_hash::TreeHash; +use types::*; + +pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; +pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; +pub const COUNT_FLAG: &str = "count"; + +/// When the `ethereum/staking-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 +/// can use here, so we choose a static string that is: +/// +/// 1. High enough that it's accepted by Launchpad. +/// 2. Weird enough to identify Lighthouse. +const LIGHTHOUSE_DEPOSIT_CLI_VERSION: &str = "20.18.20"; + +#[derive(Debug)] +pub enum UploadError { + InvalidPublicKey, + DuplicateValidator(PublicKeyBytes), + FailedToListKeys(eth2::Error), + KeyUploadFailed(eth2::Error), + IncorrectStatusCount(usize), + FeeRecipientUpdateFailed(eth2::Error), + PatchValidatorFailed(eth2::Error), +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorSpecification { + pub voting_keystore: KeystoreJsonStr, + pub voting_keystore_password: ZeroizeString, + pub slashing_protection: Option<InterchangeJsonStr>, + pub fee_recipient: Option<Address>, + pub gas_limit: Option<u64>, + pub builder_proposals: Option<bool>, + pub enabled: Option<bool>, +} + +impl ValidatorSpecification { + /// Upload the validator to a validator client via HTTP. + pub async fn upload( + self, + http_client: &ValidatorClientHttpClient, + ignore_duplicates: bool, + ) -> Result<Status<ImportKeystoreStatus>, UploadError> { + let ValidatorSpecification { + voting_keystore, + voting_keystore_password, + slashing_protection, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = self; + + let voting_public_key = voting_keystore + .public_key() + .ok_or(UploadError::InvalidPublicKey)? + .into(); + + let request = ImportKeystoresRequest { + keystores: vec![voting_keystore], + passwords: vec![voting_keystore_password], + slashing_protection, + }; + + // Check to see if this validator already exists on the remote validator. + match http_client.get_keystores().await { + Ok(response) => { + if response + .data + .iter() + .any(|validator| validator.validating_pubkey == voting_public_key) + { + if ignore_duplicates { + eprintln!( + "Duplicate validators are ignored, ignoring {:?} which exists \ + on the destination validator client", + voting_public_key + ); + } else { + return Err(UploadError::DuplicateValidator(voting_public_key)); + } + } + } + Err(e) => { + return Err(UploadError::FailedToListKeys(e)); + } + }; + + let mut statuses = http_client + .post_keystores(&request) + .await + .map_err(UploadError::KeyUploadFailed)? + .data; + + let status = statuses.pop().ok_or(UploadError::IncorrectStatusCount(0))?; + if !statuses.is_empty() { + return Err(UploadError::IncorrectStatusCount(statuses.len() + 1)); + } + + // Exit early if there's an error uploading. + if status.status == ImportKeystoreStatus::Error { + return Ok(status); + } + + if let Some(fee_recipient) = fee_recipient { + http_client + .post_fee_recipient( + &voting_public_key, + &UpdateFeeRecipientRequest { + ethaddress: fee_recipient, + }, + ) + .await + .map_err(UploadError::FeeRecipientUpdateFailed)?; + } + + if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { + http_client + .patch_lighthouse_validators( + &voting_public_key, + enabled, + gas_limit, + builder_proposals, + None, // Grafitti field is not maintained between validator moves. + ) + .await + .map_err(UploadError::PatchValidatorFailed)?; + } + + Ok(status) + } +} + +#[derive(Serialize, Deserialize)] +pub struct CreateSpec { + pub mnemonic: String, + pub validator_client_url: Option<SensitiveUrl>, + pub validator_client_token_path: Option<PathBuf>, + pub json_deposit_data_path: Option<PathBuf>, + pub ignore_duplicates: bool, + pub validators: Vec<ValidatorSpecification>, +} + +/// The structure generated by the `staking-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 +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct StandardDepositDataJson { + #[serde(with = "public_key_bytes_without_0x_prefix")] + pub pubkey: PublicKeyBytes, + #[serde(with = "hash256_without_0x_prefix")] + pub withdrawal_credentials: Hash256, + /// The `amount` field is *not* quoted (i.e., a string) like most other `u64` fields in the + /// consensus specs, it's a simple integer. + pub amount: u64, + #[serde(with = "signature_bytes_without_0x_prefix")] + pub signature: SignatureBytes, + #[serde(with = "bytes_4_without_0x_prefix")] + pub fork_version: [u8; 4], + pub network_name: String, + #[serde(with = "hash256_without_0x_prefix")] + pub deposit_message_root: Hash256, + #[serde(with = "hash256_without_0x_prefix")] + pub deposit_data_root: Hash256, + pub deposit_cli_version: String, +} + +impl StandardDepositDataJson { + pub fn new( + keypair: &Keypair, + withdrawal_credentials: Hash256, + amount: u64, + spec: &ChainSpec, + ) -> Result<Self, String> { + let deposit_data = { + let mut deposit_data = DepositData { + pubkey: keypair.pk.clone().into(), + withdrawal_credentials, + amount, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + deposit_data + }; + + let deposit_message_root = deposit_data.as_deposit_message().tree_hash_root(); + let deposit_data_root = deposit_data.tree_hash_root(); + + let DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + } = deposit_data; + + Ok(Self { + pubkey, + withdrawal_credentials, + amount, + signature, + fork_version: spec.genesis_fork_version, + network_name: spec + .config_name + .clone() + .ok_or("The network specification does not have a CONFIG_NAME set")?, + deposit_message_root, + deposit_data_root, + deposit_cli_version: LIGHTHOUSE_DEPOSIT_CLI_VERSION.to_string(), + }) + } +} + +macro_rules! without_0x_prefix { + ($mod_name: ident, $type: ty) => { + pub mod $mod_name { + use super::*; + use std::str::FromStr; + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = $type; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("ascii hex without a 0x prefix") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + <$type>::from_str(&format!("0x{}", v)).map_err(serde::de::Error::custom) + } + } + + /// Serialize with quotes. + pub fn serialize<S>(value: &$type, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let with_prefix = format!("{:?}", value); + let without_prefix = with_prefix + .strip_prefix("0x") + .ok_or_else(|| serde::ser::Error::custom("serialization is missing 0x"))?; + serializer.serialize_str(&without_prefix) + } + + /// Deserialize with quotes. + pub fn deserialize<'de, D>(deserializer: D) -> Result<$type, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Visitor) + } + } + }; +} + +without_0x_prefix!(hash256_without_0x_prefix, Hash256); +without_0x_prefix!(signature_bytes_without_0x_prefix, SignatureBytes); +without_0x_prefix!(public_key_bytes_without_0x_prefix, PublicKeyBytes); + +mod bytes_4_without_0x_prefix { + use serde::de::Error; + + const BYTES_LEN: usize = 4; + + pub fn serialize<S>(bytes: &[u8; BYTES_LEN], serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let hex_string = &hex::encode(bytes); + serializer.serialize_str(hex_string) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; BYTES_LEN], D::Error> + where + D: serde::Deserializer<'de>, + { + let decoded = deserializer.deserialize_str(serde_utils::hex::HexVisitor)?; + + if decoded.len() != BYTES_LEN { + return Err(D::Error::custom(format!( + "expected {} bytes for array, got {}", + BYTES_LEN, + decoded.len() + ))); + } + + let mut array = [0; BYTES_LEN]; + array.copy_from_slice(&decoded); + Ok(array) + } +} + +pub async fn vc_http_client<P: AsRef<Path>>( + url: SensitiveUrl, + token_path: P, +) -> Result<(ValidatorClientHttpClient, Vec<SingleKeystoreResponse>), String> { + let token_path = token_path.as_ref(); + let token_bytes = + fs::read(token_path).map_err(|e| format!("Failed to read {:?}: {:?}", token_path, e))?; + let token_string = String::from_utf8(strip_off_newlines(token_bytes)) + .map_err(|e| format!("Failed to parse {:?} as utf8: {:?}", token_path, e))?; + let http_client = ValidatorClientHttpClient::new(url.clone(), token_string).map_err(|e| { + format!( + "Could not instantiate HTTP client from URL and secret: {:?}", + e + ) + })?; + + // Perform a request to check that the connection works + let remote_keystores = http_client + .get_keystores() + .await + .map_err(|e| format!("Failed to list keystores on VC: {:?}", e))? + .data; + + eprintln!( + "Validator client is reachable at {} and reports {} validators", + url, + remote_keystores.len() + ); + + Ok((http_client, remote_keystores)) +} + +/// Write some object to a file as JSON. +/// +/// The file must be created new, it must not already exist. +pub fn write_to_json_file<P: AsRef<Path>, S: Serialize>( + path: P, + contents: &S, +) -> Result<(), String> { + eprintln!("Writing {:?}", path.as_ref()); + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?; + serde_json::to_writer(&mut file, contents) + .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) +} diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs new file mode 100644 index 000000000..8ea740ff5 --- /dev/null +++ b/validator_manager/src/create_validators.rs @@ -0,0 +1,934 @@ +use super::common::*; +use crate::DumpConfig; +use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; +use clap::{App, Arg, ArgMatches}; +use eth2::{ + lighthouse_vc::std_types::KeystoreJsonStr, + types::{StateId, ValidatorId}, + BeaconNodeHttpClient, SensitiveUrl, Timeouts, +}; +use eth2_wallet::WalletBuilder; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; +use types::*; + +pub const CMD: &str = "create"; +pub const OUTPUT_PATH_FLAG: &str = "output-path"; +pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; +pub const DISABLE_DEPOSITS_FLAG: &str = "disable-deposits"; +pub const FIRST_INDEX_FLAG: &str = "first-index"; +pub const MNEMONIC_FLAG: &str = "mnemonic-path"; +pub const SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; +pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; +pub const GAS_LIMIT_FLAG: &str = "gas-limit"; +pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; +pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; +pub const BEACON_NODE_FLAG: &str = "beacon-node"; +pub const FORCE_BLS_WITHDRAWAL_CREDENTIALS: &str = "force-bls-withdrawal-credentials"; + +pub const VALIDATORS_FILENAME: &str = "validators.json"; +pub const DEPOSITS_FILENAME: &str = "deposits.json"; + +const BEACON_NODE_HTTP_TIMEOUT: Duration = Duration::from_secs(2); + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "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.", + ) + .arg( + Arg::with_name(OUTPUT_PATH_FLAG) + .long(OUTPUT_PATH_FLAG) + .value_name("DIRECTORY") + .help( + "The path to a directory where the validator and (optionally) deposits \ + files will be created. The directory will be created if it does not exist.", + ) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(DEPOSIT_GWEI_FLAG) + .long(DEPOSIT_GWEI_FLAG) + .value_name("DEPOSIT_GWEI") + .help( + "The GWEI value of the deposit amount. Defaults to the minimum amount \ + required for an active validator (MAX_EFFECTIVE_BALANCE)", + ) + .conflicts_with(DISABLE_DEPOSITS_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(FIRST_INDEX_FLAG) + .long(FIRST_INDEX_FLAG) + .value_name("FIRST_INDEX") + .help("The first of consecutive key indexes you wish to create.") + .takes_value(true) + .required(false) + .default_value("0"), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("VALIDATOR_COUNT") + .help("The number of validators to create, regardless of how many already exist") + .conflicts_with("at-most") + .takes_value(true), + ) + .arg( + Arg::with_name(MNEMONIC_FLAG) + .long(MNEMONIC_FLAG) + .value_name("MNEMONIC_PATH") + .help("If present, the mnemonic will be read in from this file.") + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_INPUTS_FLAG) + .takes_value(false) + .hidden(cfg!(windows)) + .long(STDIN_INPUTS_FLAG) + .help("If present, read all user inputs from stdin instead of tty."), + ) + .arg( + Arg::with_name(DISABLE_DEPOSITS_FLAG) + .long(DISABLE_DEPOSITS_FLAG) + .help( + "When provided don't generate the deposits JSON file that is \ + commonly used for submitting validator deposits via a web UI. \ + Using this flag will save several seconds per validator if the \ + user has an alternate strategy for submitting deposits.", + ), + ) + .arg( + Arg::with_name(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) + .long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) + .help( + "If present, the user will be prompted to enter the voting keystore \ + password that will be used to encrypt the voting keystores. If this \ + flag is not provided, a random password will be used. It is not \ + necessary to keep backups of voting keystore passwords if the \ + mnemonic is safely backed up.", + ), + ) + .arg( + Arg::with_name(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .long(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "If this field is set, the given eth1 address will be used to create the \ + withdrawal credentials. Otherwise, it will generate withdrawal credentials \ + with the mnemonic-derived withdrawal public key in EIP-2334 format.", + ) + .conflicts_with(DISABLE_DEPOSITS_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(GAS_LIMIT_FLAG) + .long(GAS_LIMIT_FLAG) + .value_name("UINT64") + .help( + "All created validators will use this gas limit. It is recommended \ + to leave this as the default value by not specifying this flag.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(FEE_RECIPIENT_FLAG) + .long(FEE_RECIPIENT_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "All created validators will use this value for the suggested \ + fee recipient. Omit this flag to use the default value from the VC.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(BUILDER_PROPOSALS_FLAG) + .long(BUILDER_PROPOSALS_FLAG) + .help( + "When provided, all created validators will attempt to create \ + blocks via builder rather than the local EL.", + ) + .required(false) + .possible_values(&["true", "false"]) + .takes_value(true), + ) + .arg( + Arg::with_name(BEACON_NODE_FLAG) + .long(BEACON_NODE_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a beacon node using the beacon-API. \ + If this value is provided, an error will be raised if any validator \ + key here is already known as a validator by that beacon node. This helps \ + prevent the same validator being created twice and therefore slashable \ + conditions.", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(FORCE_BLS_WITHDRAWAL_CREDENTIALS) + .takes_value(false) + .long(FORCE_BLS_WITHDRAWAL_CREDENTIALS) + .help( + "If present, allows BLS withdrawal credentials rather than an execution \ + address. This is not recommended.", + ), + ) +} + +/// The CLI arguments are parsed into this struct before running the application. This step of +/// indirection allows for testing the underlying logic without needing to parse CLI arguments. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct CreateConfig { + pub output_path: PathBuf, + pub first_index: u32, + pub count: u32, + pub deposit_gwei: u64, + pub mnemonic_path: Option<PathBuf>, + pub stdin_inputs: bool, + pub disable_deposits: bool, + pub specify_voting_keystore_password: bool, + pub eth1_withdrawal_address: Option<Address>, + pub builder_proposals: Option<bool>, + pub fee_recipient: Option<Address>, + pub gas_limit: Option<u64>, + pub bn_url: Option<SensitiveUrl>, + pub force_bls_withdrawal_credentials: bool, +} + +impl CreateConfig { + fn from_cli(matches: &ArgMatches, spec: &ChainSpec) -> Result<Self, String> { + Ok(Self { + output_path: clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?, + deposit_gwei: clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? + .unwrap_or(spec.max_effective_balance), + first_index: clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?, + count: clap_utils::parse_required(matches, COUNT_FLAG)?, + mnemonic_path: clap_utils::parse_optional(matches, MNEMONIC_FLAG)?, + stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), + disable_deposits: matches.is_present(DISABLE_DEPOSITS_FLAG), + specify_voting_keystore_password: matches + .is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG), + eth1_withdrawal_address: clap_utils::parse_optional( + matches, + ETH1_WITHDRAWAL_ADDRESS_FLAG, + )?, + builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, + fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, + gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, + bn_url: clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?, + force_bls_withdrawal_credentials: matches.is_present(FORCE_BLS_WITHDRAWAL_CREDENTIALS), + }) + } +} + +struct ValidatorsAndDeposits { + validators: Vec<ValidatorSpecification>, + deposits: Option<Vec<StandardDepositDataJson>>, +} + +impl ValidatorsAndDeposits { + async fn new<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<Self, String> { + let CreateConfig { + // The output path is handled upstream. + output_path: _, + first_index, + count, + deposit_gwei, + mnemonic_path, + stdin_inputs, + disable_deposits, + specify_voting_keystore_password, + eth1_withdrawal_address, + builder_proposals, + fee_recipient, + gas_limit, + bn_url, + force_bls_withdrawal_credentials, + } = config; + + // Since Capella, it really doesn't make much sense to use BLS + // withdrawal credentials. Try to guide users away from doing so. + if eth1_withdrawal_address.is_none() && !force_bls_withdrawal_credentials { + return Err(format!( + "--{ETH1_WITHDRAWAL_ADDRESS_FLAG} is required. See --help for more information." + )); + } + + if count == 0 { + return Err(format!("--{} cannot be 0", COUNT_FLAG)); + } + + let bn_http_client = if let Some(bn_url) = bn_url { + let bn_http_client = + BeaconNodeHttpClient::new(bn_url, Timeouts::set_all(BEACON_NODE_HTTP_TIMEOUT)); + + /* + * Print the version of the remote beacon node. + */ + let version = bn_http_client + .get_node_version() + .await + .map_err(|e| format!("Failed to test connection to beacon node: {:?}", e))? + .data + .version; + eprintln!("Connected to beacon node running version {}", version); + + /* + * Attempt to ensure that the beacon node is on the same network. + */ + let bn_config = bn_http_client + .get_config_spec::<types::Config>() + .await + .map_err(|e| format!("Failed to get spec from beacon node: {:?}", e))? + .data; + if let Some(config_name) = &bn_config.config_name { + eprintln!("Beacon node is on {} network", config_name) + } + let bn_spec = bn_config + .apply_to_chain_spec::<T>(&T::default_spec()) + .ok_or("Beacon node appears to be on an incorrect network")?; + if bn_spec.genesis_fork_version != spec.genesis_fork_version { + if let Some(config_name) = bn_spec.config_name { + eprintln!("Beacon node is on {} network", config_name) + } + return Err("Beacon node appears to be on the wrong network".to_string()); + } + + Some(bn_http_client) + } else { + None + }; + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; + let voting_keystore_password = if specify_voting_keystore_password { + eprintln!("Please enter a voting keystore password when prompted."); + Some(read_password_from_user(stdin_inputs)?) + } else { + None + }; + + /* + * Generate a wallet to be used for HD key generation. + */ + + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password_string(); + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password_string(); + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + /* + * Start deriving individual validators. + */ + + eprintln!( + "Starting derivation of {} keystores. Each keystore may take several seconds.", + count + ); + + let mut validators = Vec::with_capacity(count as usize); + let mut deposits = (!disable_deposits).then(Vec::new); + + for (i, derivation_index) in (first_index..first_index + count).enumerate() { + // If the voting keystore password was not provided by the user then use a unique random + // string for each validator. + let voting_keystore_password = voting_keystore_password + .clone() + .unwrap_or_else(random_password_string); + + // Set the wallet to the appropriate derivation index. + wallet + .set_nextaccount(derivation_index) + .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + + // Derive the keystore from the HD wallet. + let keystores = wallet + .next_validator( + wallet_password.as_ref(), + voting_keystore_password.as_ref(), + withdrawal_keystore_password.as_ref(), + ) + .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; + let voting_keystore = keystores.voting; + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| { + format!("Validator keystore at index {} is missing a public key", i) + })? + .into(); + + // If the user has provided a beacon node URL, check that the validator doesn't already + // exist in the beacon chain. + if let Some(bn_http_client) = &bn_http_client { + match bn_http_client + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(voting_public_key), + ) + .await + { + Ok(Some(_)) => { + return Err(format!( + "Validator {:?} at derivation index {} already exists in the beacon chain. \ + This indicates a slashing risk, be sure to never run the same validator on two \ + 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 + ), + Err(e) => { + return Err(format!( + "Error checking if validator exists in beacon chain: {:?}", + e + )) + } + } + } + + if let Some(deposits) = &mut deposits { + // Decrypt the voting keystore so a deposit message can be signed. + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_ref()) + .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + + // Sanity check to ensure the keystore is reporting the correct public key. + if PublicKeyBytes::from(voting_keypair.pk.clone()) != voting_public_key { + return Err(format!( + "Mismatch for keystore public key and derived public key \ + for derivation index {}", + derivation_index + )); + } + + let withdrawal_credentials = + if let Some(eth1_withdrawal_address) = eth1_withdrawal_address { + WithdrawalCredentials::eth1(eth1_withdrawal_address, spec) + } else { + // Decrypt the withdrawal keystore so withdrawal credentials can be created. It's + // not strictly necessary to decrypt the keystore since we can read the pubkey + // directly from the keystore. However we decrypt the keystore to be more certain + // that we have access to the withdrawal keys. + let withdrawal_keypair = keystores + .withdrawal + .decrypt_keypair(withdrawal_keystore_password.as_ref()) + .map_err(|e| { + format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e) + })?; + WithdrawalCredentials::bls(&withdrawal_keypair.pk, spec) + }; + + // Create a JSON structure equivalent to the one generated by + // `ethereum/staking-deposit-cli`. + let json_deposit = StandardDepositDataJson::new( + &voting_keypair, + withdrawal_credentials.into(), + deposit_gwei, + spec, + )?; + + deposits.push(json_deposit); + } + + let validator = ValidatorSpecification { + voting_keystore: KeystoreJsonStr(voting_keystore), + voting_keystore_password: voting_keystore_password.clone(), + // New validators have no slashing protection history. + slashing_protection: None, + fee_recipient, + gas_limit, + builder_proposals, + // Allow the VC to choose a default "enabled" state. Since "enabled" is not part of + // the standard API, leaving this as `None` means we are not forced to use the + // non-standard API. + enabled: None, + }; + + eprintln!( + "Completed {}/{}: {:?}", + i.saturating_add(1), + count, + voting_public_key + ); + + validators.push(validator); + } + + Ok(Self { + validators, + deposits, + }) + } +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + spec: &ChainSpec, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = CreateConfig::from_cli(matches, spec)?; + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run::<T>(config, spec).await + } +} + +async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { + let output_path = config.output_path.clone(); + + if !output_path.exists() { + fs::create_dir(&output_path) + .map_err(|e| format!("Failed to create {:?} directory: {:?}", output_path, e))?; + } else if !output_path.is_dir() { + return Err(format!("{:?} must be a directory", output_path)); + } + + let validators_path = output_path.join(VALIDATORS_FILENAME); + if validators_path.exists() { + return Err(format!( + "{:?} already exists, refusing to overwrite", + validators_path + )); + } + let deposits_path = output_path.join(DEPOSITS_FILENAME); + if deposits_path.exists() { + return Err(format!( + "{:?} already exists, refusing to overwrite", + deposits_path + )); + } + + let validators_and_deposits = ValidatorsAndDeposits::new::<T>(config, spec).await?; + + eprintln!("Keystore generation complete"); + + write_to_json_file(&validators_path, &validators_and_deposits.validators)?; + + if let Some(deposits) = &validators_and_deposits.deposits { + write_to_json_file(&deposits_path, deposits)?; + } + + Ok(()) +} + +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] +#[cfg(test)] +pub mod tests { + use super::*; + use eth2_network_config::Eth2NetworkConfig; + use regex::Regex; + use std::path::Path; + use std::str::FromStr; + use tempfile::{tempdir, TempDir}; + use tree_hash::TreeHash; + + type E = MainnetEthSpec; + + const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "2.3.0"; + + fn junk_execution_address() -> Option<Address> { + Some(Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap()) + } + + pub struct TestBuilder { + spec: ChainSpec, + output_dir: TempDir, + mnemonic_dir: TempDir, + config: CreateConfig, + } + + impl Default for TestBuilder { + fn default() -> Self { + Self::new(E::default_spec()) + } + } + + impl TestBuilder { + pub fn new(spec: ChainSpec) -> Self { + let output_dir = tempdir().unwrap(); + let mnemonic_dir = tempdir().unwrap(); + let mnemonic_path = mnemonic_dir.path().join("mnemonic"); + fs::write( + &mnemonic_path, + "test test test test test test test test test test test waste", + ) + .unwrap(); + + let config = CreateConfig { + output_path: output_dir.path().into(), + first_index: 0, + count: 1, + deposit_gwei: spec.max_effective_balance, + mnemonic_path: Some(mnemonic_path), + stdin_inputs: false, + disable_deposits: false, + specify_voting_keystore_password: false, + eth1_withdrawal_address: junk_execution_address(), + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + bn_url: None, + force_bls_withdrawal_credentials: false, + }; + + Self { + spec, + output_dir, + mnemonic_dir, + config, + } + } + + pub fn mutate_config<F: Fn(&mut CreateConfig)>(mut self, func: F) -> Self { + func(&mut self.config); + self + } + + pub async fn run_test(self) -> TestResult { + let Self { + spec, + output_dir, + mnemonic_dir, + config, + } = self; + + let result = run::<E>(config.clone(), &spec).await; + + if result.is_ok() { + let validators_file_contents = + fs::read_to_string(output_dir.path().join(VALIDATORS_FILENAME)).unwrap(); + let validators: Vec<ValidatorSpecification> = + serde_json::from_str(&validators_file_contents).unwrap(); + + assert_eq!(validators.len(), config.count as usize); + + for (i, validator) in validators.iter().enumerate() { + let voting_keystore = &validator.voting_keystore.0; + let keypair = voting_keystore + .decrypt_keypair(validator.voting_keystore_password.as_ref()) + .unwrap(); + assert_eq!(keypair.pk, voting_keystore.public_key().unwrap()); + assert_eq!( + voting_keystore.path().unwrap(), + format!("m/12381/3600/{}/0/0", config.first_index as usize + i) + ); + assert!(validator.slashing_protection.is_none()); + assert_eq!(validator.fee_recipient, config.fee_recipient); + assert_eq!(validator.gas_limit, config.gas_limit); + assert_eq!(validator.builder_proposals, config.builder_proposals); + assert_eq!(validator.enabled, None); + } + + let deposits_path = output_dir.path().join(DEPOSITS_FILENAME); + if config.disable_deposits { + assert!(!deposits_path.exists()); + } else { + let deposits_file_contents = fs::read_to_string(&deposits_path).unwrap(); + let deposits: Vec<StandardDepositDataJson> = + serde_json::from_str(&deposits_file_contents).unwrap(); + + assert_eq!(deposits.len(), config.count as usize); + + for (validator, deposit) in validators.iter().zip(deposits.iter()) { + let validator_pubkey = validator.voting_keystore.0.public_key().unwrap(); + assert_eq!(deposit.pubkey, validator_pubkey.clone().into()); + if let Some(address) = config.eth1_withdrawal_address { + assert_eq!( + deposit.withdrawal_credentials.as_bytes()[0], + spec.eth1_address_withdrawal_prefix_byte + ); + assert_eq!( + &deposit.withdrawal_credentials.as_bytes()[12..], + address.as_bytes() + ); + } else { + assert_eq!( + deposit.withdrawal_credentials.as_bytes()[0], + spec.bls_withdrawal_prefix_byte + ); + } + assert_eq!(deposit.amount, config.deposit_gwei); + let deposit_message = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: SignatureBytes::empty(), + } + .as_deposit_message(); + assert!(deposit.signature.decompress().unwrap().verify( + &validator_pubkey, + deposit_message.signing_root(spec.get_deposit_domain()) + )); + assert_eq!(deposit.fork_version, spec.genesis_fork_version); + assert_eq!(&deposit.network_name, spec.config_name.as_ref().unwrap()); + assert_eq!( + deposit.deposit_message_root, + deposit_message.tree_hash_root() + ); + assert_eq!( + deposit.deposit_data_root, + DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone() + } + .tree_hash_root() + ); + } + } + } + + // The directory containing the mnemonic can now be removed. + drop(mnemonic_dir); + + TestResult { result, output_dir } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + pub struct TestResult { + pub result: Result<(), String>, + pub output_dir: TempDir, + } + + impl TestResult { + pub fn validators_file_path(&self) -> PathBuf { + self.output_dir.path().join(VALIDATORS_FILENAME) + } + + pub fn validators(&self) -> Vec<ValidatorSpecification> { + let contents = fs::read_to_string(self.validators_file_path()).unwrap(); + serde_json::from_str(&contents).unwrap() + } + + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err(self) { + assert!(self.result.is_err()) + } + } + + #[tokio::test] + async fn default_test_values() { + TestBuilder::default().run_test().await.assert_ok(); + } + + #[tokio::test] + async fn no_eth1_address_without_force() { + TestBuilder::default() + .mutate_config(|config| { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = false + }) + .run_test() + .await + .assert_err(); + } + + #[tokio::test] + async fn bls_withdrawal_credentials() { + TestBuilder::default() + .mutate_config(|config| { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = true + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn default_test_values_deposits_disabled() { + TestBuilder::default() + .mutate_config(|config| config.disable_deposits = true) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn count_is_zero() { + TestBuilder::default() + .mutate_config(|config| config.count = 0) + .run_test() + .await + .assert_err(); + } + + #[tokio::test] + async fn eth1_withdrawal_addresses() { + TestBuilder::default() + .mutate_config(|config| { + config.count = 2; + config.eth1_withdrawal_address = junk_execution_address(); + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn non_zero_first_index() { + TestBuilder::default() + .mutate_config(|config| { + config.first_index = 2; + config.count = 2; + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn misc_modifications() { + TestBuilder::default() + .mutate_config(|config| { + config.deposit_gwei = 42; + config.builder_proposals = Some(true); + config.gas_limit = Some(1337); + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn bogus_bn_url() { + TestBuilder::default() + .mutate_config(|config| { + config.bn_url = + Some(SensitiveUrl::from_str("http://sdjfvwfhsdhfschwkeyfwhwlga.com").unwrap()); + }) + .run_test() + .await + .assert_err(); + } + + #[tokio::test] + async fn staking_deposit_cli_vectors() { + let vectors_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test_vectors") + .join("vectors"); + for entry in fs::read_dir(vectors_dir).unwrap() { + let entry = entry.unwrap(); + let file_name = entry.file_name(); + let vector_name = file_name.to_str().unwrap(); + let path = entry.path(); + // Leave this `println!` so we can tell which test fails. + println!("Running test {}", vector_name); + run_test_vector(vector_name, &path).await; + } + } + + async fn run_test_vector<P: AsRef<Path>>(name: &str, vectors_path: P) { + /* + * Parse the test vector name into a set of test parameters. + */ + let re = Regex::new(r"(.*)_(.*)_(.*)_(.*)_(.*)_(.*)_(.*)").unwrap(); + let capture = re.captures_iter(name).next().unwrap(); + let network = capture.get(1).unwrap().as_str(); + let first = u32::from_str(capture.get(3).unwrap().as_str()).unwrap(); + let count = u32::from_str(capture.get(5).unwrap().as_str()).unwrap(); + let uses_eth1 = bool::from_str(capture.get(7).unwrap().as_str()).unwrap(); + + /* + * Use the test parameters to generate equivalent files "locally" (i.e., with our code). + */ + + let spec = Eth2NetworkConfig::constant(network) + .unwrap() + .unwrap() + .chain_spec::<E>() + .unwrap(); + + let test_result = TestBuilder::new(spec) + .mutate_config(|config| { + config.first_index = first; + config.count = count; + if uses_eth1 { + config.eth1_withdrawal_address = Some( + Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap(), + ); + } else { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = true; + } + }) + .run_test() + .await; + let TestResult { result, output_dir } = test_result; + result.expect("local generation should succeed"); + + /* + * Ensure the deposit data is identical when parsed as JSON. + */ + + let local_deposits = { + let path = output_dir.path().join(DEPOSITS_FILENAME); + let contents = fs::read_to_string(&path).unwrap(); + let mut deposits: Vec<StandardDepositDataJson> = + serde_json::from_str(&contents).unwrap(); + for deposit in &mut deposits { + // Ensures we can match test vectors. + deposit.deposit_cli_version = TEST_VECTOR_DEPOSIT_CLI_VERSION.to_string(); + + // We use "prater" and the vectors use "goerli" now. The two names refer to the same + // network so there should be no issue here. + if deposit.network_name == "prater" { + deposit.network_name = "goerli".to_string(); + } + } + deposits + }; + let vector_deposits: Vec<StandardDepositDataJson> = { + let path = fs::read_dir(vectors_path.as_ref().join("validator_keys")) + .unwrap() + .find_map(|entry| { + let entry = entry.unwrap(); + let file_name = entry.file_name(); + if file_name.to_str().unwrap().starts_with("deposit_data") { + Some(entry.path()) + } else { + None + } + }) + .unwrap(); + let contents = fs::read_to_string(path).unwrap(); + serde_json::from_str(&contents).unwrap() + }; + + assert_eq!(local_deposits, vector_deposits); + + /* + * Note: we don't check the keystores generated by the deposit-cli since there is little + * value in this. + * + * If we check the deposits then we are verifying the signature across the deposit message. + * This implicitly verifies that the keypair generated by the deposit-cli is identical to + * the one created by Lighthouse. + */ + } +} diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs new file mode 100644 index 000000000..4b924189f --- /dev/null +++ b/validator_manager/src/import_validators.rs @@ -0,0 +1,436 @@ +use super::common::*; +use crate::DumpConfig; +use clap::{App, Arg, ArgMatches}; +use eth2::{lighthouse_vc::std_types::ImportKeystoreStatus, SensitiveUrl}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +pub const CMD: &str = "import"; +pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; +pub const VC_URL_FLAG: &str = "vc-url"; +pub const VC_TOKEN_FLAG: &str = "vc-token"; + +pub const DETECTED_DUPLICATE_MESSAGE: &str = "Duplicate validator detected!"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "Uploads validators to a validator client using the HTTP API. The validators \ + are defined in a JSON file which can be generated using the \"create-validators\" \ + command.", + ) + .arg( + Arg::with_name(VALIDATORS_FILE_FLAG) + .long(VALIDATORS_FILE_FLAG) + .value_name("PATH_TO_JSON_FILE") + .help( + "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\".", + ) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(VC_URL_FLAG) + .long(VC_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + If this value is not supplied then a 'dry run' will be conducted where \ + no changes are made to the validator client.", + ) + .default_value("http://localhost:5062") + .requires(VC_TOKEN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(VC_TOKEN_FLAG) + .long(VC_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the validator client.") + .takes_value(true), + ) + .arg( + Arg::with_name(IGNORE_DUPLICATES_FLAG) + .takes_value(false) + .long(IGNORE_DUPLICATES_FLAG) + .help( + "If present, ignore any validators which already exist on the VC. \ + Without this flag, the process will terminate without making any changes. \ + This flag should be used with caution, whilst it does not directly cause \ + slashable conditions, it might be an indicator that something is amiss. \ + Users should also be careful to avoid submitting duplicate deposits for \ + validators that already exist on the VC.", + ), + ) +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct ImportConfig { + pub validators_file_path: PathBuf, + pub vc_url: SensitiveUrl, + pub vc_token_path: PathBuf, + pub ignore_duplicates: bool, +} + +impl ImportConfig { + fn from_cli(matches: &ArgMatches) -> Result<Self, String> { + Ok(Self { + validators_file_path: clap_utils::parse_required(matches, VALIDATORS_FILE_FLAG)?, + vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, + ignore_duplicates: matches.is_present(IGNORE_DUPLICATES_FLAG), + }) + } +} + +pub async fn cli_run<'a>( + matches: &'a ArgMatches<'a>, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = ImportConfig::from_cli(matches)?; + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run(config).await + } +} + +async fn run<'a>(config: ImportConfig) -> Result<(), String> { + let ImportConfig { + validators_file_path, + vc_url, + vc_token_path, + ignore_duplicates, + } = config; + + if !validators_file_path.exists() { + return Err(format!("Unable to find file at {:?}", validators_file_path)); + } + + let validators_file = fs::OpenOptions::new() + .read(true) + .create(false) + .open(&validators_file_path) + .map_err(|e| format!("Unable to open {:?}: {:?}", validators_file_path, e))?; + let validators: Vec<ValidatorSpecification> = serde_json::from_reader(&validators_file) + .map_err(|e| { + format!( + "Unable to parse JSON in {:?}: {:?}", + validators_file_path, e + ) + })?; + + let count = validators.len(); + + let (http_client, _keystores) = vc_http_client(vc_url.clone(), &vc_token_path).await?; + + eprintln!( + "Starting to submit {} validators to VC, each validator may take several seconds", + count + ); + + 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 => { + 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)); + } + } + } + e @ Err(UploadError::InvalidPublicKey) => { + eprintln!("Validator {} has an invalid public key", i); + return Err(format!("{:?}", e)); + } + ref e @ Err(UploadError::DuplicateValidator(voting_public_key)) => { + eprintln!( + "Duplicate validator {:?} already exists on the destination validator client. \ + This may indicate that some validators are running in two places at once, which \ + can lead to slashing. If you are certain that there is no risk, add the --{} flag.", + voting_public_key, IGNORE_DUPLICATES_FLAG + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::FailedToListKeys(e)) => { + eprintln!( + "Failed to list keystores. Some keys may have been imported whilst \ + others may not have been imported. 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.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::KeyUploadFailed(e)) => { + eprintln!( + "Failed to upload keystore. Some keys may have been imported whilst \ + others may not have been imported. 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.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::IncorrectStatusCount(count)) => { + eprintln!( + "Keystore was uploaded, however the validator client returned an invalid response. \ + 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.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!( + "Invalid status count in import response: {}", + count + )); + } + Err(UploadError::FeeRecipientUpdateFailed(e)) => { + eprintln!( + "Failed to set fee recipient for validator {}. This value may need \ + to be set manually. Continuing with other validators. Error was {:?}", + i, e + ); + } + Err(UploadError::PatchValidatorFailed(e)) => { + eprintln!( + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ + These values value may need to be set manually. Continuing with other validators. \ + Error was {:?}", + i, e + ); + } + } + } + + Ok(()) +} + +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] +#[cfg(test)] +pub mod tests { + use super::*; + use crate::create_validators::tests::TestBuilder as CreateTestBuilder; + use std::fs; + use tempfile::{tempdir, TempDir}; + use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + + const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; + + pub struct TestBuilder { + import_config: ImportConfig, + pub vc: ApiTester, + /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up + /// before we can read it. + create_dir: Option<TempDir>, + _dir: TempDir, + } + + impl TestBuilder { + pub async fn new() -> Self { + Self::new_with_http_config(ApiTester::default_http_config()).await + } + + 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; + let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); + fs::write(&vc_token_path, &vc.api_token).unwrap(); + + Self { + import_config: ImportConfig { + // This field will be overwritten later on. + validators_file_path: dir.path().into(), + vc_url: vc.url.clone(), + vc_token_path, + ignore_duplicates: false, + }, + vc, + create_dir: None, + _dir: dir, + } + } + + pub fn mutate_import_config<F: Fn(&mut ImportConfig)>(mut self, func: F) -> Self { + func(&mut self.import_config); + self + } + + pub async fn create_validators(mut self, count: u32, first_index: u32) -> Self { + let create_result = CreateTestBuilder::default() + .mutate_config(|config| { + config.count = count; + config.first_index = first_index; + }) + .run_test() + .await; + assert!( + create_result.result.is_ok(), + "precondition: validators are created" + ); + self.import_config.validators_file_path = create_result.validators_file_path(); + self.create_dir = Some(create_result.output_dir); + self + } + + /// Imports validators without running the entire test suite in `Self::run_test`. This is + /// useful for simulating duplicate imports. + pub async fn import_validators_without_checks(self) -> Self { + run(self.import_config.clone()).await.unwrap(); + self + } + + pub async fn run_test(self) -> TestResult { + let result = run(self.import_config.clone()).await; + + if result.is_ok() { + self.vc.ensure_key_cache_consistency().await; + + let local_validators: Vec<ValidatorSpecification> = { + let contents = + fs::read_to_string(&self.import_config.validators_file_path).unwrap(); + serde_json::from_str(&contents).unwrap() + }; + let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; + + assert_eq!( + local_validators.len(), + list_keystores_response.len(), + "vc should have exactly the number of validators imported" + ); + + for local_validator in &local_validators { + let local_keystore = &local_validator.voting_keystore.0; + let local_pubkey = local_keystore.public_key().unwrap().into(); + let remote_validator = list_keystores_response + .iter() + .find(|validator| validator.validating_pubkey == local_pubkey) + .expect("validator must exist on VC"); + assert_eq!(&remote_validator.derivation_path, &local_keystore.path()); + assert_eq!(remote_validator.readonly, Some(false)); + } + } + + TestResult { + result, + vc: self.vc, + } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + pub struct TestResult { + pub result: Result<(), String>, + pub vc: ApiTester, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err_contains(self, msg: &str) { + assert!(self.result.unwrap_err().contains(msg)) + } + } + + #[tokio::test] + async fn create_one_validator() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators() { + TestBuilder::new() + .await + .create_validators(3, 0) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_one_validator_with_offset() { + TestBuilder::new() + .await + .create_validators(1, 42) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators_with_offset() { + TestBuilder::new() + .await + .create_validators(3, 1337) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn import_duplicates_when_disallowed() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_err_contains("DuplicateValidator"); + } + + #[tokio::test] + async fn import_duplicates_when_allowed() { + TestBuilder::new() + .await + .mutate_import_config(|config| { + config.ignore_duplicates = true; + }) + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_ok(); + } +} diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs new file mode 100644 index 000000000..6889ee79d --- /dev/null +++ b/validator_manager/src/lib.rs @@ -0,0 +1,85 @@ +use clap::App; +use clap::ArgMatches; +use common::write_to_json_file; +use environment::Environment; +use serde::Serialize; +use std::path::PathBuf; +use types::EthSpec; + +pub mod common; +pub mod create_validators; +pub mod import_validators; +pub mod move_validators; + +pub const CMD: &str = "validator_manager"; + +/// This flag is on the top-level `lighthouse` binary. +const DUMP_CONFIGS_FLAG: &str = "dump-config"; + +/// Used only in testing, this allows a command to dump its configuration to a file and then exit +/// successfully. This allows for testing how the CLI arguments translate to some configuration. +pub enum DumpConfig { + Disabled, + Enabled(PathBuf), +} + +impl DumpConfig { + /// Returns `Ok(true)` if the configuration was successfully written to a file and the + /// application should exit successfully without doing anything else. + pub fn should_exit_early<T: Serialize>(&self, config: &T) -> Result<bool, String> { + match self { + DumpConfig::Disabled => Ok(false), + DumpConfig::Enabled(dump_path) => { + dbg!(dump_path); + write_to_json_file(dump_path, config)?; + Ok(true) + } + } + } +} + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .visible_aliases(&["vm", "validator-manager", CMD]) + .about("Utilities for managing a Lighthouse validator client via the HTTP API.") + .subcommand(create_validators::cli_app()) + .subcommand(import_validators::cli_app()) + .subcommand(move_validators::cli_app()) +} + +/// Run the account manager, returning an error if the operation did not succeed. +pub fn run<'a, T: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment<T>) -> Result<(), String> { + let context = env.core_context(); + let spec = context.eth2_config.spec; + let dump_config = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? + .map(DumpConfig::Enabled) + .unwrap_or_else(|| DumpConfig::Disabled); + + context + .executor + // This `block_on_dangerous` call reasonable since it is at the very highest level of the + // application, the rest of which is all async. All other functions below this should be + // async and should never call `block_on_dangerous` themselves. + .block_on_dangerous( + async { + match matches.subcommand() { + (create_validators::CMD, Some(matches)) => { + create_validators::cli_run::<T>(matches, &spec, dump_config).await + } + (import_validators::CMD, Some(matches)) => { + import_validators::cli_run(matches, dump_config).await + } + (move_validators::CMD, Some(matches)) => { + move_validators::cli_run(matches, dump_config).await + } + ("", _) => Err("No command supplied. See --help.".to_string()), + (unknown, _) => Err(format!( + "{} is not a valid {} command. See --help.", + unknown, CMD + )), + } + }, + "validator_manager", + ) + .ok_or("Shutting down")? +} diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs new file mode 100644 index 000000000..fa886e8f9 --- /dev/null +++ b/validator_manager/src/move_validators.rs @@ -0,0 +1,1253 @@ +use super::common::*; +use crate::DumpConfig; +use account_utils::{read_password_from_user, ZeroizeString}; +use clap::{App, Arg, ArgMatches}; +use eth2::{ + lighthouse_vc::{ + std_types::{ + DeleteKeystoreStatus, DeleteKeystoresRequest, ImportKeystoreStatus, InterchangeJsonStr, + Status, + }, + types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, + }, + SensitiveUrl, +}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; +use tokio::time::sleep; +use types::{Address, PublicKeyBytes}; + +pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; +pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; + +pub const CMD: &str = "move"; +pub const SRC_VC_URL_FLAG: &str = "src-vc-url"; +pub const SRC_VC_TOKEN_FLAG: &str = "src-vc-token"; +pub const DEST_VC_URL_FLAG: &str = "dest-vc-url"; +pub const DEST_VC_TOKEN_FLAG: &str = "dest-vc-token"; +pub const VALIDATORS_FLAG: &str = "validators"; +pub const GAS_LIMIT_FLAG: &str = "gas-limit"; +pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; +pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; + +const NO_VALIDATORS_MSG: &str = "No validators present on source validator client"; + +const UPLOAD_RETRY_WAIT: Duration = Duration::from_secs(5); + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum PasswordSource { + /// Reads the password from the user via the terminal. + Interactive { stdin_inputs: bool }, + /// This variant is panic-y and should only be used during testing. + Testing(HashMap<PublicKeyBytes, Vec<String>>), +} + +impl PasswordSource { + fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result<ZeroizeString, String> { + match self { + PasswordSource::Interactive { stdin_inputs } => { + eprintln!("Please enter a password for keystore {:?}:", pubkey); + read_password_from_user(*stdin_inputs) + } + // This path with panic if the password list is empty. Since the + // password prompt will just keep retrying on a failed password, the + // panic helps us break the loop if we misconfigure the test. + PasswordSource::Testing(passwords) => Ok(passwords + .get_mut(pubkey) + .expect("pubkey should be known") + .remove(0) + .into()), + } + } +} + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "Uploads validators to a validator client using the HTTP API. The validators \ + are defined in a JSON file which can be generated using the \"create-validators\" \ + command. This command only supports validators signing via a keystore on the local \ + file system (i.e., not Web3Signer validators).", + ) + .arg( + Arg::with_name(SRC_VC_URL_FLAG) + .long(SRC_VC_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + This validator client is the \"source\" and contains the validators \ + that are to be moved.", + ) + .required(true) + .requires(SRC_VC_TOKEN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(SRC_VC_TOKEN_FLAG) + .long(SRC_VC_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the source validator client.") + .takes_value(true), + ) + .arg( + Arg::with_name(DEST_VC_URL_FLAG) + .long(DEST_VC_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + This validator client is the \"destination\" and will have new validators \ + added as they are removed from the \"source\" validator client.", + ) + .required(true) + .requires(DEST_VC_TOKEN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(DEST_VC_TOKEN_FLAG) + .long(DEST_VC_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the destination validator client.") + .takes_value(true), + ) + .arg( + Arg::with_name(VALIDATORS_FLAG) + .long(VALIDATORS_FLAG) + .value_name("STRING") + .help( + "The validators to be moved. Either a list of 0x-prefixed \ + validator pubkeys or the keyword \"all\".", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("VALIDATOR_COUNT") + .help("The number of validators to move.") + .conflicts_with(VALIDATORS_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(GAS_LIMIT_FLAG) + .long(GAS_LIMIT_FLAG) + .value_name("UINT64") + .help( + "All created validators will use this gas limit. It is recommended \ + to leave this as the default value by not specifying this flag.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(FEE_RECIPIENT_FLAG) + .long(FEE_RECIPIENT_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "All created validators will use this value for the suggested \ + fee recipient. Omit this flag to use the default value from the VC.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(BUILDER_PROPOSALS_FLAG) + .long(BUILDER_PROPOSALS_FLAG) + .help( + "When provided, all created validators will attempt to create \ + blocks via builder rather than the local EL.", + ) + .required(false) + .possible_values(&["true", "false"]) + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_INPUTS_FLAG) + .takes_value(false) + .hidden(cfg!(windows)) + .long(STDIN_INPUTS_FLAG) + .help("If present, read all user inputs from stdin instead of tty."), + ) +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum Validators { + All, + Count(usize), + Specific(Vec<PublicKeyBytes>), +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct MoveConfig { + pub src_vc_url: SensitiveUrl, + pub src_vc_token_path: PathBuf, + pub dest_vc_url: SensitiveUrl, + pub dest_vc_token_path: PathBuf, + pub validators: Validators, + pub builder_proposals: Option<bool>, + pub fee_recipient: Option<Address>, + pub gas_limit: Option<u64>, + pub password_source: PasswordSource, +} + +impl MoveConfig { + fn from_cli(matches: &ArgMatches) -> Result<Self, String> { + let count_flag = clap_utils::parse_optional(matches, COUNT_FLAG)?; + let validators_flag = matches.value_of(VALIDATORS_FLAG); + let validators = match (count_flag, validators_flag) { + (Some(count), None) => Validators::Count(count), + (None, Some(string)) => match string { + "all" => Validators::All, + pubkeys => pubkeys + .split(',') + .map(PublicKeyBytes::from_str) + .collect::<Result<Vec<_>, _>>() + .map(Validators::Specific)?, + }, + (None, None) => Err(format!( + "Must supply either --{VALIDATORS_FLAG} or --{COUNT_FLAG}." + ))?, + (Some(_), Some(_)) => { + Err("Cannot supply both --{VALIDATORS_FLAG} and --{COUNT_FLAG}.")? + } + }; + + Ok(Self { + src_vc_url: clap_utils::parse_required(matches, SRC_VC_URL_FLAG)?, + src_vc_token_path: clap_utils::parse_required(matches, SRC_VC_TOKEN_FLAG)?, + dest_vc_url: clap_utils::parse_required(matches, DEST_VC_URL_FLAG)?, + dest_vc_token_path: clap_utils::parse_required(matches, DEST_VC_TOKEN_FLAG)?, + validators, + builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, + fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, + gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, + password_source: PasswordSource::Interactive { + stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), + }, + }) + } +} + +pub async fn cli_run<'a>( + matches: &'a ArgMatches<'a>, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = MoveConfig::from_cli(matches)?; + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run(config).await + } +} + +async fn run<'a>(config: MoveConfig) -> Result<(), String> { + let MoveConfig { + src_vc_url, + src_vc_token_path, + dest_vc_url, + dest_vc_token_path, + validators, + builder_proposals, + fee_recipient, + gas_limit, + mut password_source, + } = config; + + // Moving validators between the same VC is unlikely to be useful and probably indicates a user + // error. + if src_vc_url == dest_vc_url { + return Err(format!( + "--{} and --{} must be different", + SRC_VC_URL_FLAG, DEST_VC_URL_FLAG + )); + } + + let (src_http_client, src_keystores) = + vc_http_client(src_vc_url.clone(), &src_vc_token_path).await?; + let (dest_http_client, _dest_keystores) = + vc_http_client(dest_vc_url.clone(), &dest_vc_token_path).await?; + + if src_keystores.is_empty() { + return Err(NO_VALIDATORS_MSG.to_string()); + } + + let pubkeys_to_move = match validators { + Validators::All => src_keystores.iter().map(|v| v.validating_pubkey).collect(), + Validators::Count(count) => { + let mut viable_pubkeys: Vec<_> = src_keystores + .iter() + .filter(|v| !v.readonly.unwrap_or(true)) + .map(|v| v.validating_pubkey) + .collect(); + viable_pubkeys.sort_unstable_by_key(PublicKeyBytes::serialize); + viable_pubkeys + .get(0..count) + .ok_or_else(|| { + format!( + "Cannot move {} keystores since source validator client only has {} \ + keystores which are able to be moved (not read-only).", + count, + viable_pubkeys.len() + ) + })? + .to_vec() + } + Validators::Specific(request_pubkeys) => { + let request_pubkeys_set: HashSet<_> = request_pubkeys.iter().collect(); + let src_pubkeys_set: HashSet<_> = + src_keystores.iter().map(|v| &v.validating_pubkey).collect(); + let difference = request_pubkeys_set + .difference(&src_pubkeys_set) + .collect::<Vec<_>>(); + if !difference.is_empty() { + for pk in &difference { + eprintln!("{:?} is not present on {:?}", pk, src_vc_url); + } + return Err(format!( + "{} validators not found on {:?}", + difference.len(), + src_vc_url + )); + } + request_pubkeys + } + }; + + let src_keystores_map: HashMap<_, _> = src_keystores + .iter() + .map(|k| (k.validating_pubkey, k)) + .collect(); + + let count = pubkeys_to_move.len(); + for (i, &pubkey_to_move) in pubkeys_to_move.iter().enumerate() { + // Skip read-only validators rather than exiting. This makes it a bit easier to use the + // "all" flag. + if src_keystores_map + .get(&pubkey_to_move) + .ok_or("Inconsistent src keystore map")? + .readonly + .unwrap_or(true) + { + eprintln!("Skipping read-only validator {:?}", pubkey_to_move); + } + + let request = DeleteKeystoresRequest { + pubkeys: vec![pubkey_to_move], + }; + let deleted = match src_http_client.delete_lighthouse_keystores(&request).await { + Ok(deleted) => deleted, + Err(e) => { + match src_http_client.get_keystores().await { + Ok(response) => { + if response + .data + .iter() + .any(|v| v.validating_pubkey == pubkey_to_move) + { + eprintln!( + "There was an error removing a validator, however the validator \ + is still present on the source validator client. The recommended \ + solution is to run this command again." + ); + } + } + Err(_) => { + eprintln!( + "There was an error removing a validator and it's unclear if \ + the validator was removed or not. Manual user intervention is \ + required." + ); + } + }; + + return Err(format!("Deleting {:?} failed with {:?}", pubkey_to_move, e)); + } + }; + + let ExportKeystoresResponse { + mut data, + slashing_protection, + } = deleted; + + if data.len() != 1 { + return Err(format!( + "Too many deleted validators from VC: {}", + data.len() + )); + } + + let exported_validator = data + .pop() + .ok_or("VC responded with zero deleted validators")?; + + let (voting_keystore, voting_keystore_password) = match exported_validator { + SingleExportKeystoresResponse { + status: + Status { + status: DeleteKeystoreStatus::Deleted, + message: _, + }, + validating_keystore, + validating_keystore_password, + } => match (validating_keystore, validating_keystore_password) { + (Some(keystore), Some(password)) => (keystore, password), + (Some(keystore), None) => { + eprintln!( + "Validator {:?} requires a password, please provide it to continue \ + moving validators. \ + The dest VC will store this password on its filesystem and the password \ + will not be required next time the dest VC starts. \ + If the provided password is incorrect the user will \ + be asked to provide another password. \ + Failing to provide the correct password now will \ + result in the keystore being deleted from the src VC \ + without being transfered to the dest VC. \ + It is strongly recommend to provide a password now rather than exiting.", + pubkey_to_move + ); + + // Read the password from the user, retrying if the password is incorrect. + loop { + match password_source.read_password(&pubkey_to_move) { + Ok(password) => { + if let Err(e) = keystore.decrypt_keypair(password.as_ref()) { + eprintln!("Failed to decrypt keystore: {:?}", e); + } else { + break (keystore, password); + } + } + Err(e) => { + eprintln!( + "Retrying after error: {:?}. If this error persists the user will need to \ + manually recover their keystore for validator {:?} from the mnemonic." + , + e, pubkey_to_move + ); + } + } + + // Add a sleep here to prevent spamming the console. + sleep(Duration::from_secs(1)).await; + } + } + (None, password_opt) => { + eprintln!( + "Validator {:?} was not moved since the validator client did \ + not return a keystore. It is likely that the \ + validator has been deleted from the source validator client \ + without being moved to the destination validator client. \ + This validator will most likely need to be manually recovered \ + from a mnemonic or backup.", + pubkey_to_move + ); + return Err(format!( + "VC returned deleted but keystore not present (password {})", + password_opt.is_some() + )); + } + }, + SingleExportKeystoresResponse { + status: Status { status, .. }, + .. + } if matches!( + status, + DeleteKeystoreStatus::NotFound | DeleteKeystoreStatus::NotActive + ) => + { + eprintln!( + "Validator {:?} was not moved since it was not found or not active. This scenario \ + is unexpected and might indicate that another process is also performing \ + an export from the source validator client. Exiting now for safety. \ + If there is definitely no other process exporting validators then it \ + may be safe to run this command again.", + pubkey_to_move + ); + return Err(format!( + "VC indicated that a previously known validator was {:?}", + status, + )); + } + SingleExportKeystoresResponse { + status: Status { status, message }, + .. + } => { + eprintln!( + "Validator {:?} was not moved because the source validator client \ + indicated there was an error disabling it. Manual intervention is \ + required to recover from this scenario.", + pubkey_to_move + ); + return Err(format!( + "VC returned status {:?} with message {:?}", + status, message + )); + } + }; + + let keystore_derivation_path = voting_keystore.0.path(); + + let validator_specification = ValidatorSpecification { + voting_keystore, + voting_keystore_password, + slashing_protection: Some(InterchangeJsonStr(slashing_protection)), + fee_recipient, + gas_limit, + builder_proposals, + // Allow the VC to choose a default "enabled" state. Since "enabled" is not part of + // the standard API, leaving this as `None` means we are not forced to use the + // non-standard API. + enabled: None, + }; + + // We might as well just ignore validators that already exist on the destination machine, + // there doesn't appear to be much harm just adding them again and removing them from the + // source VC is an improvement. + let ignore_duplicates = true; + + loop { + match validator_specification + .clone() + .upload(&dest_http_client, ignore_duplicates) + .await + { + Ok(status) => { + match status.status { + ImportKeystoreStatus::Imported => { + eprintln!("Moved keystore {} of {}", i + 1, count); + break; + } + ImportKeystoreStatus::Duplicate => { + eprintln!("Moved duplicate keystore {} of {} to the VC", i + 1, count); + break; + } + ImportKeystoreStatus::Error => { + eprintln!( + "Upload of keystore {} of {} failed with message: {:?}.", + i + 1, + count, + status.message, + ); + // Retry uploading this validator. + sleep_with_retry_message( + &pubkey_to_move, + keystore_derivation_path.as_deref(), + ) + .await; + } + } + } + e @ Err(UploadError::InvalidPublicKey) => { + eprintln!("Validator {} has an invalid public key", i); + return Err(format!("{:?}", e)); + } + Err(UploadError::DuplicateValidator(_)) => { + return Err( + "Duplicate validator detected when duplicates are ignored".to_string() + ); + } + Err(UploadError::FailedToListKeys(e)) => { + eprintln!( + "Failed to list keystores. Some keys may have been moved whilst \ + others may not. Error was {:?}", + e + ); + // Retry uploading this validator. + sleep_with_retry_message(&pubkey_to_move, keystore_derivation_path.as_deref()) + .await; + } + Err(UploadError::KeyUploadFailed(e)) => { + eprintln!( + "Failed to upload keystore. Some keys may have been moved whilst \ + others may not. Error was {:?}", + e + ); + // Retry uploading this validator. + sleep_with_retry_message(&pubkey_to_move, keystore_derivation_path.as_deref()) + .await; + } + Err(UploadError::IncorrectStatusCount(count)) => { + eprintln!( + "Keystore was uploaded, however the validator client returned an invalid response." + ); + return Err(format!( + "Invalid status count in import response: {}", + count + )); + } + Err(UploadError::FeeRecipientUpdateFailed(e)) => { + eprintln!( + "Failed to set fee recipient for validator {}. This value may need \ + to be set manually. Continuing with other validators. Error was {:?}", + i, e + ); + // Continue onto the next validator. + break; + } + Err(UploadError::PatchValidatorFailed(e)) => { + eprintln!( + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit). \ + These values value may need to be set manually. Continuing with other validators. \ + Error was {:?}", + i, e + ); + // Continue onto the next validator. + break; + } + } + eprintln!( + "Uploaded keystore {} of {} to the destination VC", + i + 1, + count + ); + } + } + + eprintln!("Done."); + + Ok(()) +} + +async fn sleep_with_retry_message(pubkey: &PublicKeyBytes, path: Option<&str>) { + let path = path.unwrap_or("<unspecified>"); + eprintln!( + "Sleeping for {:?} before retrying. Exiting the application before it completes \ + may result in the loss of a validator keystore. The keystore would need to be \ + restored from a backup or mnemonic. The keystore which may be lost has a public \ + key of {:?} and a derivation path of {}", + UPLOAD_RETRY_WAIT, pubkey, path + ); + sleep(UPLOAD_RETRY_WAIT).await +} + +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] +#[cfg(test)] +mod test { + use super::*; + use crate::import_validators::tests::TestBuilder as ImportTestBuilder; + use account_utils::validator_definitions::SigningDefinition; + use std::fs; + use tempfile::{tempdir, TempDir}; + use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + + const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; + const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; + + type MutatePasswordFn = Box<dyn Fn(&mut HashMap<PublicKeyBytes, Vec<String>>)>; + + struct TestBuilder { + src_import_builder: Option<ImportTestBuilder>, + dest_import_builder: Option<ImportTestBuilder>, + http_config: HttpConfig, + duplicates: usize, + dir: TempDir, + move_back_again: bool, + remove_passwords_from_src_vc: bool, + mutate_passwords: Option<MutatePasswordFn>, + passwords: HashMap<PublicKeyBytes, Vec<String>>, + use_password_files: bool, + reuse_password_files: Option<usize>, + } + + impl TestBuilder { + async fn new() -> Self { + let dir = tempdir().unwrap(); + Self { + src_import_builder: None, + dest_import_builder: None, + http_config: ApiTester::default_http_config(), + duplicates: 0, + dir, + move_back_again: false, + remove_passwords_from_src_vc: false, + mutate_passwords: None, + passwords: <_>::default(), + use_password_files: false, + reuse_password_files: None, + } + } + + fn move_back_again(mut self) -> Self { + self.move_back_again = true; + self + } + + fn use_password_files(mut self) -> Self { + self.use_password_files = true; + self.http_config.store_passwords_in_secrets_dir = true; + self + } + + fn reuse_password_files(mut self, index: usize) -> Self { + self.reuse_password_files = Some(index); + self + } + + async fn with_src_validators(mut self, count: u32, first_index: u32) -> Self { + let builder = ImportTestBuilder::new_with_http_config(self.http_config.clone()) + .await + .create_validators(count, first_index) + .await; + self.src_import_builder = Some(builder); + self + } + + async fn with_dest_validators(mut self, count: u32, first_index: u32) -> Self { + let builder = ImportTestBuilder::new_with_http_config(self.http_config.clone()) + .await + .create_validators(count, first_index) + .await; + self.dest_import_builder = Some(builder); + self + } + + fn register_duplicates(mut self, num_duplicates: usize) -> Self { + self.duplicates = num_duplicates; + self + } + + fn remove_passwords_from_src_vc(mut self) -> Self { + self.remove_passwords_from_src_vc = true; + self + } + + fn mutate_passwords<F: Fn(&mut HashMap<PublicKeyBytes, Vec<String>>) + 'static>( + mut self, + func: F, + ) -> Self { + self.mutate_passwords = Some(Box::new(func)); + self + } + + async fn move_validators<F>( + &self, + gen_validators_enum: F, + src_vc: &ApiTester, + dest_vc: &ApiTester, + ) -> Result<(), String> + where + F: Fn(&[PublicKeyBytes]) -> Validators, + { + let src_vc_token_path = self.dir.path().join(SRC_VC_TOKEN_FILE_NAME); + fs::write(&src_vc_token_path, &src_vc.api_token).unwrap(); + let (src_vc_client, src_vc_initial_keystores) = + vc_http_client(src_vc.url.clone(), &src_vc_token_path) + .await + .unwrap(); + + let src_vc_initial_pubkeys: Vec<_> = src_vc_initial_keystores + .iter() + .map(|k| k.validating_pubkey) + .collect(); + let validators = gen_validators_enum(&src_vc_initial_pubkeys); + + let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); + fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); + + let (dest_vc_client, dest_vc_initial_keystores) = + vc_http_client(dest_vc.url.clone(), &dest_vc_token_path) + .await + .unwrap(); + + let move_config = MoveConfig { + src_vc_url: src_vc.url.clone(), + src_vc_token_path, + dest_vc_url: dest_vc.url.clone(), + dest_vc_token_path: dest_vc_token_path.clone(), + validators: validators.clone(), + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + password_source: PasswordSource::Testing(self.passwords.clone()), + }; + + let result = run(move_config).await; + + if result.is_ok() { + let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; + let dest_vc_final_keystores = dest_vc_client.get_keystores().await.unwrap().data; + + src_vc.ensure_key_cache_consistency().await; + dest_vc.ensure_key_cache_consistency().await; + + match validators { + Validators::All => { + assert!( + src_vc_final_keystores.is_empty(), + "all keystores should be removed from source vc" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + src_vc_initial_keystores.len() + - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); + for initial_keystore in &src_vc_initial_keystores { + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the source keystore should be present at the dest" + ); + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", initial_keystore.validating_pubkey)) + .exists(), + "the source password file should be deleted" + ) + } + } + Validators::Count(count) => { + assert_eq!( + src_vc_final_keystores.len(), + src_vc_initial_keystores.len() - count, + "keystores should be removed from source vc" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + count - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); + let moved_keystores: Vec<_> = { + let initial_set: HashSet<_> = src_vc_initial_keystores.iter().collect(); + let final_set: HashSet<_> = src_vc_final_keystores.iter().collect(); + initial_set.difference(&final_set).cloned().collect() + }; + assert_eq!(moved_keystores.len(), count); + for moved_keystore in &moved_keystores { + assert!( + dest_vc_final_keystores.contains(moved_keystore), + "the moved keystore should be present at the dest" + ); + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", moved_keystore.validating_pubkey)) + .exists(), + "the source password file should be deleted" + ) + } + } + Validators::Specific(pubkeys) => { + assert_eq!( + src_vc_final_keystores.len(), + src_vc_initial_keystores + .len() + .checked_sub(pubkeys.len()) + .unwrap(), + "the correct count of validators should have been removed from the src" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + pubkeys.len() - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); + for pubkey in pubkeys { + let initial_keystore = src_vc_initial_keystores + .iter() + .find(|k| k.validating_pubkey == pubkey) + .unwrap(); + assert!( + !src_vc_final_keystores.contains(initial_keystore), + "the keystore should not be present at the source" + ); + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the keystore should be present at the dest" + ); + 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" + ) + } else { + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", pubkey)) + .exists(), + "the source password file should be deleted" + ) + } + } + } + } + + // If enabled, check that all VCs still have the password files for their validators. + if self.use_password_files { + src_vc_final_keystores + .iter() + .map(|keystore| (&src_vc, keystore)) + .chain( + dest_vc_final_keystores + .iter() + .map(|keystore| (&dest_vc, keystore)), + ) + .for_each(|(vc, keystore)| { + assert!( + vc.secrets_dir + .path() + .join(format!("{:?}", keystore.validating_pubkey)) + .exists(), + "the password file should exist" + ) + }); + } + } + + result + } + + async fn run_test<F>(mut self, gen_validators_enum: F) -> TestResult + where + F: Fn(&[PublicKeyBytes]) -> Validators + Copy, + { + let src_vc = if let Some(import_builder) = self.src_import_builder.take() { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new_with_http_config(self.http_config.clone()).await + }; + + // If enabled, set all the validator definitions on the src_vc to + // use the same password path as the given `master_index`. This + // helps test that we don't delete a password file if it's in use by + // another validator. + if let Some(primary_index) = self.reuse_password_files { + let mut initialized_validators = src_vc.initialized_validators.write(); + let definitions = initialized_validators.as_mut_slice_testing_only(); + // Find the path of the "primary" definition. + let primary_path = definitions + .get(primary_index) + .map(|def| match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => path.clone(), + _ => panic!("primary index does not have password path"), + }) + .unwrap(); + // Set all definitions to use the same password path as the primary. + definitions.iter_mut().enumerate().for_each(|(_, def)| { + match &mut def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => *path = primary_path.clone(), + _ => (), + } + }) + } + + let dest_vc = if let Some(import_builder) = self.dest_import_builder.take() { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new_with_http_config(self.http_config.clone()).await + }; + + if self.remove_passwords_from_src_vc { + let passwords = src_vc + .initialized_validators + .write() + .delete_passwords_from_validator_definitions() + .unwrap(); + + self.passwords = passwords + .into_iter() + .map(|(pubkey, password)| { + ( + PublicKeyBytes::from(&pubkey), + vec![password.as_str().to_string()], + ) + }) + .collect(); + + if let Some(func) = self.mutate_passwords.take() { + func(&mut self.passwords) + } + } + + let result = self + .move_validators(gen_validators_enum, &src_vc, &dest_vc) + .await; + + if self.move_back_again { + self.move_validators(gen_validators_enum, &dest_vc, &src_vc) + .await + .unwrap(); + } + + TestResult { result } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + struct TestResult { + result: Result<(), String>, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err(self) { + assert!(self.result.is_err()) + } + + fn assert_err_is(self, msg: String) { + assert_eq!(self.result, Err(msg)) + } + } + + #[tokio::test] + async fn no_validators() { + TestBuilder::new() + .await + .run_test(|_| Validators::All) + .await + .assert_err_is(NO_VALIDATORS_MSG.to_string()); + } + + #[tokio::test] + async fn one_validator_move_all() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validator_move_one() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .run_test(|pubkeys| Validators::Specific(pubkeys.to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validator_to_non_empty_dest() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .with_dest_validators(1, 10) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_all_where_one_is_a_duplicate() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .with_dest_validators(1, 1) + .await + .register_duplicates(1) + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_one_where_one_is_a_duplicate() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .with_dest_validators(2, 0) + .await + .register_duplicates(1) + .run_test(|pubkeys| Validators::Specific(pubkeys[0..1].to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_all() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_one() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|pubkeys| Validators::Specific(pubkeys[0..1].to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_two() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|pubkeys| Validators::Specific(pubkeys[0..2].to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_three() { + TestBuilder::new() + .await + .with_src_validators(3, 42) + .await + .run_test(|pubkeys| Validators::Specific(pubkeys.to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_one_by_count() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::Count(1)) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_two_by_count() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::Count(2)) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validators_move_two_by_count() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .run_test(|_| Validators::Count(2)) + .await + .assert_err(); + } + + #[tokio::test] + async fn two_validator_move_all_and_back_again() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .move_back_again() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validator_move_all_passwords_removed() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .remove_passwords_from_src_vc() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + /// This test simulates a src VC that doesn't know the keystore passwords + /// and provide the wrong password before providing the correct password. + #[tokio::test] + async fn two_validator_move_all_passwords_removed_failed_password_attempt() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .remove_passwords_from_src_vc() + .mutate_passwords(|passwords| { + passwords.iter_mut().for_each(|(_, passwords)| { + passwords.insert(0, "wrong-password".to_string()); + passwords.push("wrong-password".to_string()); + }) + }) + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + /// This test simulates a src VC that doesn't know the keystore passwords + /// and we have not provided the correct password. + #[should_panic] + #[tokio::test] + async fn two_validator_move_all_passwords_removed_without_correct_password() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .remove_passwords_from_src_vc() + .mutate_passwords(|passwords| { + passwords + .iter_mut() + .for_each(|(_, passwords)| *passwords = vec!["wrong-password".to_string()]) + }) + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validator_move_all_with_password_files() { + TestBuilder::new() + .await + .use_password_files() + .with_src_validators(1, 0) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_one_with_identical_password_files() { + TestBuilder::new() + .await + .use_password_files() + // The password file for validator 0 will be shared with other + // validators on the src vc. + .reuse_password_files(0) + .with_src_validators(2, 0) + .await + .run_test(|validators| Validators::Specific(validators[0..1].to_vec())) + .await + .assert_ok(); + } +} diff --git a/validator_manager/test_vectors/.gitignore b/validator_manager/test_vectors/.gitignore new file mode 100644 index 000000000..3fec32c84 --- /dev/null +++ b/validator_manager/test_vectors/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py new file mode 100644 index 000000000..722414de7 --- /dev/null +++ b/validator_manager/test_vectors/generate.py @@ -0,0 +1,123 @@ +# This script uses the `ethereum/staking-deposit-cli` tool to generate +# deposit data files which are then used for testing by Lighthouse. +# +# To generate vectors, simply run this Python script: +# +# `python generate.py` +# +import os +import sys +import shutil +import subprocess +from subprocess import Popen, PIPE, STDOUT + + +NUM_VALIDATORS=3 +TEST_MNEMONIC = "test test test test test test test test test test test waste" +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") +vectors_dir = os.path.join(".", "vectors") + + +def setup(): + cleanup() + + if os.path.exists(vectors_dir): + shutil.rmtree(vectors_dir) + + os.mkdir(tmp_dir) + os.mkdir(sdc_dir) + os.mkdir(vectors_dir) + + setup_sdc() + with open(mnemonic_path, "x") as file: + file.write(TEST_MNEMONIC) + + +def cleanup(): + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) + + # Remove all the keystores since we don't use them in testing. + if os.path.exists(vectors_dir): + for root, dirs, files in os.walk(vectors_dir): + for file in files: + if file.startswith("keystore"): + os.remove(os.path.join(root, file)) + + +def setup_sdc(): + result = subprocess.run([ + "git", + "clone", + "--single-branch", + "https://github.com/ethereum/staking-deposit-cli.git", + str(sdc_git_dir) + ]) + assert(result.returncode == 0) + result = subprocess.run([ + "pip", + "install", + "-r", + "requirements.txt", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + result = subprocess.run([ + "python", + "setup.py", + "install", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + + +def sdc_generate(network, first_index, count, eth1_withdrawal_address=None): + if eth1_withdrawal_address is not None: + eth1_flags = ['--eth1_withdrawal_address', eth1_withdrawal_address] + uses_eth1 = True + else: + eth1_flags = [] + uses_eth1 = False + + test_name = "{}_first_{}_count_{}_eth1_{}".format(network, first_index, count, + str(uses_eth1).lower()) + output_dir = os.path.join(vectors_dir, test_name) + os.mkdir(output_dir) + + command = [ + '/bin/sh', + 'deposit.sh', + '--language', 'english', + '--non_interactive', + 'existing-mnemonic', + '--validator_start_index', str(first_index), + '--num_validators', str(count), + '--mnemonic', TEST_MNEMONIC, + '--chain', network, + '--keystore_password', 'MyPassword', + '--folder', os.path.abspath(output_dir), + ] + eth1_flags + + print("Running " + test_name) + process = Popen(command, cwd=sdc_git_dir, text=True, stdin = PIPE) + process.wait() + + +def test_network(network): + sdc_generate(network, first_index=0, count=1) + sdc_generate(network, first_index=0, count=2) + sdc_generate(network, first_index=12, count=1) + sdc_generate(network, first_index=99, count=2) + sdc_generate(network, first_index=1024, count=3) + sdc_generate(network, first_index=0, count=2, + eth1_withdrawal_address="0x0f51bb10119727a7e5ea3538074fb341f56b09ad") + + +setup() +test_network("mainnet") +test_network("prater") +cleanup() diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json new file mode 100644 index 000000000..31c00c57f --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json @@ -0,0 +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.3.0"}] \ 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-1660803669.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json new file mode 100644 index 000000000..2880b7724 --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json @@ -0,0 +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.3.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.3.0"}] \ 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-1660803684.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json new file mode 100644 index 000000000..da92a1d0d --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json @@ -0,0 +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.3.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.3.0"}] \ 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-1660803679.json b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json new file mode 100644 index 000000000..9cc01dc0d --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json @@ -0,0 +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.3.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.3.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.3.0"}] \ 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-1660803672.json b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json new file mode 100644 index 000000000..3a971d095 --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json @@ -0,0 +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.3.0"}] \ 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-1660803675.json b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json new file mode 100644 index 000000000..2efa5c4ec --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json @@ -0,0 +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.3.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.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json b/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json new file mode 100644 index 000000000..c736d75b7 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "a940e0142ad9b56a1310326137347d1ada275b31b3748af4accc63bd189573376615be8e8ae047766c6d10864e54b2e7098177598edf3a043eb560bbdf1a1c12588375a054d1323a0900e2286d0993cde9675e5b74523e6e8e03715cc96b3ce5", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "28484efb20c961a1354689a556d4c352fe9deb24684efdb32d22e1af17e2a45d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json new file mode 100644 index 000000000..e86500d14 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "a940e0142ad9b56a1310326137347d1ada275b31b3748af4accc63bd189573376615be8e8ae047766c6d10864e54b2e7098177598edf3a043eb560bbdf1a1c12588375a054d1323a0900e2286d0993cde9675e5b74523e6e8e03715cc96b3ce5", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "28484efb20c961a1354689a556d4c352fe9deb24684efdb32d22e1af17e2a45d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "87b4b4e9c923aa9e1687219e9df0e838956ee6e15b7ab18142467430d00940dc7aa243c9996e85125dfe72d9dbdb00a30a36e16a2003ee0c86f29c9f5d74f12bfe5b7f62693dbf5187a093555ae8d6b48acd075788549c4b6a249b397af24cd0", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "ea80b639356a03f6f58e4acbe881fabefc9d8b93375a6aa7e530c77d7e45d3e4", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json new file mode 100644 index 000000000..c79ae5a4f --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "ab32595d8201c2b4e8173aece9151fdc15f4d2ad36008462d0416598ddbf0f37ed0877f06d284a9669e73dbc0885bd2207fe64385e95a4488dc2bcb2c324d5c20da3248a6244463583dfbba8db20805765421e59cb56b0bc3ee6d24a9218216d", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "b4df3a3a26dd5f6eb32999d8a7051a7d1a8573a16553d4b45ee706a0d59c1066", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "9655e195eda5517efe6f36bcebd45250c889a4177d7bf5fcd59598d2d03f37f038b5ee2ec079a30a8382ea42f351943f08a6f006bab9c2130db2742bd7315c8ad5aa1f03a0801c26d4c9efdef71c4c59c449c7f9b21fa62600ab8f5f1e2b938a", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "7661474fba11bfb453274f62df022cab3c0b6f4a58af4400f6bce83c9cb5fcb8", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json b/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json new file mode 100644 index 000000000..136dc3855 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json @@ -0,0 +1 @@ +[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "b5dae79ce8f3d7326b46f93182981c5f3d64257a457f038caa78ec8e5cc25a9fdac52c7beb221ab2a3205404131366ad18e1e13801393b3d486819e8cca96128bf1244884a91d05dced092c74bc1e7259788f30dd3432df15f3d2f629645f345", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "94213d76aba9e6a434589d4939dd3764e0832df78f66d30db22a760c14ba1b89", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "816f38a321c4f84ad5187eda58f6d9c1fd1e81c860ed1722bdb76b920fdd430a1e814b9bb893837ae3b38ad738684fbf1795fa687f617c52121472b1ac8d2e34e5c1127186233a8833ffb54c509d9e52cb7242c6c6a65b5e496296b3caa90d89", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "7ad1d059d69794680a1deef5e72c33827f0c449a5f0917095821c0343572789d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "95d20c35484dea6b2a0bd7c2da2d2e810d7829e14c03657b2524adfc2111aa5ed95908ecb975ff75ff742c68ce8df417016c048959b0f807675430f6d981478e26d48e594e0830a0406da9817f8a1ecb94bd8be1f9281eeb5e952a82173c72bb", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "83abfb2a166f7af708526a9bdd2767c4be3cd231c9bc4e2f047a80df88a2860c", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json b/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json new file mode 100644 index 000000000..ccd2ece06 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json @@ -0,0 +1 @@ +[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "8f75836ceb390dd4fc8c16bc4be52ca09b9c5aa0ab5bc16dcfdb344787b29ddfd76d877b0a2330bc8e904b233397c6bd124845d1b868e4951cb6daacea023c986bdf0c6ac28d73f65681d941ea96623bc23acc7c84dcfc1304686240d9171cfc", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "3011f5cac32f13e86ecc061e89ed6675c27a46ab6ecb1ec6f6e5f133ae1d0287", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json b/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json new file mode 100644 index 000000000..2ab590830 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json @@ -0,0 +1 @@ +[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "a7706e102bfb0b986a5c8050044f7e221919463149771a92c3ca46ff7d4564867db48eaf89b5237fed8db2cdb9c9c057099d0982bbdb3fbfcbe0ab7259ad3f31f7713692b78ee25e6251982e7081d049804632b70b8a24d8c3e59b624a0bd221", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "8a26fbee0c3a99fe090af1fce68afc525b4e7efa70df72abaa91f29148b2f672", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "8b7aa5b0e97d15ec8c2281b919fde9e064f6ac064b163445ea99441ab063f9d10534bfde861b5606021ae46614ff075e0c2305ce5a6cbcc9f0bc8e7df1a177c4d969a5ed4ac062b0ea959bdac963fe206b73565a1a3937adcca736c6117c15f0", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "d38575167a94b516455c5b7e36d24310a612fa0f4580446c5f9d45e4e94f0642", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/watch/Cargo.toml b/watch/Cargo.toml index d1793a9d0..67cbc3cc2 100644 --- a/watch/Cargo.toml +++ b/watch/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "watch" version = "0.1.0" -edition = "2018" +edition = { workspace = true } [lib] name = "watch" @@ -14,32 +14,35 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "2.33.3" -log = "0.4.14" -env_logger = "0.9.0" -types = { path = "../consensus/types" } -eth2 = { path = "../common/eth2" } -beacon_node = { path = "../beacon_node"} -tokio = { version = "1.14.0", features = ["time"] } -axum = "0.5.15" -hyper = "0.14.20" -serde = "1.0.116" -serde_json = "1.0.58" -reqwest = { version = "0.11.0", features = ["json","stream"] } -url = "2.2.2" -rand = "0.7.3" +clap = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +types = { workspace = true } +eth2 = { workspace = true } +beacon_node = { workspace = true } +tokio = { workspace = true } +axum = "0.6.18" +hyper = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true } +url = { workspace = true } +rand = { workspace = true } diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } diesel_migrations = { version = "2.0.0", features = ["postgres"] } -byteorder = "1.4.3" -bls = { path = "../crypto/bls" } -hex = "0.4.2" -r2d2 = "0.8.9" -serde_yaml = "0.8.24" +byteorder = { workspace = true } +bls = { workspace = true } +hex = { workspace = true } +r2d2 = { workspace = true } +serde_yaml = { workspace = true } [dev-dependencies] tokio-postgres = "0.7.5" -http_api = { path = "../beacon_node/http_api" } -beacon_chain = { path = "../beacon_node/beacon_chain" } -network = { path = "../beacon_node/network" } -testcontainers = "0.14.0" -unused_port = { path = "../common/unused_port" } +http_api = { workspace = true } +beacon_chain = { workspace = true } +network = { workspace = true } +# TODO: update to 0.15 when released: https://github.com/testcontainers/testcontainers-rs/issues/497 +testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs/", rev = "0f2c9851" } +unused_port = { workspace = true } +task_executor = { workspace = true } +logging = { workspace = true } diff --git a/watch/README.md b/watch/README.md index 18bf39394..34519e52e 100644 --- a/watch/README.md +++ b/watch/README.md @@ -12,7 +12,7 @@ data which is: ### Requirements - `git` - `rust` : https://rustup.rs/ -- `libpg` : https://www.postgresql.org/download/ +- `libpq` : https://www.postgresql.org/download/ - `diesel_cli` : ``` cargo install diesel_cli --no-default-features --features postgres diff --git a/watch/src/server/mod.rs b/watch/src/server/mod.rs index 09d5ec6aa..d8ae0eb6c 100644 --- a/watch/src/server/mod.rs +++ b/watch/src/server/mod.rs @@ -5,7 +5,6 @@ use crate::config::Config as FullConfig; use crate::database::{self, PgPool}; use crate::suboptimal_attestations::{attestation_routes, blockprint_attestation_routes}; use axum::{ - handler::Handler, http::{StatusCode, Uri}, routing::get, Extension, Json, Router, @@ -104,7 +103,7 @@ pub fn start_server( } let app = routes - .fallback(route_not_found.into_service()) + .fallback(route_not_found) .layer(Extension(pool)) .layer(Extension(slots_per_epoch)); diff --git a/watch/tests/tests.rs b/watch/tests/tests.rs index acdda8c30..dc0b8af6e 100644 --- a/watch/tests/tests.rs +++ b/watch/tests/tests.rs @@ -1,17 +1,27 @@ #![recursion_limit = "256"] #![cfg(unix)] -use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, +use beacon_chain::{ + test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, + ChainConfig, }; use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use http_api::test_utils::{create_api_server, ApiServer}; +use log::error; +use logging::test_logger; use network::NetworkReceivers; - use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; +use std::collections::HashMap; +use std::env; +use std::net::SocketAddr; +use std::time::Duration; +use testcontainers::{clients::Cli, core::WaitFor, Image, RunnableImage}; use tokio::sync::oneshot; +use tokio::{runtime, task::JoinHandle}; +use tokio_postgres::{config::Config as PostgresConfig, Client, NoTls}; use types::{Hash256, MainnetEthSpec, Slot}; +use unused_port::unused_tcp4_port; use url::Url; use watch::{ client::WatchHttpClient, @@ -21,14 +31,40 @@ use watch::{ updater::{handler::*, run_updater, Config as UpdaterConfig, WatchSpec}, }; -use log::error; -use std::net::SocketAddr; -use std::time::Duration; -use tokio::{runtime, task::JoinHandle}; -use tokio_postgres::{config::Config as PostgresConfig, Client, NoTls}; -use unused_port::unused_tcp4_port; +#[derive(Debug)] +pub struct Postgres(HashMap<String, String>); -use testcontainers::{clients::Cli, images::postgres::Postgres, RunnableImage}; +impl Default for Postgres { + fn default() -> Self { + let mut env_vars = HashMap::new(); + env_vars.insert("POSTGRES_DB".to_owned(), "postgres".to_owned()); + env_vars.insert("POSTGRES_HOST_AUTH_METHOD".into(), "trust".into()); + + Self(env_vars) + } +} + +impl Image for Postgres { + type Args = (); + + fn name(&self) -> String { + "postgres".to_owned() + } + + fn tag(&self) -> String { + "11-alpine".to_owned() + } + + fn ready_conditions(&self) -> Vec<WaitFor> { + vec![WaitFor::message_on_stderr( + "database system is ready to accept connections", + )] + } + + fn env_vars(&self) -> Box<dyn Iterator<Item = (&String, &String)> + '_> { + Box::new(self.0.iter()) + } +} type E = MainnetEthSpec; @@ -36,6 +72,11 @@ const VALIDATOR_COUNT: usize = 32; const SLOTS_PER_EPOCH: u64 = 32; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); +/// Set this environment variable to use a different hostname for connecting to +/// the database. Can be set to `host.docker.internal` for docker-in-docker +/// setups. +const WATCH_HOST_ENV_VARIABLE: &str = "WATCH_HOST"; + fn build_test_config(config: &DatabaseConfig) -> PostgresConfig { let mut postgres_config = PostgresConfig::new(); postgres_config @@ -71,17 +112,25 @@ pub async fn create_test_database(config: &DatabaseConfig) { .expect("Database creation failed"); } +pub fn get_host_from_env() -> String { + env::var(WATCH_HOST_ENV_VARIABLE).unwrap_or_else(|_| "localhost".to_string()) +} + struct TesterBuilder { pub harness: BeaconChainHarness<EphemeralHarnessType<E>>, pub config: Config, _bn_network_rx: NetworkReceivers<E>, - _bn_api_shutdown_tx: oneshot::Sender<()>, } impl TesterBuilder { pub async fn new() -> TesterBuilder { let harness = BeaconChainHarness::builder(E::default()) .default_spec() + .chain_config(ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }) + .logger(test_logger()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .build(); @@ -92,10 +141,14 @@ impl TesterBuilder { let ApiServer { server, listening_socket: bn_api_listening_socket, - shutdown_tx: _bn_api_shutdown_tx, network_rx: _bn_network_rx, .. - } = create_api_server(harness.chain.clone(), harness.logger().clone()).await; + } = create_api_server( + harness.chain.clone(), + &harness.runtime, + harness.logger().clone(), + ) + .await; tokio::spawn(server); /* @@ -107,6 +160,7 @@ impl TesterBuilder { database: DatabaseConfig { dbname: random_dbname(), port: database_port, + host: get_host_from_env(), ..Default::default() }, server: ServerConfig { @@ -128,7 +182,6 @@ impl TesterBuilder { harness, config, _bn_network_rx, - _bn_api_shutdown_tx, } } pub async fn build(self, pool: PgPool) -> Tester { @@ -175,7 +228,6 @@ impl TesterBuilder { config: self.config, updater, _bn_network_rx: self._bn_network_rx, - _bn_api_shutdown_tx: self._bn_api_shutdown_tx, _watch_shutdown_tx, } } @@ -193,7 +245,6 @@ struct Tester { pub config: Config, pub updater: UpdateHandler<E>, _bn_network_rx: NetworkReceivers<E>, - _bn_api_shutdown_tx: oneshot::Sender<()>, _watch_shutdown_tx: oneshot::Sender<()>, }